fix(shifts): operator filter — select over real operators, no more focus loss
The admin operator filter was a free-text input that broke three ways at once: its visibility hangs off the query response (scope === "all") and its value is part of the query key, so every keystroke started a new query, data went undefined for the round-trip, and the input UNMOUNTED mid-keystroke (lost focus, list blanking that read as a page reload). Filtering also silently failed — the server matches the operator by exact username, so partial text matched nothing. - keepPreviousData on the shifts query: previous data (and scope) stays live during refetch, so filter controls never unmount and the list never blanks on preset/filter changes. - The filter is now a <select> of operators that HAVE shifts: the server returns the distinct list (signed z-reports + the open shift's holder) on GET /api/shifts, admin scope only — operators still can't see other names. Exact match by construction. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
closeShift,
|
||||
fetchEvents,
|
||||
@@ -105,9 +105,17 @@ export function ShiftsHistory({ user, canManage = false }: { user: SessionUser |
|
||||
to: range?.to ? new Date(`${range.to}T23:59:59`).toISOString() : undefined,
|
||||
};
|
||||
|
||||
const q = useQuery({ queryKey: ["shifts", applied], queryFn: () => fetchShifts(applied) });
|
||||
// keepPreviousData: every filter change makes a NEW query key; without it the
|
||||
// data (and with it `scope`) goes undefined for the fetch round-trip, which
|
||||
// unmounted the admin filter controls mid-interaction and blanked the list.
|
||||
const q = useQuery({
|
||||
queryKey: ["shifts", applied],
|
||||
queryFn: () => fetchShifts(applied),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
const isAdmin = q.data?.scope === "all";
|
||||
const closed = q.data?.shifts ?? [];
|
||||
const operators = q.data?.operators ?? [];
|
||||
|
||||
// The current/open shift sits at the TOP of the list (when present + visible to me).
|
||||
const list: (ShiftSummary & { open?: boolean })[] = current && (isMine || isAdmin) ? [current, ...closed] : closed;
|
||||
@@ -169,7 +177,14 @@ export function ShiftsHistory({ user, canManage = false }: { user: SessionUser |
|
||||
{isAdmin && (
|
||||
<div className="field">
|
||||
<span className="label">{t("shifts.operator")}</span>
|
||||
<input className="input w-44" value={operator} onChange={(e) => setOperator(e.target.value)} placeholder={t("shifts.allOperators")} />
|
||||
{/* A select over operators that HAVE shifts — the server filter is an
|
||||
exact username match, so free text could only miss. */}
|
||||
<select className="input w-44" value={operator} onChange={(e) => setOperator(e.target.value)}>
|
||||
<option value="">{t("shifts.allOperators")}</option>
|
||||
{operators.map((op) => (
|
||||
<option key={op} value={op}>{op}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1151,6 +1151,8 @@ export interface ShiftSummary extends ShiftSourceSplit {
|
||||
export function fetchShifts(params: { operator?: string; from?: string; to?: string } = {}): Promise<{
|
||||
shifts: ShiftSummary[];
|
||||
scope: "all" | "self";
|
||||
/** Admin scope only: every operator that has a shift — feeds the filter dropdown. */
|
||||
operators?: string[];
|
||||
}> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.operator) qs.set("operator", params.operator);
|
||||
|
||||
Reference in New Issue
Block a user