mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
201 lines
6.7 KiB
TypeScript
201 lines
6.7 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import toast from "react-hot-toast";
|
|
|
|
import { useAuth } from "@/auth/useAuth";
|
|
import { useEmployees } from "@/user-management/hooks/useEmployees";
|
|
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 { user } = useAuth();
|
|
const queryClient = useQueryClient();
|
|
const [search, setSearch] = useState("");
|
|
|
|
const organizationId =
|
|
user?.employee && user.employee.length > 0
|
|
? user.employee[0].organizationId
|
|
: undefined;
|
|
|
|
const { employeesResponseByOrg, isLoadingEmployeesByOrg } = useEmployees({
|
|
organizationId,
|
|
});
|
|
|
|
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<string, TradeDirection[]>();
|
|
for (const row of configs ?? []) map.set(row.userId, row.directions);
|
|
return map;
|
|
}, [configs]);
|
|
|
|
const rows: EmployeeRow[] = useMemo(() => {
|
|
const items = employeesResponseByOrg?.items ?? [];
|
|
const mapped = items
|
|
.map((item: { user?: { id?: string; name?: { en?: string }; email?: string; username?: string } }) => ({
|
|
userId: item.user?.id ?? "",
|
|
name: item.user?.name?.en ?? item.user?.username ?? "—",
|
|
email: item.user?.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),
|
|
);
|
|
}, [employeesResponseByOrg, 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 = isLoadingEmployeesByOrg || configsLoading;
|
|
|
|
return (
|
|
<div className="space-y-4 p-4">
|
|
<div>
|
|
<h1 className="text-xl font-bold">Trade direction access</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
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.
|
|
</p>
|
|
</div>
|
|
|
|
<input
|
|
type="search"
|
|
value={search}
|
|
onChange={(e) => 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 ? (
|
|
<p className="text-sm text-muted-foreground">Loading users…</p>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>User</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
{ALL_TRADE_DIRECTIONS.map((d) => (
|
|
<TableHead key={d} className="text-center">
|
|
{TRADE_DIRECTION_LABELS[d]}
|
|
</TableHead>
|
|
))}
|
|
<TableHead>Access</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((row) => {
|
|
const dirs = directionsFor(row.userId);
|
|
const unrestricted = dirs.length === ALL_TRADE_DIRECTIONS.length;
|
|
return (
|
|
<TableRow key={row.userId}>
|
|
<TableCell className="font-medium">{row.name}</TableCell>
|
|
<TableCell>{row.email}</TableCell>
|
|
{ALL_TRADE_DIRECTIONS.map((d) => (
|
|
<TableCell key={d} className="text-center">
|
|
<input
|
|
type="checkbox"
|
|
className="h-4 w-4 accent-primary"
|
|
checked={dirs.includes(d)}
|
|
disabled={saveMutation.isPending}
|
|
onChange={() => toggle(row.userId, d)}
|
|
aria-label={`${row.name} — ${TRADE_DIRECTION_LABELS[d]}`}
|
|
/>
|
|
</TableCell>
|
|
))}
|
|
<TableCell>
|
|
{unrestricted ? (
|
|
<span className="text-xs text-muted-foreground">
|
|
Full access
|
|
</span>
|
|
) : dirs.length === 0 ? (
|
|
<span className="text-xs font-medium text-red-600">
|
|
No data
|
|
</span>
|
|
) : (
|
|
<span className="text-xs font-medium text-amber-600">
|
|
{dirs.map((d) => TRADE_DIRECTION_LABELS[d]).join(" + ")}{" "}
|
|
only
|
|
</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
{rows.length === 0 && (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={3 + ALL_TRADE_DIRECTIONS.length}
|
|
className="text-center text-sm text-muted-foreground"
|
|
>
|
|
No users found.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|