> ## Documentation Index
> Fetch the complete documentation index at: https://cona.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Filtering system with nuqs

# Filtering System with nuqs Integration

## Overview

CONA uses a sophisticated filtering system that combines [nuqs](https://nuqs.47ng.com/) for URL state management with Prisma database queries. This system provides type-safe, URL-synchronized filtering across all data tables in the application.

## Architecture

### Core Components

1. **nuqs** - Handles URL state management and type-safe query parameter parsing
2. **Filter Mapping** - Maps URL parameters to database fields and types
3. **Filter Utils** - Processes and applies filters to Prisma queries
4. **Filter Components** - UI components for different filter types

### Flow Diagram

```
URL Parameters → nuqs → Search Actions → Filter Utils → Prisma Query → Database
                ↓
         UI Components (synchronized)
```

## nuqs Integration

### Search Parameters Configuration

The core nuqs configuration is in `apps/webapp/app/lib/search-params-config.ts`:

```typescript theme={null}
import {
  createSearchParamsCache,
  parseAsString,
  parseAsBoolean,
  parseAsInteger,
} from "nuqs/server";

export const searchParamsCache = createSearchParamsCache(
  {
    // Pagination
    page: parseAsInteger.withDefault(1),
    pageSize: parseAsInteger.withDefault(10),

    // Filtering
    filter: parseAsBoolean.withDefault(false),
    tab: parseAsString.withDefault("published"),

    // Sorting
    sort: parseAsString.withDefault(""),
    asc: parseAsBoolean.withDefault(true),

    // Dynamic filters handled separately in actions
  },
  {
    urlKeys: {
      pageSize: "size", // URL shows 'size' but internally uses 'pageSize'
    },
  }
);
```

### URL Parameter Structure

The filtering system uses a specific URL parameter structure:

```
/sales-order?filter=true&doc_date=2025-07-07&doc_date_op=equals&status=Draft&page=1&size=10
```

**Parameter Types:**

* `filter=true` - Enables filtering
* `{field}={value}` - Filter value
* `{field}_op={operator}` - Filter operator (optional)
* `page={number}` - Pagination page
* `size={number}` - Page size
* `sort={field}` - Sort field
* `asc={boolean}` - Sort direction

## Filter Types and Operators

### String Filters

**Available Operators:**

* `equals` - Exact match
* `contains` - Partial match (default)
* `startsWith` - Starts with value
* `endsWith` - Ends with value
* `not` - Not equal to

**Examples:**

```
?filter=true&doc_nr=INV-001&doc_nr_op=equals
?filter=true&doc_receiver_name=John&doc_receiver_name_op=contains
```

### Date Filters

**Available Operators:**

* `equals` - Exact date match (default)
* `gte` - Greater than or equal
* `lte` - Less than or equal
* `gt` - Greater than
* `lt` - Less than
* `between` - Date range

**Timezone-Aware Date Handling:**
Date filters now properly handle timezones using the organization's default timezone. When users enter dates like `2025-07-07`, the system:

1. Gets the organization's default timezone from subsidiary settings
2. Interprets the date in that timezone (e.g., `2025-07-07` in `Europe/Berlin`)
3. Converts the full day range to UTC for database queries
4. Ensures users get results for the complete day in their timezone

**Example:**

* User selects: `2025-07-07`
* Organization timezone: `Europe/Berlin` (UTC+1)
* System converts: `2025-07-06T23:00:00Z` to `2025-07-07T22:59:59Z` (UTC)
* Result: All documents created on July 7th Berlin time

**Examples:**

```
?filter=true&doc_date=2025-07-07                    # Equals (default)
?filter=true&doc_date=2025-07-07&doc_date_op=gte    # From date
?filter=true&doc_date=2025-07-01,2025-07-31         # Date range (auto-detects 'between')
```

### Number Filters

**Available Operators:**

* `equals` - Exact match (default)
* `gt` - Greater than
* `gte` - Greater than or equal
* `lt` - Less than
* `lte` - Less than or equal
* `between` - Number range

**Examples:**

```
?filter=true&doc_total_amount=100.50                   # Equals
?filter=true&doc_total_amount=100&doc_total_amount_op=gte  # Greater than or equal
?filter=true&doc_total_amount=100,500                 # Range (auto-detects 'between')
```

### Array Filters

**Available Operators:**

* `in` - Contains any of the values (default)

**Examples:**

```
?filter=true&doc_tags=Important,Urgent&doc_tags_op=in
```

### Boolean Filters

**Available Operators:**

* `equals` - Exact match (default)
* `not` - Not equal to

**Examples:**

```
?filter=true&is_credit=true
?filter=true&is_credit=false&is_credit_op=not
```

## Filter Mapping

The `filter-mapping.ts` file defines how URL parameters map to database fields:

```typescript theme={null}
export const filterMapping: Record<string, FilterMappingConfig> = {
  doc_date: {
    field: "date",           // Database field
    type: "date",            // Filter type
    filterType: "date",      // UI component type
  },
  doc_nr: {
    field: "nr",
    type: "string",
    filterType: "text",
  },
  status: {
    field: "status.label",   // Nested field
    type: "string",
    filterType: "select",
  },
  // ... more mappings
};
```

## Implementation in Search Actions

### Basic Search Action Structure

```typescript theme={null}
"use server";

import { searchParamsCache } from "@/app/lib/search-params-config";
import { applyFilter } from "@/app/lib/utils/filter-utils";
import { filterMapping } from "@/app/lib/utils/filter-mapping";

export async function searchDocuments({
  objectTypeSlug,
  searchParams,
}: SearchDocumentsParams) {
  // 1. Parse nuqs parameters
  await searchParamsCache.parse(searchParams);
  const params = searchParamsCache.all();

  // 2. Build base where clause
  let where: DocumentWhereInput = {
    is_deleted: false,
    object_type: { slug: objectTypeSlug },
  };

  // 3. Apply filters if enabled
  if (params.filter) {
    Object.entries(searchParams).forEach(([key, value]) => {
      if (key in filterMapping && value) {
        const config = filterMapping[key];
        const operator = searchParams[`${key}_op`] || getDefaultOperator(config.type);
        
        where = applyFilter(where, config.field, value, operator);
      }
    });
  }

  // 4. Execute query
  const documents = await prisma.documents.findMany({ where });
  
  return { success: true, data: documents };
}
```

### Default Operator Logic

The system automatically determines operators based on field types:

```typescript theme={null}
function getDefaultOperator(type: string, value: any): string {
  switch (type) {
    case "string":
      return "contains";
    case "date":
      return value.includes(",") ? "between" : "equals";
    case "number":
      return value.includes(",") ? "between" : "equals";
    case "array":
      return "in";
    case "boolean":
      return "equals";
    default:
      return "contains";
  }
}
```

## Timezone-Aware Date Processing

The filtering system now properly handles timezone conversion using date-fns-tz:

### How It Works

1. **Get Organization Timezone**: Retrieves default timezone from subsidiary settings
2. **Parse User Input**: User-entered dates are interpreted in the organization's timezone
3. **Convert to UTC**: Uses proper timezone conversion with DST handling
4. **Database Queries**: All date comparisons are done in UTC with correct ranges
5. **Consistent Results**: Users get results for complete days in their timezone

### Implementation

```typescript theme={null}
// In search-documents.ts
const subsidiaryResult = await getDefaultSubsidiaryDetails({ organizationId });
const timezone = subsidiaryResult.success ? subsidiaryResult.data?.timezone || "UTC" : "UTC";

// Pass timezone to filter processing
where = applyFilter(where, config.field, value, operator, timezone);
```

```typescript theme={null}
// In filter-utils.ts using @cona/utils
import { convertUserDateToUtcRange, convertUserDateRangeToUtc } from "@cona/utils";

// Convert user date to proper UTC range
const { startOfDay, endOfDay } = convertUserDateToUtcRange(year, month, day, timezone || "UTC");
```

### Date Processing Example

```typescript theme={null}
// User enters: 2025-07-07
// Organization timezone: Europe/Berlin (UTC+1)
// System converts to UTC range: 
//   Start: 2025-07-06T23:00:00.000Z
//   End: 2025-07-07T22:59:59.999Z
// Database query: WHERE date >= '2025-07-06T23:00:00.000Z' AND date <= '2025-07-07T22:59:59.999Z'
// Result: All documents created on July 7th Berlin time
```

### Benefits

* **Timezone Accuracy**: Proper handling of user timezones with DST support
* **User-Friendly**: Users see results for complete days in their timezone
* **Robust**: Uses date-fns-tz for reliable timezone conversion
* **Consistent**: Same behavior across different deployments and timezones

## Custom Properties Filtering

The system supports filtering by custom properties using a special syntax:

```typescript theme={null}
// URL: ?filter=true&custom_invoice_type=recurring
// This searches for documents with custom_properties.invoice_type.value = "recurring"

if (key.startsWith("custom_") && value) {
  const customPropertyKey = key.replace("custom_", "");
  const andConditions = Array.isArray(where.AND) ? where.AND : [];
  
  andConditions.push({
    custom_properties: {
      path: [customPropertyKey, "value"],
      string_contains: typeof value === "string" ? value : String(value),
    },
  });
  
  where.AND = andConditions;
}
```

## UI Components

### Filter Input Components

Each filter type has its own component:

* `TextFilter` - For string fields
* `NumberFilter` - For numeric fields
* `DateFilter` - For date fields
* `SelectFilter` - For dropdown selections
* `CreditDebitFilter` - For boolean credit/debit fields

### Base Filter Props

All filter components share common props:

```typescript theme={null}
interface BaseFilterProps<TData, TValue> {
  column: { columnDef: CustomColumnDef<TData, TValue> };
  value: string | null;
  setValue: (value: string | null) => void;
  operator?: string | null;
  setOperator?: (operator: string | null) => void;
  isChanging: boolean;
  setIsChanging: (isChanging: boolean) => void;
}
```

### Example Filter Component

```typescript theme={null}
export function DateFilter<TData, TValue>({
  value,
  setValue,
  operator,
  setOperator,
}: BaseFilterProps<TData, TValue>) {
  return (
    <div className="flex gap-0">
      <Select
        value={operator ?? "equals"}
        onValueChange={setOperator}
      >
        <SelectTrigger>
          <SelectValue>{operatorSymbols[operator]}</SelectValue>
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="equals">Equal to =</SelectItem>
          <SelectItem value="gt">After &gt;</SelectItem>
          <SelectItem value="gte">From ≥</SelectItem>
          <SelectItem value="lt">Before &lt;</SelectItem>
          <SelectItem value="lte">Until ≤</SelectItem>
          <SelectItem value="between">Between ↔</SelectItem>
        </SelectContent>
      </Select>
      
      <DatePicker
        value={value}
        onChange={setValue}
      />
    </div>
  );
}
```

## Data Processing

### Filter Utils

The `filter-utils.ts` file handles the core filtering logic:

```typescript theme={null}
export function applyFilter(
  where: WhereInput,
  field: string,
  value: any,
  operator: string = "contains"
): WhereInput {
  // 1. Parse and validate value
  const fieldType = getFieldType(field);
  let processedValue = processValue(value, fieldType);

  // 2. Apply operator
  const operatorValue = applyOperator(processedValue, operator, fieldType);

  // 3. Handle nested fields
  if (field.includes(".")) {
    return buildNestedWhere(where, field, operatorValue);
  }

  // 4. Return updated where clause
  return { ...where, [field]: operatorValue };
}
```

### Value Processing

Values are processed based on their type:

```typescript theme={null}
function processValue(value: any, fieldType: string): any {
  switch (fieldType) {
    case "date":
      if (typeof value === "string") {
        const [year, month, day] = value.split("-").map(Number);
        return new Date(Date.UTC(year, month - 1, day));
      }
      return value;
      
    case "number":
      if (typeof value === "string") {
        return parseFloat(value.replace(/[^\d,.,-]/g, "").replace(",", "."));
      }
      return value;
      
    case "boolean":
      return typeof value === "string" ? value.toLowerCase() === "true" : Boolean(value);
      
    default:
      return value;
  }
}
```

## Common Issues and Solutions

### Issue 1: Date Filtering Not Working

**Problem:** Setting `doc_date=2025-07-07` doesn't filter correctly.

**Solution:** Ensure the default operator is `equals` and timezone is handled properly:

```typescript theme={null}
// Fixed in search-documents.ts
operator = "equals";  // Changed from "gte" to "equals"

// Get organization timezone
const subsidiaryResult = await getDefaultSubsidiaryDetails({ organizationId });
const timezone = subsidiaryResult.success ? subsidiaryResult.data?.timezone || "UTC" : "UTC";

// Pass timezone to filter processing
where = applyFilter(where, config.field, value, operator, timezone);
```

### Issue 2: Custom Properties Not Filtering

**Problem:** Custom property filters don't work.

**Solution:** Use the `custom_` prefix in URL parameters:

```
?filter=true&custom_invoice_type=recurring
```

### Issue 3: Nested Field Filtering

**Problem:** Filtering on related entity fields fails.

**Solution:** Use dot notation in filter mapping:

```typescript theme={null}
doc_receiver_name: {
  field: "entity.label",  // Nested field
  type: "string",
}
```

### Issue 4: Number Format Issues

**Problem:** European number formats (1.234,56) not working.

**Solution:** The system handles both formats automatically:

```typescript theme={null}
const normalized = value
  .replace(/[^\d,.,-]/g, "")
  .replace(/[.,](?=.*[.,])/g, "");
const numericValue = parseFloat(normalized.replace(",", "."));
```

### Issue 5: Date Filtering and Timezones

**Problem:** Date filters show incorrect results due to timezone differences.

**✅ Solution Implemented:** The system now properly handles timezone conversion:

```typescript theme={null}
// Implemented timezone-aware filtering:
// 1. Uses date-fns-tz library for accurate timezone conversion
// 2. Gets organization timezone from subsidiary settings
// 3. Converts user dates to proper UTC ranges

// In @cona/utils/src/date.ts
export function convertUserDateToUtcRange(
  year: number,
  month: number,
  day: number,
  timezone: BusinessTimezone = "UTC"
): { startOfDay: Date; endOfDay: Date } {
  // Proper timezone conversion with DST handling
}

// In search actions
const timezone = subsidiaryResult.success ? subsidiaryResult.data?.timezone || "UTC" : "UTC";
where = applyFilter(where, config.field, value, operator, timezone);
```

**Benefits:**

* Users get complete day results in their timezone
* Proper DST handling with date-fns-tz
* No more half-day results from neighboring timezones

## Best Practices

### 1. Always Use Filter Mapping

Don't hardcode field mappings. Use the centralized `filterMapping` object:

```typescript theme={null}
// ✅ Good
const config = filterMapping[key];
where = applyFilter(where, config.field, value, operator);

// ❌ Bad
where = applyFilter(where, "some_field", value, operator);
```

### 2. Handle Optional Operators

Always provide fallback operators:

```typescript theme={null}
const operator = searchParams[`${key}_op`] || getDefaultOperator(config.type);
```

### 3. Validate Filter Values

Check for null/undefined values:

```typescript theme={null}
if (key in filterMapping && value) {
  // Apply filter
}
```

### 4. Use Consistent URL Keys

Map internal names to user-friendly URL keys:

```typescript theme={null}
urlKeys: {
  pageSize: "size",
}
```

### 5. Log Filter Operations

Add logging for debugging:

```typescript theme={null}
console.log("Processing filter:", { key, value, operator });
```

### 6. Handle Timezone for Date Filters

Always retrieve and pass timezone information for accurate date filtering:

```typescript theme={null}
// Get organization timezone
const subsidiaryResult = await getDefaultSubsidiaryDetails({ organizationId });
const timezone = subsidiaryResult.success ? subsidiaryResult.data?.timezone || "UTC" : "UTC";

// Pass timezone to filter processing
where = applyFilter(where, config.field, value, operator, timezone);

// User enters: 2025-07-07 in Europe/Berlin
// System processes: 2025-07-06T23:00:00.000Z to 2025-07-07T22:59:59.999Z (UTC)
// Result: Complete day in user's timezone
```

## Testing Filters

### Unit Tests

Test the filter utility functions:

```typescript theme={null}
describe("applyFilter", () => {
  it("should handle date equals correctly", () => {
    const where = {};
    const result = applyFilter(where, "date", "2025-07-07", "equals");
    
    expect(result.date.gte).toBeDefined();
    expect(result.date.lte).toBeDefined();
  });
});
```

### Integration Tests

Test complete filtering workflows:

```typescript theme={null}
describe("Document Filtering", () => {
  it("should filter by date correctly", async () => {
    const result = await searchDocuments({
      objectTypeSlug: "sales-order",
      searchParams: {
        filter: "true",
        doc_date: "2025-07-07",
      },
    });
    
    expect(result.success).toBe(true);
    expect(result.data.documents).toHaveLength(1);
  });
});
```

## Future Enhancements

### 1. Advanced Operators

Add more sophisticated operators:

* `regex` - Regular expression matching
* `fuzzy` - Fuzzy text matching
* `geospatial` - Location-based filtering

### 2. Filter Presets

Allow users to save and reuse filter combinations:

```typescript theme={null}
const presets = {
  "overdue-invoices": {
    object_type: "sales-invoice",
    doc_due_date_op: "lt",
    doc_due_date: "today",
    status: "Sent",
  },
};
```

### 3. Real-time Filtering

Implement debounced real-time filtering:

```typescript theme={null}
const debouncedFilter = useDebouncedCallback(
  (filters) => updateFilters(filters),
  500
);
```

***

This filtering system provides a powerful, type-safe, and URL-synchronized filtering experience across all CONA data tables. The nuqs integration ensures that filter states are preserved in URLs and can be shared or bookmarked by users.
