import type { EmploymentStatus } from "./supabase/types"; // The SQL counterpart of deriveStatusAsOf() in lib/reports.ts. // // The employee list is paged in the database, so it cannot derive status in // JavaScript the way the reports and the dashboard do — it has to filter on // the server. Filtering on the `employees.status` column instead was the // reason a dashboard tile and the list it links to could disagree: that // column holds whatever the last mutation or cron run wrote, while every // other surface computes status from entry/exit/karenz dates. // // Kept deliberately close to deriveStatusAsOf, clause for clause: // // entry_date > asOf -> Geplant // exit_date <= asOf -> Ausgetreten // karenz window covers asOf -> Karenz // otherwise -> Aktiv // // tests/integration/employee-status-filter.test.ts asserts the two agree // against a real database, which is the only place that can prove it. type Filterable = { gt: (column: string, value: string) => Filterable; lte: (column: string, value: string) => Filterable; gte: (column: string, value: string) => Filterable; or: (filters: string) => Filterable; is: (column: string, value: null) => Filterable; not: (column: string, operator: string, value: null) => Filterable; }; /** True once the person has started and has not left yet. */ function employed(query: Q, asOf: string): Q { return query.lte("entry_date", asOf).or(`exit_date.is.null,exit_date.gt.${asOf}`) as Q; } /** * Narrows a PostgREST query to the employees whose *derived* status on * `asOf` is one of `statuses`. Only the combinations the UI offers are * supported; anything else is left unfiltered rather than silently applying * a wrong one. */ export function applyDerivedStatusFilter(query: Q, statuses: EmploymentStatus[], asOf: string): Q { const wanted = new Set(statuses); if (wanted.size === 0) return query; // A single non-employed status is a straight date comparison. if (wanted.size === 1 && wanted.has("Geplant")) return query.gt("entry_date", asOf) as Q; if (wanted.size === 1 && wanted.has("Ausgetreten")) return query.not("exit_date", "is", null).lte("exit_date", asOf) as Q; const wantsAktiv = wanted.has("Aktiv"); const wantsKarenz = wanted.has("Karenz"); if (wantsAktiv && wantsKarenz && wanted.size === 2) { // Everyone employed today, whether or not they are on leave. return employed(query, asOf); } if (wantsKarenz && !wantsAktiv && wanted.size === 1) { return employed(query, asOf) .not("karenz_start_date", "is", null) .lte("karenz_start_date", asOf) .or(`karenz_return_date.is.null,karenz_return_date.gt.${asOf}`) as Q; } if (wantsAktiv && !wantsKarenz && wanted.size === 1) { // Employed but *not* inside a karenz window: either no start date, a // start still ahead, or a return that has already happened. return employed(query, asOf).or( `karenz_start_date.is.null,karenz_start_date.gt.${asOf},karenz_return_date.lte.${asOf}` ) as Q; } // Mixed selections spanning employed and non-employed states have no UI // path today; filtering on a guess would be worse than not filtering. return query; }