Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/dropdown_settings/DropdownSettingsPage.tsx
Marshal 4b7f6d2548 enhance contract and booking services with server-side search and validation improvements
- Added  parameter to  and  for server-side free-text search on contract reference, company name, and booking details.
- Introduced new validation errors in  for container clashes and space issues when creating bookings.
- Implemented paginated dropdown settings retrieval in .
- Updated  to fetch active yards using a new method that handles pagination.
- Enhanced  with a  method to fetch all records by walking through pages.
- Refactored  to support filtering and pagination in schedule listings.
- Improved  to return a paginated list of facilities.
- Updated UI components in  and  to utilize debounced search inputs for better performance.
- Added alerts in  to inform users about booking constraints related to splits and capacity.
- Enhanced  to display notifications for split bookings and capacity usage.
2026-07-12 10:51:31 +00:00

365 lines
11 KiB
TypeScript

import { useMemo, useState } from "react";
import {
Boxes,
CheckCircle2,
Eye,
Filter,
ListOrdered,
MoreHorizontal,
Pencil,
Plus,
Search,
Settings,
Shield,
Sparkles,
Trash2,
} from "lucide-react";
import {
ActionIcon,
Badge,
Button,
Card,
Code,
Group,
Menu,
Text,
TextInput,
ThemeIcon,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import EditDropdownSettingDialog from "./EditDropdownSettingDialog";
import ManageDropdownOptionsDialog from "./ManageDropdownOptionsDialog";
import DeleteDropdownSettingDialog from "./DeleteDropdownSettingDialog";
import { useMutation, useQuery } from "@tanstack/react-query";
import { api } from "@/services/api";
import type { DropdownSetting } from "@/types/dropdownSettings";
import {
DataTable,
DataTableFooter,
type ColumnDef,
usePagination,
} from "@edr/ui-common";
type ActiveDialog = "edit" | "options" | "delete";
export default function DropdownSettingsPage() {
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [query, setQuery] = useState("");
const [debouncedQuery] = useDebouncedValue(query, 300);
const [createOpen, setCreateOpen] = useState(false);
const [activeDialog, setActiveDialog] = useState<ActiveDialog | null>(null);
const [activeSetting, setActiveSetting] = useState<DropdownSetting | null>(
null,
);
const openDialogFor = (dialog: ActiveDialog, setting: DropdownSetting) => {
setActiveSetting(setting);
setActiveDialog(dialog);
};
const closeDialog = () => setActiveDialog(null);
// Table data: server-side pagination + search via GET /dropdown-settings/paged.
const listQuery = useMemo(
() => ({
page: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
search: debouncedQuery.trim() || undefined,
}),
[pagination.pageIndex, pagination.pageSize, debouncedQuery],
);
const { data, isLoading, isError, error } = useQuery(
api.dropdownSettings.listPaged.queryOptions({ input: { query: listQuery } }),
);
// Full (unpaged) list feeds the KPI strip only — its aggregates span every
// setting, not just the current page.
const { data: allSettings, isLoading: kpiLoading } = useQuery(
api.dropdownSettings.list.queryOptions(),
);
const deleteMutation = useMutation(api.dropdownSettings.remove.mutationOptions());
const dropdownSettings = useMemo<DropdownSetting[]>(
() => (Array.isArray(allSettings) ? allSettings : []),
[allSettings],
);
const rows = data?.items ?? [];
const total = data?.meta.total ?? 0;
const pageCount = Math.max(1, data?.meta.totalPages ?? 1);
const totalOptions = dropdownSettings.reduce(
(sum, s) => sum + (s.children?.length ?? 0),
0,
);
const multipleCount = dropdownSettings.filter((s) => s.multiple).length;
const searchableCount = dropdownSettings.filter(
(s) => s.meta?.searchable,
).length;
const status: "loading" | "error" | "success" = isLoading
? "loading"
: isError
? "error"
: "success";
const columns: ColumnDef<DropdownSetting>[] = [
{
id: "setting",
header: "Setting",
cell: ({ row }) => {
const s = row.original;
return (
<Group gap="sm" wrap="nowrap">
<ThemeIcon variant="light" color="edr-green" size={40} radius="xl">
<Settings size={18} />
</ThemeIcon>
<div style={{ minWidth: 0 }}>
<Text fw={500} c="edr-text" truncate>
{s.label}
</Text>
<Text size="xs" c="edr-muted" truncate>
{s.description ?? "No description"}
</Text>
</div>
</Group>
);
},
},
{
id: "code",
header: "Code",
cell: ({ row }) => <Code>{row.original.code}</Code>,
},
{
id: "options",
header: "Options",
cell: ({ row }) => (
<Group gap={6} wrap="nowrap">
<Boxes size={16} className="text-edr-muted" />
<Text size="sm" fw={500} c="edr-text">
{row.original.children?.length ?? 0}
</Text>
</Group>
),
},
{
id: "behavior",
header: "Behavior",
cell: ({ row }) => {
const s = row.original;
return (
<Group gap={4} wrap="wrap">
<Badge variant="light" color={s.multiple ? "edr-green" : "gray"}>
{s.multiple ? "Multi" : "Single"}
</Badge>
{s.meta?.searchable ? (
<Badge variant="light" color="edr-green">
Searchable
</Badge>
) : null}
{s.meta?.clearable ? (
<Badge variant="light" color="edr-green">
Clearable
</Badge>
) : null}
</Group>
);
},
},
{
id: "permissions",
header: "Permissions",
cell: ({ row }) => {
const perms = row.original.meta?.permissions ?? [];
if (perms.length === 0) {
return (
<Text size="xs" c="dimmed">
</Text>
);
}
return (
<Group gap={4} wrap="wrap">
{perms.map((p) => (
<Badge
key={p}
variant="light"
color="edr-green"
leftSection={<Shield size={11} />}
>
{p}
</Badge>
))}
</Group>
);
},
},
{
id: "actions",
size: 40,
cell: ({ row }) => {
const setting = row.original;
return (
<Group justify="flex-end" onClick={(e) => e.stopPropagation()}>
<Menu position="bottom-end" withinPortal shadow="md" width={180}>
<Menu.Target>
<ActionIcon variant="default" aria-label="Row actions">
<MoreHorizontal size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<Eye size={15} />}>View</Menu.Item>
<Menu.Item
leftSection={<CheckCircle2 size={15} />}
onClick={() => openDialogFor("options", setting)}
>
Options
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<Pencil size={15} />}
onClick={() => openDialogFor("edit", setting)}
>
Edit
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<Trash2 size={15} />}
color="red"
onClick={() => openDialogFor("delete", setting)}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
);
},
},
];
return (
<PageContainer>
<PageHeader
title="Dropdown Settings"
subtitle="Manage every dynamic dropdown across the platform — labels, options, ordering, and permissions."
action={
<>
<TextInput
w={{ base: "100%", sm: 280 }}
leftSection={<Search size={16} />}
value={query}
onChange={(e) => {
setQuery(e.currentTarget.value);
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}}
placeholder="Search by code, label, description…"
/>
<Button
leftSection={<Plus size={16} />}
onClick={() => setCreateOpen(true)}
>
New Setting
</Button>
</>
}
/>
<KpiStrip
loading={kpiLoading}
items={[
{ label: "Settings", value: dropdownSettings.length, icon: Settings },
{ label: "Total Options", value: totalOptions, icon: Boxes },
{ label: "Multi-select", value: multipleCount, icon: ListOrdered },
{ label: "Searchable", value: searchableCount, icon: Sparkles },
]}
/>
<Card p={0}>
<Group
justify="space-between"
p="md"
className="border-b border-edr-border"
>
<div>
<Text fw={600} c="edr-text">
Registered Dropdowns
</Text>
<Text size="sm" c="dimmed">
Every dynamic dropdown the platform reads from.
</Text>
</div>
<Button variant="default" size="sm" leftSection={<Filter size={16} />}>
Filter
</Button>
</Group>
<DataTable
columns={columns}
data={rows}
status={status}
error={
isError
? {
message: "Failed to load dropdown settings.",
description:
error instanceof Error ? error.message : undefined,
}
: undefined
}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount: total,
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
}}
containerClassName="border-0 shadow-none"
footer={DataTableFooter}
/>
</Card>
{/* Create — fully controlled, no shadcn trigger child. */}
<EditDropdownSettingDialog
mode="create"
open={createOpen}
onOpenChange={setCreateOpen}
/>
{/* Controlled row-action dialogs. */}
{activeSetting ? (
<>
<EditDropdownSettingDialog
key={`edit-${activeSetting.id}`}
mode="edit"
setting={activeSetting}
open={activeDialog === "edit"}
onOpenChange={(next) => (next ? null : closeDialog())}
/>
<ManageDropdownOptionsDialog
key={`options-${activeSetting.id}`}
setting={activeSetting}
open={activeDialog === "options"}
onOpenChange={(next) => (next ? null : closeDialog())}
/>
<DeleteDropdownSettingDialog
key={`delete-${activeSetting.id}`}
settingLabel={activeSetting.label}
settingCode={activeSetting.code}
onConfirm={() => deleteMutation.mutate({ id: activeSetting.id })}
open={activeDialog === "delete"}
onOpenChange={(next) => (next ? null : closeDialog())}
/>
</>
) : null}
</PageContainer>
);
}