From 4c315e77e9b7a11c6f8187da8fb5d5be0706a4eb Mon Sep 17 00:00:00 2001 From: StanisLove Date: Thu, 18 Jun 2026 23:19:54 +0300 Subject: [PATCH] feat: split big file and update agents.md --- AGENTS.md | 56 +- src/app/analytics/page.tsx | 2 +- src/app/dashboard/page.tsx | 2 +- src/app/layout.tsx | 2 +- src/app/page.tsx | 2 +- src/app/returns/page.tsx | 2 +- src/app/routes/page.tsx | 2 +- src/app/settings/page.tsx | 2 +- src/app/shipments/page.tsx | 2 +- src/shared/lib/ops-tone.ts | 16 + src/shared/ui/container.tsx | 5 + src/shared/ui/panel.tsx | 5 + src/widgets/analytics-workspace.tsx | 78 +++ src/widgets/control-room-board.tsx | 87 +++ src/widgets/exception-board.tsx | 42 ++ src/widgets/icon-cards.tsx | 26 + src/widgets/metric-strip.tsx | 41 ++ src/widgets/ops-dashboard.tsx | 77 +++ src/widgets/returns-workspace.tsx | 54 ++ src/widgets/routes-workspace.tsx | 82 +++ src/widgets/settings-workspace.tsx | 43 ++ src/widgets/shipment-control.tsx | 90 +++ src/widgets/shipment-table.tsx | 64 +++ src/widgets/site-shell.tsx | 163 ++++++ src/widgets/template-ui.tsx | 856 ---------------------------- 25 files changed, 936 insertions(+), 865 deletions(-) create mode 100644 src/shared/lib/ops-tone.ts create mode 100644 src/shared/ui/container.tsx create mode 100644 src/shared/ui/panel.tsx create mode 100644 src/widgets/analytics-workspace.tsx create mode 100644 src/widgets/control-room-board.tsx create mode 100644 src/widgets/exception-board.tsx create mode 100644 src/widgets/icon-cards.tsx create mode 100644 src/widgets/metric-strip.tsx create mode 100644 src/widgets/ops-dashboard.tsx create mode 100644 src/widgets/returns-workspace.tsx create mode 100644 src/widgets/routes-workspace.tsx create mode 100644 src/widgets/settings-workspace.tsx create mode 100644 src/widgets/shipment-control.tsx create mode 100644 src/widgets/shipment-table.tsx create mode 100644 src/widgets/site-shell.tsx delete mode 100644 src/widgets/template-ui.tsx diff --git a/AGENTS.md b/AGENTS.md index a72417b..56b3fcc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ FreightOps Control — dense internal logistics dashboard; сохраняй ут ## Project Specifics - Source content lives in `src/entities/site-content.ts`; keep visible copy there or directly in route JSX. -- `src/app` is routing only; page composition belongs to `src/widgets/template-ui.tsx`. +- `src/app` is routing only; page composition belongs to per-page widgets (`src/widgets/.tsx`). См. File Map. - Keep cards at 8px radius or less and preserve the selected palette in `src/app/globals.css`. - This is a static frontend template: do not add real payments, auth, persistence, external API calls, or backend contracts without an explicit product request. - After changes run `pnpm lint` and `pnpm build`. @@ -14,3 +14,57 @@ FreightOps Control — dense internal logistics dashboard; сохраняй ут - Кириллица обязательна для видимого текста и выбранных Google Fonts. - При AI-правках сохраняйте доменные блоки этого шаблона: они отличают проект от generic landing. + +## Design System + +Источник токенов — `src/app/globals.css` (`@theme` + `:root`/`.dark`). Два шрифта: тело — Inter (`--font-sans`), заголовки/метрики/числа — IBM Plex Mono (`--font-display`, через класс `.font-display` и на `h1/h2/h3`). Работай через семантические классы Tailwind (`bg-card`, `text-foreground`, `text-primary`, `text-muted-foreground`, `border-border`), не хардкодь hex/oklch. Статусные оттенки (emerald/amber/rose) централизованы в `src/shared/lib/ops-tone.ts` — переиспользуй их, а не пиши классы заново. + +Личность: **utilitarian control room** — холодный сине-серый фон (почти белый, лёгкая синяя примесь), глубокий teal primary как рабочий акцент и янтарный accent для предупреждений. Тёмный `foreground` используется как сплошная заливка контрастных панелей (risk queue). Это плотный operator-интерфейс: данные, не маркетинг. + +| Роль | Light | Характер | +|---|---|---| +| `background` | холодный почти-белый (oklch 0.972 0.004 255) | фон рабочей области | +| `foreground` | тёмный сине-чёрный (oklch 0.16 0.012 255) | текст + **инверс-панели** (`bg-foreground text-background`) | +| `primary` | глубокий teal (oklch 0.39 0.105 186) | кнопки, иконки, числа, ring, bar-chart | +| `secondary` | светлый сине-серый | вторичные badge | +| `muted` / `muted-foreground` | сине-серый | подложки иконок, table head, вторичный текст | +| `accent` | янтарь (oklch 0.74 0.14 72) | предупреждения (вне семантики — чаще точечно) | +| `card` | чистый белый | все панели (`Panel`) | +| `border` | мягкий сине-серый | тонкие границы `border border-border` | + +Узнаваемые приёмы (держи их, это и есть «лицо» проекта): +- **Mono-числа:** метрики, SLA, проценты и заголовки — `font-display` (IBM Plex Mono), `font-semibold`, `tracking-normal`/`tracking-[0.12em–0.18em]` для caps-лейблов. +- **Panel-система:** компонент `Panel` (`@/shared/ui/panel`) — `rounded-md border border-border bg-card` + почти невидимая тень `shadow-[0_1px_0_rgba(15,23,42,0.04)]`. Всё строится из панелей. +- **Умеренный radius:** `--radius` = 0.375rem; везде `rounded-md` — мягко, но не «pill». +- **Dashboard shell:** `DashboardShell` даёт sticky sidebar (236px) + header-bar с live-бейджами; все рабочие страницы оборачиваются им. +- **Статус-токены:** `statusTone`/`routeTone`/`severityTone` — emerald = healthy/green, amber = watch/P2, rose = risk/red/P1; цветные точки и outline-бейджи. +- **Тонкие данные:** `Progress` h-2 для capacity/score, CSS bar-chart на `style={{ height }}`, плотные таблицы (`min-w-[860px]`, uppercase thead). +- **Inverse hero-блок:** `bg-foreground text-background` для risk queue — единственная тёмная панель на светлом дашборде. + +Do / Don't: +- **Do:** держи Panel-сетку, mono-заголовки/числа, sidebar shell, статус-токены из `ops-tone.ts`, плотные таблицы и прогресс-бары; данные — first-class. +- **Don't:** маркетинговый hero с крупным sans-заголовком и градиентом, скруглённые «pill»-карточки, тяжёлые тени, декоративные фото — это ломает control-room личность. (`.grainient-field` существует в CSS, но это не часть дашборда — не тащи её в рабочие страницы.) + +## File Map + +| Route | Widget | +|---|---| +| `/` | `src/widgets/ops-dashboard.tsx` (`OpsDashboard`) | +| `/dashboard` | `src/widgets/ops-dashboard.tsx` (`OpsDashboard`) | +| `/shipments` | `src/widgets/shipment-control.tsx` (`ShipmentControl`) | +| `/routes` | `src/widgets/routes-workspace.tsx` (`RoutesWorkspace`) | +| `/returns` | `src/widgets/returns-workspace.tsx` (`ReturnsWorkspace`) | +| `/analytics` | `src/widgets/analytics-workspace.tsx` (`AnalyticsWorkspace`) | +| `/settings` | `src/widgets/settings-workspace.tsx` (`SettingsWorkspace`) | + +Переиспользуемые блоки: +- `src/widgets/site-shell.tsx` — `SiteHeader`, `SiteFooter` (рендерятся в `app/layout.tsx`), `DashboardShell` (sidebar + header-bar обёртка всех рабочих страниц). +- `src/widgets/metric-strip.tsx` — `MetricStrip` (Dashboard + Analytics). +- `src/widgets/control-room-board.tsx` — `ControlRoomBoard` (Dashboard + Routes). +- `src/widgets/exception-board.tsx` — `ExceptionBoard` (Dashboard + Shipments + Returns). +- `src/widgets/shipment-table.tsx` — `ShipmentTable` (Dashboard + Shipments). +- `src/widgets/icon-cards.tsx` — `IconCards` (Returns + Settings). +- `src/shared/ui/panel.tsx` / `container.tsx` — `Panel`, `Container` (layout-примитивы шаблона). +- `src/shared/lib/ops-tone.ts` — `statusTone`, `routeTone`, `severityTone`. + +Одноразовые блоки колоцированы со своей страницей: `ShiftBriefing` в `ops-dashboard.tsx`; `CatalogToolbar` + `FeaturedGrid` в `shipment-control.tsx`; `PricingTiles` + `RouteTimeline` в `routes-workspace.tsx`; `ReturnsBoard` в `returns-workspace.tsx`; `OpsAnalytics` + `TestimonialBand` в `analytics-workspace.tsx`; `SettingsPanel` в `settings-workspace.tsx`. diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx index e38ef29..76420e6 100644 --- a/src/app/analytics/page.tsx +++ b/src/app/analytics/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { AnalyticsWorkspace } from "@/widgets/template-ui"; +import { AnalyticsWorkspace } from "@/widgets/analytics-workspace"; export default function Page() { return ; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index e716ecd..52aa2ee 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { OpsDashboard } from "@/widgets/template-ui"; +import { OpsDashboard } from "@/widgets/ops-dashboard"; export default function Page() { return ; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 93d996e..ca98194 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,7 +3,7 @@ import { IBM_Plex_Mono, Inter } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/shared/hooks/theme-provider"; import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener"; -import { SiteHeader, SiteFooter } from "@/widgets/template-ui"; +import { SiteHeader, SiteFooter } from "@/widgets/site-shell"; const mono = IBM_Plex_Mono({ variable: "--font-display", diff --git a/src/app/page.tsx b/src/app/page.tsx index e716ecd..52aa2ee 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { OpsDashboard } from "@/widgets/template-ui"; +import { OpsDashboard } from "@/widgets/ops-dashboard"; export default function Page() { return ; diff --git a/src/app/returns/page.tsx b/src/app/returns/page.tsx index 0e1688c..5bdb80d 100644 --- a/src/app/returns/page.tsx +++ b/src/app/returns/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { ReturnsWorkspace } from "@/widgets/template-ui"; +import { ReturnsWorkspace } from "@/widgets/returns-workspace"; export default function Page() { return ; diff --git a/src/app/routes/page.tsx b/src/app/routes/page.tsx index 8d26ccd..e885682 100644 --- a/src/app/routes/page.tsx +++ b/src/app/routes/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { RoutesWorkspace } from "@/widgets/template-ui"; +import { RoutesWorkspace } from "@/widgets/routes-workspace"; export default function Page() { return ; diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 66ca4ac..2e577f4 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { SettingsWorkspace } from "@/widgets/template-ui"; +import { SettingsWorkspace } from "@/widgets/settings-workspace"; export default function Page() { return ; diff --git a/src/app/shipments/page.tsx b/src/app/shipments/page.tsx index 8f40beb..abae585 100644 --- a/src/app/shipments/page.tsx +++ b/src/app/shipments/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { ShipmentControl } from "@/widgets/template-ui"; +import { ShipmentControl } from "@/widgets/shipment-control"; export default function Page() { return ; diff --git a/src/shared/lib/ops-tone.ts b/src/shared/lib/ops-tone.ts new file mode 100644 index 0000000..b4a1982 --- /dev/null +++ b/src/shared/lib/ops-tone.ts @@ -0,0 +1,16 @@ +export const statusTone = { + green: "border-emerald-200 bg-emerald-50 text-emerald-700", + amber: "border-amber-200 bg-amber-50 text-amber-800", + red: "border-rose-200 bg-rose-50 text-rose-700", +} as const; + +export const routeTone = { + healthy: "bg-emerald-500", + watch: "bg-amber-500", + risk: "bg-rose-500", +} as const; + +export const severityTone = { + P1: "border-rose-200 bg-rose-50 text-rose-700", + P2: "border-amber-200 bg-amber-50 text-amber-800", +} as const; diff --git a/src/shared/ui/container.tsx b/src/shared/ui/container.tsx new file mode 100644 index 0000000..86f2753 --- /dev/null +++ b/src/shared/ui/container.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export function Container({ children, className = "" }: { children: ReactNode; className?: string }) { + return
{children}
; +} diff --git a/src/shared/ui/panel.tsx b/src/shared/ui/panel.tsx new file mode 100644 index 0000000..7d80bef --- /dev/null +++ b/src/shared/ui/panel.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export function Panel({ children, className = "" }: { children: ReactNode; className?: string }) { + return
{children}
; +} diff --git a/src/widgets/analytics-workspace.tsx b/src/widgets/analytics-workspace.tsx new file mode 100644 index 0000000..ead404a --- /dev/null +++ b/src/widgets/analytics-workspace.tsx @@ -0,0 +1,78 @@ +import { analyticsBars, carrierScores, highlights, testimonials } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Progress } from "@/shared/ui/progress"; +import { Panel } from "@/shared/ui/panel"; +import { DashboardShell } from "@/widgets/site-shell"; +import { MetricStrip } from "@/widgets/metric-strip"; + +type TestimonialItem = { + name: string; + text: string; + rating: string; +}; + +function OpsAnalytics() { + return ( +
+ +
+
+

Volume by checkpoint

+

Shipped, delayed и returned по контрольным часам смены.

+
+ static chart +
+
+ {analyticsBars.map((bar) => ( +
+
+
+
+
{bar.label}
+
+ ))} +
+ + +

Carrier score

+

Оценка курьеров по scans, ETA drift и missed slots.

+
+ {carrierScores.map((carrier) => ( +
+
+ {carrier.name} + {carrier.score} +
+ +
{carrier.meta}
+
+ ))} +
+
+
+ ); +} + +function TestimonialBand({ items }: { items: readonly TestimonialItem[] }) { + return ( +
+ {items.map((item) => ( + +
{item.rating}
+

"{item.text}"

+
{item.name}
+
+ ))} +
+ ); +} + +export function AnalyticsWorkspace() { + return ( + + + + + + ); +} diff --git a/src/widgets/control-room-board.tsx b/src/widgets/control-room-board.tsx new file mode 100644 index 0000000..3c5082a --- /dev/null +++ b/src/widgets/control-room-board.tsx @@ -0,0 +1,87 @@ +import { RouteIcon } from "lucide-react"; + +import { routeLanes, sidePanels, warehouseNodes } from "@/entities/site-content"; +import { routeTone } from "@/shared/lib/ops-tone"; +import { Button } from "@/shared/ui/button"; +import { Progress } from "@/shared/ui/progress"; +import { Separator } from "@/shared/ui/separator"; +import { Panel } from "@/shared/ui/panel"; + +export function ControlRoomBoard() { + return ( +
+ +
+
+

Route health

+

Маршруты читаются по SLA, capacity и следующему checkpoint.

+
+ +
+
+ {routeLanes.map((route) => ( +
+
+
+
+ +

{route.name}

+
+
+ {route.stops} stops + capacity {route.capacity}% + delay {route.delay} +
+
+
{route.sla}
+
+
+ +
+
+ {route.checkpoints.map((checkpoint, index) => ( +
+ {checkpoint} +
+ ))} +
+
+ ))} +
+
+ +

Warehouse load

+

Где появится очередь до того, как она станет SLA breach.

+
+ {warehouseNodes.map((node) => ( +
+
+ {node.name} + {node.load}% +
+ +
+ ))} +
+ +
+ {sidePanels.slice(3).map((item) => { + const Icon = item.icon; + return ( +
+ +
+
{item.title}: {item.value}
+
{item.text}
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/src/widgets/exception-board.tsx b/src/widgets/exception-board.tsx new file mode 100644 index 0000000..3dc4cbf --- /dev/null +++ b/src/widgets/exception-board.tsx @@ -0,0 +1,42 @@ +import { ClockIcon } from "lucide-react"; + +import { exceptionQueue } from "@/entities/site-content"; +import { severityTone } from "@/shared/lib/ops-tone"; +import { Badge } from "@/shared/ui/badge"; +import { Separator } from "@/shared/ui/separator"; +import { Panel } from "@/shared/ui/panel"; + +export function ExceptionBoard() { + return ( + +
+
+

Exception queue

+

Каждое исключение имеет owner, timer и следующее действие.

+
+ + no fire-and-forget handoff + +
+
+ {exceptionQueue.map((item) => ( +
+
+ + {item.severity} + + + + {item.time} + +
+

{item.title}

+

{item.owner}

+ +

{item.action}

+
+ ))} +
+
+ ); +} diff --git a/src/widgets/icon-cards.tsx b/src/widgets/icon-cards.tsx new file mode 100644 index 0000000..b57d2ec --- /dev/null +++ b/src/widgets/icon-cards.tsx @@ -0,0 +1,26 @@ +import type { ComponentType } from "react"; + +import { Panel } from "@/shared/ui/panel"; + +type IconItem = { + title: string; + text: string; + icon: ComponentType<{ className?: string }>; +}; + +export function IconCards({ items }: { items: readonly IconItem[] }) { + return ( +
+ {items.map((item) => { + const Icon = item.icon; + return ( + + +

{item.title}

+

{item.text}

+
+ ); + })} +
+ ); +} diff --git a/src/widgets/metric-strip.tsx b/src/widgets/metric-strip.tsx new file mode 100644 index 0000000..1f37478 --- /dev/null +++ b/src/widgets/metric-strip.tsx @@ -0,0 +1,41 @@ +import type { ComponentType } from "react"; + +import { Badge } from "@/shared/ui/badge"; +import { Panel } from "@/shared/ui/panel"; + +type MetricItem = { + title: string; + value: string; + text: string; + trend: string; + icon: ComponentType<{ className?: string }>; +}; + +export function MetricStrip({ items }: { items: readonly MetricItem[] }) { + return ( +
+ {items.map((item) => { + const Icon = item.icon; + const isNegative = item.trend.startsWith("-"); + return ( + +
+
+ +
+ + {item.trend} + +
+
{item.value}
+

{item.title}

+

{item.text}

+
+ ); + })} +
+ ); +} diff --git a/src/widgets/ops-dashboard.tsx b/src/widgets/ops-dashboard.tsx new file mode 100644 index 0000000..4258c71 --- /dev/null +++ b/src/widgets/ops-dashboard.tsx @@ -0,0 +1,77 @@ +import { BellIcon } from "lucide-react"; + +import { exceptionQueue, highlights } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Panel } from "@/shared/ui/panel"; +import { DashboardShell } from "@/widgets/site-shell"; +import { MetricStrip } from "@/widgets/metric-strip"; +import { ControlRoomBoard } from "@/widgets/control-room-board"; +import { ExceptionBoard } from "@/widgets/exception-board"; +import { ShipmentTable } from "@/widgets/shipment-table"; + +function ShiftBriefing() { + return ( + +
+
+ dispatch briefing +

+ Сегодня смена ломается не объемом, а исключениями на маршрутах +

+

+ Фокус диспетчера: missed pickup на North-1, cold chain ETA drift и return bay load 88%. Панель показывает, кто владеет + каждым риском и какое действие нужно следующим. +

+
+ {[ + ["Next checkpoint", "14:30", "call courier C04"], + ["Warehouse load", "84%", "line 1 is hot"], + ["Customer notices", "17", "ready to send"], + ].map(([label, value, text]) => ( +
+
{label}
+
{value}
+
{text}
+
+ ))} +
+
+
+
+
+
risk queue
+
27
+
+ +
+
+ {exceptionQueue.slice(0, 3).map((item) => ( +
+
+
{item.title}
+ {item.time} +
+

{item.action}

+
+ ))} +
+
+
+
+ ); +} + +export function OpsDashboard() { + return ( + + + + + + + + ); +} diff --git a/src/widgets/returns-workspace.tsx b/src/widgets/returns-workspace.tsx new file mode 100644 index 0000000..a779569 --- /dev/null +++ b/src/widgets/returns-workspace.tsx @@ -0,0 +1,54 @@ +import { RefreshCcwIcon } from "lucide-react"; + +import { eventTypes, returnCases } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { Separator } from "@/shared/ui/separator"; +import { Panel } from "@/shared/ui/panel"; +import { DashboardShell } from "@/widgets/site-shell"; +import { ExceptionBoard } from "@/widgets/exception-board"; +import { IconCards } from "@/widgets/icon-cards"; + +function ReturnsBoard() { + return ( + +
+
+

Return inspection

+

Refund readiness зависит от evidence, причины и состояния товара.

+
+ +
+
+ {returnCases.map((item) => ( +
+
+ {item.id} + {item.value} +
+

{item.reason}

+

{item.evidence}

+ +
+ Decision + {item.decision} +
+
+ ))} +
+
+ ); +} + +export function ReturnsWorkspace() { + return ( + + + + + + ); +} diff --git a/src/widgets/routes-workspace.tsx b/src/widgets/routes-workspace.tsx new file mode 100644 index 0000000..2ebd343 --- /dev/null +++ b/src/widgets/routes-workspace.tsx @@ -0,0 +1,82 @@ +import { CheckIcon, TruckIcon } from "lucide-react"; + +import { routeLanes, tastingSets } from "@/entities/site-content"; +import { routeTone } from "@/shared/lib/ops-tone"; +import { Separator } from "@/shared/ui/separator"; +import { Panel } from "@/shared/ui/panel"; +import { DashboardShell } from "@/widgets/site-shell"; +import { ControlRoomBoard } from "@/widgets/control-room-board"; + +type TileItem = { + title: string; + price: string; + items: readonly string[]; +}; + +function PricingTiles({ title, items }: { title: string; items: readonly TileItem[] }) { + return ( + +

{title}

+
+ {items.map((item) => ( +
+

{item.title}

+
{item.price}
+ +
+ {item.items.map((line) => ( +
+ + {line} +
+ ))} +
+
+ ))} +
+
+ ); +} + +function RouteTimeline() { + return ( + +
+
+

Stop timeline

+

Timeline нужен для ответа на вопрос: где потеряем слот.

+
+ +
+
+ {routeLanes.map((route) => ( +
+
+
{route.name}
+
{route.delay}
+
+
+ {route.checkpoints.map((checkpoint, index) => ( +
+ + {checkpoint} +
+ ))} +
+
{route.sla}
+
+ ))} +
+
+ ); +} + +export function RoutesWorkspace() { + return ( + + + + + + ); +} diff --git a/src/widgets/settings-workspace.tsx b/src/widgets/settings-workspace.tsx new file mode 100644 index 0000000..6105eb0 --- /dev/null +++ b/src/widgets/settings-workspace.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { SettingsIcon } from "lucide-react"; + +import { contactCards, settingsRules } from "@/entities/site-content"; +import { Switch } from "@/shared/ui/switch"; +import { Panel } from "@/shared/ui/panel"; +import { DashboardShell } from "@/widgets/site-shell"; +import { IconCards } from "@/widgets/icon-cards"; + +function SettingsPanel() { + return ( + +
+
+

Dispatch rules

+

Настройки описывают операционные правила, но не подключены к API.

+
+ +
+
+ {settingsRules.map((rule) => ( +
+
+

{rule.title}

+

{rule.text}

+
+ +
+ ))} +
+
+ ); +} + +export function SettingsWorkspace() { + return ( + + + + + ); +} diff --git a/src/widgets/shipment-control.tsx b/src/widgets/shipment-control.tsx new file mode 100644 index 0000000..9c781c2 --- /dev/null +++ b/src/widgets/shipment-control.tsx @@ -0,0 +1,90 @@ +import { FilterIcon, SearchIcon, SlidersHorizontalIcon } from "lucide-react"; + +import { products } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { Input } from "@/shared/ui/input"; +import { Panel } from "@/shared/ui/panel"; +import { DashboardShell } from "@/widgets/site-shell"; +import { ShipmentTable } from "@/widgets/shipment-table"; +import { ExceptionBoard } from "@/widgets/exception-board"; + +type TextItem = { + name: string; + price: string; + tag: string; + text: string; +}; + +function CatalogToolbar() { + return ( +
+
+ + +
+
+ + +
+
+ ); +} + +function FeaturedGrid({ + eyebrow, + title, + text, + items, +}: { + eyebrow: string; + title: string; + text: string; + items: readonly TextItem[]; +}) { + return ( + +
+
+
{eyebrow}
+

{title}

+
+

{text}

+
+
+ {items.map((item) => ( +
+ + {item.tag} + +
{item.price}
+

{item.name}

+

{item.text}

+
+ ))} +
+
+ ); +} + +export function ShipmentControl() { + return ( + + + + + + + ); +} diff --git a/src/widgets/shipment-table.tsx b/src/widgets/shipment-table.tsx new file mode 100644 index 0000000..91ab537 --- /dev/null +++ b/src/widgets/shipment-table.tsx @@ -0,0 +1,64 @@ +import { PackageCheckIcon } from "lucide-react"; + +import { shipmentRows } from "@/entities/site-content"; +import { statusTone } from "@/shared/lib/ops-tone"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { Progress } from "@/shared/ui/progress"; +import { Panel } from "@/shared/ui/panel"; + +export function ShipmentTable() { + return ( + +
+
+

Live shipments

+

Рабочая таблица для dispatch review, а не декоративный список.

+
+ +
+
+ + + + + + + + + + + + + + + {shipmentRows.map((row) => ( + + + + + + + + + + + ))} + +
OrderCustomerZoneStatusETASLAProgressValue
{row.id}{row.customer}{row.zone}{row.status}{row.eta} + + {row.sla} + + +
+ + {row.progress}% +
+
{row.value}
+
+
+ ); +} diff --git a/src/widgets/site-shell.tsx b/src/widgets/site-shell.tsx new file mode 100644 index 0000000..b9a46d4 --- /dev/null +++ b/src/widgets/site-shell.tsx @@ -0,0 +1,163 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; +import { + ArrowRightIcon, + CalendarClockIcon, + ChevronRightIcon, + CircleAlertIcon, + MenuIcon, + RadioTowerIcon, +} from "lucide-react"; + +import { shiftSummary, sidePanels, site } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { Separator } from "@/shared/ui/separator"; +import { Container } from "@/shared/ui/container"; + +type ShellProps = { + title: string; + description: string; + children: ReactNode; +}; + +export function SiteHeader() { + return ( +
+
+ + + FC + + + {site.name} + + {shiftSummary.operatingMode} + + + + +
+ + + {shiftSummary.riskLabel} + + + +
+
+
+ ); +} + +export function SiteFooter() { + return ( +
+
+
+
{site.name}
+

{site.tagline}

+
+
+
Рабочие зоны
+ {site.nav.slice(0, 4).map((item) => ( + + {item.label} + + ))} +
+
+
Template note
+ Static dashboard UI: no auth, payments, persistence, carrier APIs, or background jobs. +
+
+
+ ); +} + +export function DashboardShell({ title, description, children }: ShellProps) { + return ( + +
+ +
+
+
+
+ + + live operations + + + {shiftSummary.dispatcher} + +
+

{title}

+

{description}

+
+
+ + +
+
+
{children}
+
+
+
+ ); +} diff --git a/src/widgets/template-ui.tsx b/src/widgets/template-ui.tsx deleted file mode 100644 index c08dbe6..0000000 --- a/src/widgets/template-ui.tsx +++ /dev/null @@ -1,856 +0,0 @@ -"use client"; - -import Link from "next/link"; -import type { ComponentType, ReactNode } from "react"; -import { - ArrowRightIcon, - BellIcon, - CalendarClockIcon, - CheckIcon, - ChevronRightIcon, - CircleAlertIcon, - ClockIcon, - FilterIcon, - MenuIcon, - PackageCheckIcon, - RadioTowerIcon, - RefreshCcwIcon, - RouteIcon, - SearchIcon, - SettingsIcon, - SlidersHorizontalIcon, - TruckIcon, -} from "lucide-react"; - -import { - analyticsBars, - carrierScores, - contactCards, - eventTypes, - exceptionQueue, - highlights, - products, - returnCases, - routeLanes, - settingsRules, - shiftSummary, - shipmentRows, - sidePanels, - site, - tastingSets, - testimonials, - warehouseNodes, -} from "@/entities/site-content"; -import { Badge } from "@/shared/ui/badge"; -import { Button } from "@/shared/ui/button"; -import { Input } from "@/shared/ui/input"; -import { Progress } from "@/shared/ui/progress"; -import { Separator } from "@/shared/ui/separator"; -import { Switch } from "@/shared/ui/switch"; - -type IconComponent = ComponentType<{ className?: string }>; - -type MetricItem = { - title: string; - value: string; - text: string; - trend: string; - icon: IconComponent; -}; - -type TextItem = { - name: string; - price: string; - tag: string; - text: string; -}; - -type TileItem = { - title: string; - price: string; - items: readonly string[]; -}; - -type IconItem = { - title: string; - text: string; - icon: IconComponent; -}; - -type TestimonialItem = { - name: string; - text: string; - rating: string; -}; - -type ShellProps = { - title: string; - description: string; - children: ReactNode; -}; - -const statusTone = { - green: "border-emerald-200 bg-emerald-50 text-emerald-700", - amber: "border-amber-200 bg-amber-50 text-amber-800", - red: "border-rose-200 bg-rose-50 text-rose-700", -} as const; - -const routeTone = { - healthy: "bg-emerald-500", - watch: "bg-amber-500", - risk: "bg-rose-500", -} as const; - -const severityTone = { - P1: "border-rose-200 bg-rose-50 text-rose-700", - P2: "border-amber-200 bg-amber-50 text-amber-800", -} as const; - -function Container({ children, className = "" }: { children: ReactNode; className?: string }) { - return
{children}
; -} - -function Panel({ children, className = "" }: { children: ReactNode; className?: string }) { - return
{children}
; -} - -export function SiteHeader() { - return ( -
-
- - - FC - - - {site.name} - - {shiftSummary.operatingMode} - - - - -
- - - {shiftSummary.riskLabel} - - - -
-
-
- ); -} - -export function SiteFooter() { - return ( -
-
-
-
{site.name}
-

{site.tagline}

-
-
-
Рабочие зоны
- {site.nav.slice(0, 4).map((item) => ( - - {item.label} - - ))} -
-
-
Template note
- Static dashboard UI: no auth, payments, persistence, carrier APIs, or background jobs. -
-
-
- ); -} - -export function DashboardShell({ title, description, children }: ShellProps) { - return ( - -
- -
-
-
-
- - - live operations - - - {shiftSummary.dispatcher} - -
-

{title}

-

{description}

-
-
- - -
-
-
{children}
-
-
-
- ); -} - -export function OpsDashboard() { - return ( - - - - - - - - ); -} - -export function PageHero() { - return ; -} - -export function ShiftBriefing() { - return ( - -
-
- dispatch briefing -

- Сегодня смена ломается не объемом, а исключениями на маршрутах -

-

- Фокус диспетчера: missed pickup на North-1, cold chain ETA drift и return bay load 88%. Панель показывает, кто владеет - каждым риском и какое действие нужно следующим. -

-
- {[ - ["Next checkpoint", "14:30", "call courier C04"], - ["Warehouse load", "84%", "line 1 is hot"], - ["Customer notices", "17", "ready to send"], - ].map(([label, value, text]) => ( -
-
{label}
-
{value}
-
{text}
-
- ))} -
-
-
-
-
-
risk queue
-
27
-
- -
-
- {exceptionQueue.slice(0, 3).map((item) => ( -
-
-
{item.title}
- {item.time} -
-

{item.action}

-
- ))} -
-
-
-
- ); -} - -export function MetricStrip({ items }: { items: readonly MetricItem[] }) { - return ( -
- {items.map((item) => { - const Icon = item.icon; - const isNegative = item.trend.startsWith("-"); - return ( - -
-
- -
- - {item.trend} - -
-
{item.value}
-

{item.title}

-

{item.text}

-
- ); - })} -
- ); -} - -export function ControlRoomBoard() { - return ( -
- -
-
-

Route health

-

Маршруты читаются по SLA, capacity и следующему checkpoint.

-
- -
-
- {routeLanes.map((route) => ( -
-
-
-
- -

{route.name}

-
-
- {route.stops} stops - capacity {route.capacity}% - delay {route.delay} -
-
-
{route.sla}
-
-
- -
-
- {route.checkpoints.map((checkpoint, index) => ( -
- {checkpoint} -
- ))} -
-
- ))} -
-
- -

Warehouse load

-

Где появится очередь до того, как она станет SLA breach.

-
- {warehouseNodes.map((node) => ( -
-
- {node.name} - {node.load}% -
- -
- ))} -
- -
- {sidePanels.slice(3).map((item) => { - const Icon = item.icon; - return ( -
- -
-
{item.title}: {item.value}
-
{item.text}
-
-
- ); - })} -
-
-
- ); -} - -export function ExceptionBoard() { - return ( - -
-
-

Exception queue

-

Каждое исключение имеет owner, timer и следующее действие.

-
- - no fire-and-forget handoff - -
-
- {exceptionQueue.map((item) => ( -
-
- - {item.severity} - - - - {item.time} - -
-

{item.title}

-

{item.owner}

- -

{item.action}

-
- ))} -
-
- ); -} - -export function CatalogToolbar() { - return ( -
-
- - -
-
- - -
-
- ); -} - -export function ShipmentTable() { - return ( - -
-
-

Live shipments

-

Рабочая таблица для dispatch review, а не декоративный список.

-
- -
-
- - - - - - - - - - - - - - - {shipmentRows.map((row) => ( - - - - - - - - - - - ))} - -
OrderCustomerZoneStatusETASLAProgressValue
{row.id}{row.customer}{row.zone}{row.status}{row.eta} - - {row.sla} - - -
- - {row.progress}% -
-
{row.value}
-
-
- ); -} - -export function ShipmentControl() { - return ( - - - - - - - ); -} - -export function RouteTimeline() { - return ( - -
-
-

Stop timeline

-

Timeline нужен для ответа на вопрос: где потеряем слот.

-
- -
-
- {routeLanes.map((route) => ( -
-
-
{route.name}
-
{route.delay}
-
-
- {route.checkpoints.map((checkpoint, index) => ( -
- - {checkpoint} -
- ))} -
-
{route.sla}
-
- ))} -
-
- ); -} - -export function RoutesWorkspace() { - return ( - - - - - - ); -} - -export function ReturnsWorkspace() { - return ( - - - - - - ); -} - -export function ReturnsBoard() { - return ( - -
-
-

Return inspection

-

Refund readiness зависит от evidence, причины и состояния товара.

-
- -
-
- {returnCases.map((item) => ( -
-
- {item.id} - {item.value} -
-

{item.reason}

-

{item.evidence}

- -
- Decision - {item.decision} -
-
- ))} -
-
- ); -} - -export function OpsAnalytics() { - return ( -
- -
-
-

Volume by checkpoint

-

Shipped, delayed и returned по контрольным часам смены.

-
- static chart -
-
- {analyticsBars.map((bar) => ( -
-
-
-
-
{bar.label}
-
- ))} -
- - -

Carrier score

-

Оценка курьеров по scans, ETA drift и missed slots.

-
- {carrierScores.map((carrier) => ( -
-
- {carrier.name} - {carrier.score} -
- -
{carrier.meta}
-
- ))} -
-
-
- ); -} - -export function AnalyticsWorkspace() { - return ( - - - - - - ); -} - -export function SettingsPanel() { - return ( - -
-
-

Dispatch rules

-

Настройки описывают операционные правила, но не подключены к API.

-
- -
-
- {settingsRules.map((rule) => ( -
-
-

{rule.title}

-

{rule.text}

-
- -
- ))} -
-
- ); -} - -export function SettingsWorkspace() { - return ( - - - - - ); -} - -export function FeaturedGrid({ - eyebrow, - title, - text, - items, -}: { - eyebrow: string; - title: string; - text: string; - items: readonly TextItem[]; -}) { - return ( - -
-
-
{eyebrow}
-

{title}

-
-

{text}

-
-
- {items.map((item) => ( -
- - {item.tag} - -
{item.price}
-

{item.name}

-

{item.text}

-
- ))} -
-
- ); -} - -export function PricingTiles({ title, items }: { title: string; items: readonly TileItem[] }) { - return ( - -

{title}

-
- {items.map((item) => ( -
-

{item.title}

-
{item.price}
- -
- {item.items.map((line) => ( -
- - {line} -
- ))} -
-
- ))} -
-
- ); -} - -export function IconCards({ items }: { items: readonly IconItem[] }) { - return ( -
- {items.map((item) => { - const Icon = item.icon; - return ( - - -

{item.title}

-

{item.text}

-
- ); - })} -
- ); -} - -export function TestimonialBand({ items }: { items: readonly TestimonialItem[] }) { - return ( -
- {items.map((item) => ( - -
{item.rating}
-

"{item.text}"

-
{item.name}
-
- ))} -
- ); -} - -export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) { - return ( - - - {eyebrow} -

{title}

-

{text}

-
-
- ); -} - -export function InfoColumns({ title, items }: { title: string; items: readonly { title: string; text: string }[] }) { - return ( - -

{title}

-
- {items.map((item) => ( -
-

{item.title}

-

{item.text}

-
- ))} -
-
- ); -}