mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 22:53:26 +00:00
- 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.
365 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|