import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; import { ALL_TRADE_DIRECTIONS, TRADE_DIRECTION_LABELS, userTradeAccessService, type TradeDirection, } from "@/services/userTradeAccess.service"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; const QUERY_KEY = ["user-trade-access", "list"] as const; type EmployeeRow = { userId: string; name: string; email: string; }; /** * Per-user trade-direction access (Import / Export / Intercity checkboxes). * All three checked (or never configured) = unrestricted; unchecking limits * the user's contracts, bookings, schedules, batch board, payments, invoices * and overview to the checked directions. Admins always bypass the scope. */ export default function TradeAccessPage() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); const { data: usersResponse, isLoading: usersLoading } = useQuery({ queryKey: ["staff-users", "employees"], queryFn: userTradeAccessService.employees, }); const { data: configs, isLoading: configsLoading } = useQuery({ queryKey: QUERY_KEY, queryFn: userTradeAccessService.list, }); const saveMutation = useMutation({ mutationFn: ({ userId, directions, }: { userId: string; directions: TradeDirection[]; }) => userTradeAccessService.set(userId, directions), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: QUERY_KEY }); void queryClient.invalidateQueries({ queryKey: ["user-trade-access", "me"], }); toast.success("Trade access updated"); }, }); const configByUser = useMemo(() => { const map = new Map(); for (const row of configs ?? []) map.set(row.userId, row.directions); return map; }, [configs]); const rows: EmployeeRow[] = useMemo(() => { const items = usersResponse?.items ?? []; const mapped = items .map((u) => ({ userId: u.id ?? "", name: u.name?.en ?? u.username ?? "—", email: u.email ?? "", })) .filter((r: EmployeeRow) => r.userId); const term = search.trim().toLowerCase(); if (!term) return mapped; return mapped.filter( (r: EmployeeRow) => r.name.toLowerCase().includes(term) || r.email.toLowerCase().includes(term), ); }, [usersResponse, search]); // No row yet = unrestricted, so render as all three checked. const directionsFor = (userId: string): TradeDirection[] => configByUser.get(userId) ?? [...ALL_TRADE_DIRECTIONS]; const toggle = (userId: string, direction: TradeDirection) => { const current = directionsFor(userId); const next = current.includes(direction) ? current.filter((d) => d !== direction) : [...current, direction]; saveMutation.mutate({ userId, directions: next }); }; const loading = usersLoading || configsLoading; return (

Trade direction access

Choose which trade directions each backoffice user can see. This filters their contracts, bookings, schedules, batch board, payments, invoices and overview. All three checked means full access; super and organization admins are never restricted.

setSearch(e.target.value)} placeholder="Search by name or email…" className="w-full max-w-sm rounded-md border px-3 py-2 text-sm" /> {loading ? (

Loading users…

) : ( User Email {ALL_TRADE_DIRECTIONS.map((d) => ( {TRADE_DIRECTION_LABELS[d]} ))} Access {rows.map((row) => { const dirs = directionsFor(row.userId); const unrestricted = dirs.length === ALL_TRADE_DIRECTIONS.length; return ( {row.name} {row.email} {ALL_TRADE_DIRECTIONS.map((d) => ( toggle(row.userId, d)} aria-label={`${row.name} — ${TRADE_DIRECTION_LABELS[d]}`} /> ))} {unrestricted ? ( Full access ) : dirs.length === 0 ? ( No data ) : ( {dirs.map((d) => TRADE_DIRECTION_LABELS[d]).join(" + ")}{" "} only )} ); })} {rows.length === 0 && ( No users found. )}
)}
); }