import type { Expression, ExpressionBuilder, SqlBool } from "kysely"; import type { Schema } from "./db/schema"; 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 Eb = ExpressionBuilder; /** True once the person has started and has not left yet. */ function employed(eb: Eb, asOf: string): Expression { return eb.and([eb("entry_date", "<=", asOf), eb.or([eb("exit_date", "is", null), eb("exit_date", ">", asOf)])]); } /** * Die Bedingung für die Menge, deren *abgeleiteter* Status am Stichtag einer * der genannten ist — oder null, wenn nicht eingeschränkt werden soll. * * Nur die Kombinationen, die die Oberfläche anbietet, sind abgedeckt. Für * alles andere kommt null zurück: lieber nicht filtern als falsch filtern. */ export function derivedStatusFilter(eb: Eb, statuses: EmploymentStatus[], asOf: string): Expression | null { const wanted = new Set(statuses); if (wanted.size === 0) return null; // A single non-employed status is a straight date comparison. if (wanted.size === 1 && wanted.has("Geplant")) return eb("entry_date", ">", asOf); if (wanted.size === 1 && wanted.has("Ausgetreten")) { return eb.and([eb("exit_date", "is not", null), eb("exit_date", "<=", asOf)]); } const wantsAktiv = wanted.has("Aktiv"); const wantsKarenz = wanted.has("Karenz"); // Everyone employed today, whether or not they are on leave. if (wantsAktiv && wantsKarenz && wanted.size === 2) return employed(eb, asOf); if (wantsKarenz && !wantsAktiv && wanted.size === 1) { return eb.and([ employed(eb, asOf), eb("karenz_start_date", "is not", null), eb("karenz_start_date", "<=", asOf), eb.or([eb("karenz_return_date", "is", null), eb("karenz_return_date", ">", asOf)]), ]); } 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 eb.and([ employed(eb, asOf), eb.or([ eb("karenz_start_date", "is", null), eb("karenz_start_date", ">", asOf), eb("karenz_return_date", "<=", asOf), ]), ]); } // Mixed selections spanning employed and non-employed states have no UI // path today; filtering on a guess would be worse than not filtering. return null; }