From e04b69ce49417721acde590ccbbb14771af2472a Mon Sep 17 00:00:00 2001 From: Gulum Date: Fri, 5 Jun 2026 23:50:30 +0200 Subject: [PATCH] FEAT-1: add Gridify query builder + API DTO types Co-Authored-By: Claude Opus 4.8 (1M context) --- gerbil-manager-web/src/api/gridify.ts | 68 +++++++++++++++++++ gerbil-manager-web/src/api/types.ts | 95 +++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 gerbil-manager-web/src/api/gridify.ts create mode 100644 gerbil-manager-web/src/api/types.ts diff --git a/gerbil-manager-web/src/api/gridify.ts b/gerbil-manager-web/src/api/gridify.ts new file mode 100644 index 0000000..19bf8dd --- /dev/null +++ b/gerbil-manager-web/src/api/gridify.ts @@ -0,0 +1,68 @@ +/** + * Gridify query-string builder. + * + * The backend list endpoints accept Gridify query params: `filter`, `orderBy`, + * `page`, `pageSize`. This module turns typed UI filter state into a Gridify + * `filter` string and assembles the query string. + * + * Gridify filter syntax (subset we use): + * field==value equals + * field=*value* contains (append /i for case-insensitive) + * a==1,b==2 AND (comma) + * a==1|b==2 OR (pipe) + * Special characters in values (comma, pipe, parens, =, <, >, !, asterisk, + * backslash) must be escaped with a leading backslash. + */ + +export interface GridifyQuery { + /** Gridify filter expression, already composed. */ + filter?: string + /** Gridify orderBy expression, e.g. "name" or "dateOfBirth desc". */ + orderBy?: string + /** 1-based page number. */ + page?: number + pageSize?: number +} + +/** Escape a value for use inside a Gridify filter expression. */ +export function escapeGridifyValue(value: string): string { + return value.replace(/([,|()=<>!*\\])/g, '\\$1') +} + +export type GridifyOp = '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' + +export interface FilterCondition { + field: string + op: GridifyOp + value: string | number | boolean + /** Case-insensitive match (appends /i). Only meaningful for string ops. */ + caseInsensitive?: boolean +} + +/** Build a single Gridify condition, e.g. a name-contains or `status==Active`. */ +export function condition(c: FilterCondition): string { + const escaped = escapeGridifyValue(String(c.value)) + if (c.op === 'contains') { + // Gridify "contains" operator is =* ; default to case-insensitive. + const suffix = c.caseInsensitive === false ? '' : '/i' + return `${c.field}=*${escaped}*${suffix}` + } + const suffix = c.caseInsensitive ? '/i' : '' + return `${c.field}${c.op}${escaped}${suffix}` +} + +/** Join conditions with AND (comma). Falsy/empty conditions are dropped. */ +export function andFilter(...conditions: Array): string { + return conditions.filter((c): c is string => Boolean(c)).join(',') +} + +/** Assemble the URL query string ("?...") from a GridifyQuery. */ +export function toQueryString(q: GridifyQuery): string { + const params = new URLSearchParams() + if (q.filter) params.set('filter', q.filter) + if (q.orderBy) params.set('orderBy', q.orderBy) + if (q.page != null) params.set('page', String(q.page)) + if (q.pageSize != null) params.set('pageSize', String(q.pageSize)) + const s = params.toString() + return s ? `?${s}` : '' +} diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts new file mode 100644 index 0000000..158db4b --- /dev/null +++ b/gerbil-manager-web/src/api/types.ts @@ -0,0 +1,95 @@ +/** + * API DTO types — mirror the backend (DATA-1/DATA-2) contract. + * + * Code is English/camelCase (matching the DTOs); German is UI-only (de.ts). + * Enums are string unions matching the C# member names verbatim (assumes + * JsonStringEnumConverter on the backend — confirmed with DATA-2). + */ + +/** Gerbil.Gender — C# enum { unknown, male, female }. */ +export type Gender = 'unknown' | 'male' | 'female' +export const GENDERS: Gender[] = ['unknown', 'male', 'female'] + +/** Gerbil.Status — C# enum { Active, Deceased, GivenAway }. */ +export type GerbilStatus = 'Active' | 'Deceased' | 'GivenAway' +export const GERBIL_STATUSES: GerbilStatus[] = ['Active', 'Deceased', 'GivenAway'] + +/** ISO date string "YYYY-MM-DD" (maps to C# DateOnly). */ +export type DateOnlyString = string + +export interface Gerbil { + id: string + name: string + gender: Gender + status: GerbilStatus + dateOfBirth: DateOnlyString | null + dateOfDeath: DateOnlyString | null + causeOfDeath: string | null + goHomeDate: DateOnlyString | null + litterId: string | null + enclosureId: string | null + colorVarietyId: string | null + originContactId: string | null + receiverContactId: string | null + /** Compact GEN-1 genotype string, e.g. "Aa CC Dd EE GG Pp Spsp rere" (or "?"-wildcards). */ + genotype: string | null + notes: string | null +} + +/** Payload for POST /gerbils. */ +export interface CreateGerbil { + name: string + gender: Gender + status?: GerbilStatus + dateOfBirth?: DateOnlyString | null + dateOfDeath?: DateOnlyString | null + causeOfDeath?: string | null + goHomeDate?: DateOnlyString | null + litterId?: string | null + enclosureId?: string | null + colorVarietyId?: string | null + originContactId?: string | null + receiverContactId?: string | null + genotype?: string | null + notes?: string | null +} + +/** Payload for PUT /gerbils/{id} (all optional / partial update). */ +export type UpdateGerbil = Partial + +export interface ColorVariety { + id: string + name: string + canonicalGenotype: string | null + sortOrder: number +} + +export interface Enclosure { + id: string + name: string + notes: string | null +} + +export interface Contact { + id: string + name: string + contactInfo: string | null + notes: string | null +} + +export interface Litter { + id: string + name: string + date: DateOnlyString + totalBorn: number | null + fatherId: string | null + motherId: string | null +} + +/** Paged list envelope returned by Gridify list endpoints. */ +export interface Paged { + items: T[] + totalCount: number + page: number + pageSize: number +}