style: finalize the style of the dashboard

This commit is contained in:
Nathnael
2026-06-22 08:52:45 +00:00
parent feddce8a0c
commit 8fb432e30f
11 changed files with 699 additions and 642 deletions

View File

@@ -1,25 +1,35 @@
import { useEffect, useMemo, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import type { ColumnDef } from "@edr/ui-common";
import { Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core";
import {
Archive,
Circle,
CircleCheck,
CircleSlash,
Layers,
Link2,
Plus,
Wrench,
type LucideIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { PageContainer } from "@/components/page";
import FleetCardGrid from "@/components/fleet/FleetCardGrid";
import FleetFormDialog from "@/components/fleet/FleetFormDialog";
import FleetRecordActions from "@/components/fleet/FleetRecordActions";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/fleetFormat";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { useFleetList, useFleetMutations } from "@/hooks/fleet/useFleet";
import { useCargoTypes } from "@/hooks/use-cargo-types";
import { useContainerTypes } from "@/hooks/use-container-types";
import { useWagonTypes } from "@/hooks/use-wagon-types";
import { useFleetList, useFleetMutations } from "@/hooks/fleet/useFleet";
import { useContainers } from "@/hooks/useContainers";
import { useToast } from "@/hooks/use-toast";
import { useWagonTypes } from "@/hooks/use-wagon-types";
import { useContainers } from "@/hooks/useContainers";
import { useRouteYards } from "@/hooks/useRoutes";
import { useWagons } from "@/hooks/useWagons";
import type { FleetListFilters } from "@/services/fleet/fleet.service";
import {
FLEET_SELECT_NONE,
getFleetResource,
@@ -27,11 +37,27 @@ import {
type FleetFormFieldDef,
type FleetResourceSlug,
} from "@/pages/fleet/config/resources";
import type { FleetRecord } from "@/services/fleet/fleet.service";
import type { FleetListFilters, FleetRecord } from "@/services/fleet/fleet.service";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
const DEFAULT_SLUG: FleetResourceSlug = "locomotives";
const FLEET_STATUS_META: Record<
string,
{ label: string; icon: LucideIcon; color: string }
> = {
AVAILABLE: { label: "Available", icon: CircleCheck, color: "edr-green" },
ASSIGNED: { label: "Assigned", icon: Link2, color: "blue" },
MAINTENANCE: { label: "Maintenance", icon: Wrench, color: "yellow" },
OUT_OF_SERVICE: { label: "Out of service", icon: CircleSlash, color: "red" },
RETIRED: { label: "Retired", icon: Archive, color: "gray" },
};
const humanizeStatus = (status: string) => {
const text = status.replace(/_/g, " ").toLowerCase();
return text.charAt(0).toUpperCase() + text.slice(1);
};
const FleetResourcePage = () => {
const location = useLocation();
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
@@ -250,6 +276,39 @@ const FleetResourcePage = () => {
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
const kpiItems = useMemo(() => {
const items = [
{
label: `Total ${config?.label.toLowerCase() ?? ""}`,
value: allRows.length,
icon: Layers,
color: "edr-green",
},
];
if (hasStatusColumn) {
const counts = new Map<string, number>();
for (const row of allRows) {
const status = String(
(row as unknown as Record<string, unknown>).status ?? "",
);
if (status) counts.set(status, (counts.get(status) ?? 0) + 1);
}
const top = [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 4);
for (const [status, count] of top) {
const meta = FLEET_STATUS_META[status];
items.push({
label: meta?.label ?? humanizeStatus(status),
value: count,
icon: meta?.icon ?? Circle,
color: meta?.color ?? "gray",
});
}
}
return items.slice(0, 5);
}, [allRows, hasStatusColumn, config?.label]);
if (!config) {
return <Navigate to="/dashboard/locomotives" replace />;
}
@@ -293,6 +352,24 @@ const FleetResourcePage = () => {
return (
<PageContainer>
<PageHeader
title={config.label}
subtitle={config.subtitle}
action={
<Button
leftSection={<Plus size={18} />}
onClick={() => {
setEditing(null);
setFormOpen(true);
}}
>
{config.addLabel}
</Button>
}
/>
<KpiStrip loading={isLoading} items={kpiItems} />
<Card radius="lg" padding={0} withBorder>
<Stack gap={0}>
<Box px="md" pt="md" pb="sm" w="100%">
@@ -301,11 +378,6 @@ const FleetResourcePage = () => {
onSearchChange={setSearch}
searchPlaceholder={config.searchPlaceholder}
showSearch={config.supportsSearch}
addLabel={config.addLabel}
onAdd={() => {
setEditing(null);
setFormOpen(true);
}}
viewMode={viewMode}
onViewModeChange={setViewMode}
filters={
@@ -316,7 +388,6 @@ const FleetResourcePage = () => {
key={filter.key}
size="sm"
radius="lg"
label={filter.label}
value={filter.value}
onChange={(v) => {
if (!v) return;

View File

@@ -1,5 +1,5 @@
import { FormEvent, useMemo, useState } from "react";
import { Edit, Eye, Trash2 } from "lucide-react";
import { Ban, CircleCheck, Edit, Eye, Plus, Route as RouteIcon, Trash2 } from "lucide-react";
import type { ColumnDef } from "@edr/ui-common";
import {
ActionIcon,
@@ -17,7 +17,7 @@ import {
Tooltip,
} from "@mantine/core";
import { PageContainer } from "@/components/page";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import FleetToolbar from "@/components/fleet/FleetToolbar";
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
@@ -102,6 +102,9 @@ export default function RoutesPage() {
return filteredRoutes.slice(start, start + pagination.pageSize);
}, [filteredRoutes, pagination.pageIndex, pagination.pageSize]);
const allRoutes = routesQuery.data ?? [];
const activeCount = allRoutes.filter((route) => route.isActive).length;
const yardOptions = useMemo(
() =>
(yardsQuery.data ?? []).map((yard) => ({
@@ -281,6 +284,30 @@ export default function RoutesPage() {
return (
<PageContainer>
<PageHeader
title="Routes"
subtitle="Define rail corridors and their ordered yard stops used by train scheduling."
action={
<Button leftSection={<Plus size={18} />} onClick={openCreate}>
Add route
</Button>
}
/>
<KpiStrip
loading={routesQuery.isLoading}
items={[
{ label: "Total routes", value: allRoutes.length, icon: RouteIcon },
{ label: "Active", value: activeCount, icon: CircleCheck, color: "edr-green" },
{
label: "Inactive",
value: allRoutes.length - activeCount,
icon: Ban,
color: "gray",
},
]}
/>
<Card radius="lg" padding={0} withBorder>
<Stack gap={0}>
<Box px="md" pt="md" pb="sm" w="100%">
@@ -288,8 +315,6 @@ export default function RoutesPage() {
search={search}
onSearchChange={setSearch}
searchPlaceholder="Search routes…"
addLabel="Add Route"
onAdd={openCreate}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>