diff --git a/app/services/auth.py b/app/services/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/reference.png b/frontend/public/reference.png new file mode 100644 index 0000000..b0b2ca6 Binary files /dev/null and b/frontend/public/reference.png differ diff --git a/frontend/src/app/clients/[clientId]/page.tsx b/frontend/src/app/clients/[clientId]/page.tsx new file mode 100644 index 0000000..6fa2f43 --- /dev/null +++ b/frontend/src/app/clients/[clientId]/page.tsx @@ -0,0 +1,2 @@ +import { ClientDetailPage } from "@/features/dashboard/pages"; +export default async function Page({ params }: { params: Promise<{ clientId: string }> }) { return ; } diff --git a/frontend/src/app/clients/page.tsx b/frontend/src/app/clients/page.tsx new file mode 100644 index 0000000..732c489 --- /dev/null +++ b/frontend/src/app/clients/page.tsx @@ -0,0 +1,2 @@ +import { ClientsPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/dashboard.css b/frontend/src/app/dashboard.css new file mode 100644 index 0000000..bc5961d --- /dev/null +++ b/frontend/src/app/dashboard.css @@ -0,0 +1,18 @@ +.dashboard-content { max-width: 1160px; margin: 0 auto; padding: 32px 28px 40px; } +.dashboard-header { margin-bottom: 22px; }.dashboard-header-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; }.dashboard-header h1 { color: var(--text-primary); font-size: 21px; font-weight: 620; letter-spacing: -.03em; }.dashboard-header p { max-width: 640px; margin-top: 7px; color: var(--text-secondary); font-size: 12px; }.dashboard-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; }.breadcrumbs { display: flex; align-items: center; gap: 7px; margin-bottom: 15px; color: var(--text-muted); font-size: 11px; }.breadcrumbs span { display: inline-flex; align-items: center; gap: 7px; }.breadcrumbs a { color: var(--accent); }.breadcrumbs strong { color: var(--text-secondary); font-weight: 500; } +.dashboard-panel { min-width: 0; margin-top: 12px; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); }.dashboard-panel-title { display: flex; align-items: center; justify-content: space-between; min-height: 44px; padding: 0 14px; border-bottom: 1px solid var(--border-subtle); }.dashboard-panel-title h2 { color: var(--text-primary); font-size: 12px; font-weight: 620; }.dashboard-panel-title a { color: var(--accent); font-size: 11px; } +.dash-stat-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }.dash-stat-strip > div { display: grid; gap: 4px; min-width: 0; padding: 13px 14px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-raised); }.dash-stat-strip span, .dash-stat-strip small { color: var(--text-secondary); font-size: 11px; }.dash-stat-strip strong { color: var(--text-primary); font-size: 21px; font-weight: 620; letter-spacing: -.03em; }.dash-stat-strip small { color: var(--text-muted); } +.dash-status { display: inline-flex; align-items: center; gap: 5px; width: fit-content; padding: 3px 6px; border-radius: var(--radius-sm); font-size: 10px; line-height: 1.2; white-space: nowrap; }.dash-status > span { width: 5px; height: 5px; border-radius: 50%; }.dash-status-good { color: var(--success); background: #2dd4a314; }.dash-status-good > span { background: var(--success); }.dash-status-warn { color: var(--warning); background: #d99a3217; }.dash-status-warn > span { background: var(--warning); }.dash-status-bad { color: var(--danger); background: #e0525217; }.dash-status-bad > span { background: var(--danger); }.dash-status-neutral { color: var(--text-secondary); background: #9aa4aa14; }.dash-status-neutral > span { background: var(--text-secondary); } +.data-table-wrap { overflow-x: auto; }.data-table { width: 100%; border-collapse: collapse; min-width: 720px; }.data-table th { padding: 10px 14px; color: var(--text-muted); border-bottom: 1px solid var(--border-subtle); font-size: 10px; font-weight: 500; text-align: left; white-space: nowrap; }.data-table td { padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); color: var(--text-secondary); font-size: 11px; vertical-align: middle; }.data-table tr:last-child td { border-bottom: 0; }.data-table tbody tr:hover td { background: var(--surface-hover); }.data-table a { color: var(--accent); }.data-table strong { display: block; color: var(--text-primary); font-weight: 550; }.data-table small { display: block; margin-top: 3px; color: var(--text-muted); font-size: 10px; }.mono { font-family: var(--mono); } +.dash-button { display: inline-flex; align-items: center; gap: 5px; min-height: 29px; padding: 0 9px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-raised); color: var(--text-secondary); font-size: 11px; }.dash-button:hover { background: var(--surface-hover); color: var(--text-primary); }.filter-chip { display: inline-flex; align-items: center; min-height: 29px; padding: 0 9px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-raised); color: var(--text-secondary); font-size: 11px; } +.empty-state { display: grid; justify-items: center; gap: 7px; padding: 42px 20px; color: var(--text-muted); text-align: center; }.empty-state svg { color: var(--success); }.empty-state strong { color: var(--text-secondary); font-size: 12px; font-weight: 550; }.empty-state p { max-width: 360px; font-size: 11px; } +.loading-state { display: flex; justify-content: center; gap: 5px; padding: 80px 0; }.loading-state span { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); opacity: .45; } +.detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, .7fr); gap: 12px; align-items: start; }.detail-stack { display: grid; gap: 12px; }.detail-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); padding: 4px 14px; }.detail-fields > div { padding: 10px 0; border-bottom: 1px solid var(--border-subtle); }.detail-fields > div:nth-last-child(-n+2) { border-bottom: 0; }.detail-fields dt { color: var(--text-muted); font-size: 10px; }.detail-fields dd { margin-top: 4px; color: var(--text-primary); font-size: 12px; }.detail-fields .mono { font-size: 11px; }.entity-link { color: var(--accent); }.entity-title { display: flex; align-items: center; gap: 9px; }.entity-title h1 { color: var(--text-primary); font-size: 21px; font-weight: 620; }.entity-subtitle { margin-top: 6px; color: var(--text-secondary); font-size: 12px; } +.evidence-timeline { display: grid; gap: 0; margin: 0; padding: 14px; list-style: none; }.evidence-timeline li { position: relative; display: flex; gap: 10px; min-height: 52px; }.evidence-timeline li:not(:last-child)::before { position: absolute; top: 23px; bottom: 0; left: 10px; width: 1px; background: var(--border); content: ""; }.timeline-icon { z-index: 1; display: grid; place-items: center; width: 21px; height: 21px; flex: 0 0 21px; border: 1px solid var(--border); border-radius: 50%; background: var(--surface-raised); }.timeline-good { color: var(--success); }.timeline-warn { color: var(--warning); }.evidence-timeline strong { display: block; color: var(--text-primary); font-size: 11px; font-weight: 550; }.evidence-timeline time { display: block; margin-top: 2px; color: var(--text-muted); font: 10px var(--mono); }.evidence-timeline p { margin-top: 3px; color: var(--text-secondary); font-size: 10px; } +.code-block { overflow: hidden; margin: 12px 14px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); }.code-block-title { padding: 8px 10px; border-bottom: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 10px; }.code-block pre { overflow: auto; padding: 12px; color: var(--text-secondary); font: 10px/1.6 var(--mono); white-space: pre-wrap; } +.split-panels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }.split-panels > .dashboard-panel { margin-top: 12px; }.inline-list { display: grid; }.inline-list > a, .inline-list > div { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 45px; padding: 0 14px; border-bottom: 1px solid var(--border-subtle); }.inline-list > :last-child { border-bottom: 0; }.inline-list strong { color: var(--text-primary); font-size: 11px; font-weight: 550; }.inline-list small { display: block; margin-top: 2px; color: var(--text-muted); font-size: 10px; }.inline-list a:hover { background: var(--surface-hover); } +.attempt-list { display: grid; }.attempt-row { display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; align-items: center; gap: 10px; min-height: 53px; padding: 0 14px; border-bottom: 1px solid var(--border-subtle); }.attempt-row:last-child { border-bottom: 0; }.attempt-number { color: var(--text-muted); font: 11px var(--mono); }.attempt-row strong { color: var(--text-primary); font-size: 11px; font-weight: 550; }.attempt-row small { display: block; margin-top: 2px; color: var(--text-muted); font: 10px var(--mono); } +.tabs { display: flex; gap: 16px; margin-top: 17px; border-bottom: 1px solid var(--border); }.tabs a { padding: 0 0 9px; color: var(--text-muted); font-size: 11px; }.tabs a:first-child { color: var(--text-primary); border-bottom: 1px solid var(--accent); } +.back-link, .attempt-navigation a { display: inline-flex; align-items: center; gap: 6px; color: var(--accent); font-size: 11px; }.attempt-navigation { display: flex; justify-content: space-between; margin: -8px 0 10px; }.failure-window { display: flex; align-items: center; gap: 10px; padding: 24px 16px; color: var(--text-muted); font: 10px var(--mono); }.failure-window > div { position: relative; display: flex; align-items: center; justify-content: space-between; width: 100%; height: 3px; background: var(--border); }.failure-window i { position: relative; width: 9px; height: 9px; margin-left: 28%; border-radius: 50%; background: var(--danger); box-shadow: 0 0 0 4px #e052521b; }.failure-window i.recovery { margin: 0 20% 0 0; background: var(--success); box-shadow: 0 0 0 4px #2dd4a31b; }.failure-window b { position: absolute; top: 14px; left: 21%; color: var(--danger); font: 9px var(--sans); white-space: nowrap; }.failure-window b.recovery-label { right: 8%; left: auto; color: var(--success); text-align: right; }.timing-bars { display: grid; gap: 10px; padding: 16px 14px; }.timing-bars span { display: grid; grid-template-columns: 70px 1fr; align-items: center; color: var(--text-muted); font: 10px var(--mono); }.timing-bars i { display: block; height: 7px; background: var(--accent); }.report-number { padding: 20px 14px 10px; color: var(--text-primary); font-size: 27px; font-weight: 620; letter-spacing: -.04em; }.report-number small { display: block; margin-top: 5px; color: var(--text-muted); font-size: 10px; font-weight: 400; letter-spacing: 0; }.report-bars { display: flex; align-items: end; gap: 8px; height: 94px; padding: 8px 14px 14px; border-top: 1px solid var(--border-subtle); }.report-bars i { flex: 1; min-height: 8px; background: var(--accent); opacity: .75; }.report-summary { display: grid; gap: 12px; padding: 20px 14px; color: var(--text-secondary); font-size: 11px; }.report-summary span { display: flex; align-items: center; gap: 7px; }.report-summary svg { color: var(--success); }.settings-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }.settings-grid .dashboard-panel { margin-top: 0; } +@media (max-width: 900px) { .dashboard-content { padding-inline: 18px; }.detail-grid, .split-panels { grid-template-columns: 1fr; }.dash-stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 720px) { .dashboard-content { padding: 26px 14px 34px; }.dashboard-header-row { align-items: flex-start; flex-direction: column; }.dashboard-actions { justify-content: flex-start; }.dash-stat-strip, .settings-grid { grid-template-columns: 1fr; }.detail-fields { grid-template-columns: 1fr; }.detail-fields > div:nth-last-child(-n+2) { border-bottom: 1px solid var(--border-subtle); }.detail-fields > div:last-child { border-bottom: 0; }.attempt-row { grid-template-columns: 34px minmax(0, 1fr); }.attempt-row .dash-status { grid-column: 2; justify-self: start; }.breadcrumbs { overflow-x: auto; white-space: nowrap; }.failure-window { padding-inline: 8px; }.failure-window b { left: 10%; }.failure-window b.recovery-label { right: 0; } } diff --git a/frontend/src/app/deliveries/[deliveryId]/attempts/[attemptId]/page.tsx b/frontend/src/app/deliveries/[deliveryId]/attempts/[attemptId]/page.tsx new file mode 100644 index 0000000..f2a966e --- /dev/null +++ b/frontend/src/app/deliveries/[deliveryId]/attempts/[attemptId]/page.tsx @@ -0,0 +1,2 @@ +import { AttemptDetailPage } from "@/features/dashboard/pages"; +export default async function Page({ params }: { params: Promise<{ deliveryId: string; attemptId: string }> }) { const resolved = await params; return ; } diff --git a/frontend/src/app/deliveries/[deliveryId]/page.tsx b/frontend/src/app/deliveries/[deliveryId]/page.tsx new file mode 100644 index 0000000..d34d530 --- /dev/null +++ b/frontend/src/app/deliveries/[deliveryId]/page.tsx @@ -0,0 +1,2 @@ +import { DeliveryDetailPage } from "@/features/dashboard/pages"; +export default async function Page({ params }: { params: Promise<{ deliveryId: string }> }) { return ; } diff --git a/frontend/src/app/deliveries/page.tsx b/frontend/src/app/deliveries/page.tsx new file mode 100644 index 0000000..db1ec65 --- /dev/null +++ b/frontend/src/app/deliveries/page.tsx @@ -0,0 +1,2 @@ +import { DeliveriesPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 25f6729..1efa6d0 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,20 +1,34 @@ :root { color-scheme: dark; - --base: #101215; - --surface: #171a1e; - --raised: #1e2227; - --hover: #25292f; - --line: #2b3037; - --line-subtle: #22262c; - --text: #eef0f3; - --soft: #bec4cd; - --muted: #929ca9; - --accent: #ff956e; - --accent-dim: #31231f; - --green: #83d9b0; - --red: #ff989f; - --amber: #eac281; - --blue: #91bdff; + --base: #090d10; + --surface: #0e1519; + --raised: #121b20; + --hover: #121b20; + --line: #253036; + --line-subtle: #1b252a; + --text: #e6eaed; + --soft: #9aa4aa; + --muted: #69757c; + --accent: #6c8cff; + --accent-dim: #1d2946; + --green: #2dd4a3; + --red: #e05252; + --amber: #d99a32; + --blue: #6c8cff; + --bg: var(--base); + --sidebar: #0b1319; + --surface-raised: var(--raised); + --surface-hover: var(--hover); + --border: var(--line); + --border-subtle: var(--line-subtle); + --text-primary: var(--text); + --text-secondary: var(--soft); + --text-muted: var(--muted); + --success: var(--green); + --warning: var(--amber); + --danger: var(--red); + --radius-sm: 4px; + --radius-md: 6px; --mono: "Geist Mono Variable", ui-monospace, monospace; --sans: "Geist Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } diff --git a/frontend/src/app/incidents/[incidentId]/page.tsx b/frontend/src/app/incidents/[incidentId]/page.tsx new file mode 100644 index 0000000..b29e6e9 --- /dev/null +++ b/frontend/src/app/incidents/[incidentId]/page.tsx @@ -0,0 +1,2 @@ +import { IncidentDetailPage } from "@/features/dashboard/pages"; +export default async function Page({ params }: { params: Promise<{ incidentId: string }> }) { return ; } diff --git a/frontend/src/app/incidents/page.tsx b/frontend/src/app/incidents/page.tsx new file mode 100644 index 0000000..ceb26cf --- /dev/null +++ b/frontend/src/app/incidents/page.tsx @@ -0,0 +1,2 @@ +import { IncidentsPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index c377843..46e01f9 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -2,11 +2,11 @@ import type { Metadata } from "next"; import "@fontsource-variable/geist"; import "@fontsource-variable/geist-mono"; import "./globals.css"; -import "@/features/desk/desk.css"; +import "./dashboard.css"; export const metadata: Metadata = { - title: "Investigation desk · Ackvia", - description: "Captured submissions. Delivery evidence. Recovery in context.", + title: "Overview · Ackvia", + description: "Portfolio lead reliability at a glance.", }; export default function RootLayout({ children }: LayoutProps<"/">) { diff --git a/frontend/src/app/leads/[leadId]/page.tsx b/frontend/src/app/leads/[leadId]/page.tsx new file mode 100644 index 0000000..e7ec023 --- /dev/null +++ b/frontend/src/app/leads/[leadId]/page.tsx @@ -0,0 +1,2 @@ +import { LeadDetailPage } from "@/features/dashboard/pages"; +export default async function Page({ params }: { params: Promise<{ leadId: string }> }) { return ; } diff --git a/frontend/src/app/leads/page.tsx b/frontend/src/app/leads/page.tsx new file mode 100644 index 0000000..662b71a --- /dev/null +++ b/frontend/src/app/leads/page.tsx @@ -0,0 +1,2 @@ +import { LeadsPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/loading.tsx b/frontend/src/app/loading.tsx index 9e5d444..f3e20e2 100644 --- a/frontend/src/app/loading.tsx +++ b/frontend/src/app/loading.tsx @@ -1 +1,7 @@ -export { DeskLoading as default } from "@/features/desk/desk-loading"; +import "./overview.css"; +import { AppShell } from "@/components/app-shell"; +import { DashboardPage, LoadingState } from "@/components/dashboard-ui"; + +export default function Loading() { + return ; +} diff --git a/frontend/src/app/monitoring/page.tsx b/frontend/src/app/monitoring/page.tsx new file mode 100644 index 0000000..0d61001 --- /dev/null +++ b/frontend/src/app/monitoring/page.tsx @@ -0,0 +1,2 @@ +import { MonitoringPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/overview.css b/frontend/src/app/overview.css new file mode 100644 index 0000000..eebe875 --- /dev/null +++ b/frontend/src/app/overview.css @@ -0,0 +1,80 @@ +.overview-shell { min-height: 100vh; display: flex; background: #0b1116; color: #e8edf2; } +.overview-sidebar { position: fixed; inset: 0 auto 0 0; width: 220px; display: flex; flex-direction: column; padding: 22px 12px 14px; background: #0b1319; border-right: 1px solid #26313a; } +.overview-brand { display: flex; align-items: center; gap: 9px; padding: 0 10px 27px; font-size: 15px; font-weight: 650; letter-spacing: -.02em; } +.overview-mark { position: relative; width: 17px; height: 18px; display: inline-block; } +.overview-mark span { position: absolute; width: 4px; height: 15px; border-radius: 4px; transform: rotate(28deg); } +.overview-mark span:first-child { left: 1px; top: 2px; background: #f18bcd; }.overview-mark span:nth-child(2) { left: 7px; top: 0; background: #81aaff; }.overview-mark span:last-child { right: 0; top: 3px; background: #77d4b1; } +.overview-nav { display: grid; gap: 3px; }.nav-item { display: flex; align-items: center; gap: 10px; min-height: 32px; padding: 0 10px; color: #9ba8b5; border-radius: 5px; font-size: 12px; }.nav-item:hover { color: #e8edf2; background: #16212a; }.nav-item-active { color: #edf3f8; background: #1b355d; }.nav-item b { margin-left: auto; display: grid; place-items: center; width: 16px; height: 16px; border-radius: 4px; background: #f04d5e; color: white; font-size: 10px; } +.sidebar-bottom { margin-top: auto; display: grid; gap: 14px; }.workspace-switcher { display: flex; align-items: center; gap: 9px; padding: 9px 8px; background: none; border-top: 1px solid #26313a; color: #dce4eb; text-align: left; }.workspace-switcher > svg { margin-left: auto; color: #81909d; }.avatar, .top-avatar { display: grid; place-items: center; border-radius: 50%; background: #7196dc; color: white; font-size: 10px; font-weight: 700; }.avatar { width: 24px; height: 24px; }.workspace-copy { display: grid; gap: 1px; }.workspace-copy strong { font-size: 11px; font-weight: 550; }.workspace-copy small { color: #8794a0; font-size: 10px; } +.overview-main { width: calc(100% - 220px); margin-left: 220px; min-width: 0; }.overview-topbar { position: relative; height: 62px; display: flex; align-items: center; justify-content: space-between; padding: 0 28px; border-bottom: 1px solid #26313a; }.search-trigger, .agency-button, .filter-button { display: flex; align-items: center; gap: 9px; border: 1px solid #25323c; border-radius: 5px; background: #101a21; color: #9daab5; }.search-trigger { width: 268px; height: 28px; padding: 0 9px; font-size: 11px; text-align: left; }.search-trigger svg { color: #a5b4c2; }.search-trigger kbd { margin-left: auto; padding: 2px 5px; border: 1px solid #2b3944; border-radius: 3px; color: #8e9aa6; font-size: 10px; }.topbar-actions { display: flex; align-items: center; gap: 14px; }.icon-button { position: relative; display: grid; place-items: center; width: 25px; height: 25px; background: none; color: #c0cad2; }.notification i { position: absolute; top: 3px; right: 2px; width: 4px; height: 4px; border-radius: 50%; background: #79d1af; }.top-avatar { width: 24px; height: 24px; }.agency-button { height: 28px; padding: 0 9px; color: #c7d0d8; font-size: 10px; }.search-popover { position: absolute; z-index: 5; top: 48px; left: 28px; width: 320px; display: flex; gap: 8px; padding: 12px; border: 1px solid #32414c; border-radius: 6px; background: #121c23; box-shadow: 0 16px 30px #0005; color: #8897a4; font-size: 11px; } +.overview-content { max-width: 1160px; margin: 0 auto; padding: 37px 28px 28px; }.page-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 22px; }.page-header h1 { font-size: 20px; line-height: 1.1; letter-spacing: -.03em; font-weight: 620; }.portfolio-status { display: flex; align-items: center; gap: 7px; margin-top: 7px; color: #a5b1bb; font-size: 12px; }.pulse-dot, .footer-dot { width: 7px; height: 7px; border-radius: 50%; background: #53d4a1; box-shadow: 0 0 0 3px #53d4a11c; }.header-filters { display: flex; gap: 7px; }.filter-button { height: 29px; padding: 0 9px; font-size: 10px; color: #c5ced7; }.filter-button svg { color: #8393a1; } +.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }.metric { position: relative; min-height: 79px; padding: 13px 14px; border: 1px solid #26343d; border-radius: 6px; background: #111c23; }.metric span { display: block; color: #a1adb7; font-size: 11px; }.metric strong { display: block; margin-top: 4px; font-size: 22px; font-weight: 620; letter-spacing: -.04em; }.metric em { position: absolute; right: 12px; bottom: 14px; font-style: normal; font-size: 10px; }.trend-positive { color: #52d5a4; }.metric-attention { border-color: #33404a; }.trend-attention { color: #e5a35f; } +.attention-panel { display: flex; align-items: center; gap: 12px; min-height: 70px; margin-top: 10px; padding: 13px 15px; border: 1px solid #4b382b; border-radius: 6px; background: #171b1e; }.attention-icon { display: grid; place-items: center; width: 29px; height: 29px; border-radius: 50%; color: #edb15b; background: #edb15b18; }.attention-copy { flex: 1; min-width: 0; }.attention-title { display: flex; align-items: center; gap: 9px; }.attention-title h2 { font-size: 12px; font-weight: 620; }.attention-copy p { display: flex; align-items: center; gap: 12px; margin-top: 5px; font-size: 11px; }.attention-copy p span { color: #8997a3; }.attention-copy p b { color: #d4dce2; font-weight: 450; }.attention-time { display: grid; justify-items: end; gap: 6px; color: #84919d; font-size: 10px; }.attention-time a, .panel-heading a { display: flex; align-items: center; gap: 2px; color: #82adff; } +.visual-grid, .lower-grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 10px; margin-top: 10px; }.lower-grid { grid-template-columns: 1.35fr 1fr; }.panel { min-width: 0; border: 1px solid #26343d; border-radius: 6px; background: #111b22; overflow: hidden; }.panel-heading { display: flex; align-items: center; justify-content: space-between; min-height: 43px; padding: 0 13px; border-bottom: 1px solid #26343d; }.panel-heading h2 { font-size: 12px; font-weight: 620; }.panel-heading a { font-size: 10px; }.chart-meta { display: flex; align-items: baseline; gap: 7px; padding: 12px 13px 0; color: #8795a1; font-size: 10px; }.chart-meta strong { color: #d8e0e6; font-size: 14px; font-weight: 600; }.line-chart { position: relative; height: 172px; padding: 8px 12px 29px 42px; }.line-chart::before { content: ""; position: absolute; inset: 10px 12px 29px 42px; background: repeating-linear-gradient(to bottom, #2a3740 0 1px, transparent 1px 32px); opacity: .7; }.line-chart svg { position: relative; width: 100%; height: 130px; overflow: visible; }.chart-area { fill: url(#chart-fill); }.chart-line { fill: none; stroke: #75a4ff; stroke-width: 2; vector-effect: non-scaling-stroke; }.y-labels { position: absolute; z-index: 1; left: 13px; top: 3px; bottom: 32px; display: flex; flex-direction: column; justify-content: space-between; color: #778692; font: 9px var(--mono); }.x-labels { position: absolute; left: 42px; right: 12px; bottom: 7px; display: flex; justify-content: space-between; color: #778692; font: 9px var(--mono); }.delivery-body { display: flex; align-items: center; justify-content: center; gap: 30px; min-height: 192px; padding: 10px 20px; }.donut { display: grid; place-items: center; width: 116px; height: 116px; border-radius: 50%; background: conic-gradient(#45d5a3 0 97.4%, #e0a549 97.4% 98.8%, #ed6d73 98.8% 99.7%, #718297 99.7%); }.donut::before { content: ""; position: absolute; width: 88px; height: 88px; border-radius: 50%; background: #111b22; }.donut > div { position: relative; display: grid; justify-items: center; }.donut strong { font-size: 16px; }.donut span { color: #8795a1; font-size: 10px; }.legend { display: grid; gap: 10px; min-width: 138px; list-style: none; padding: 0; }.legend li { display: grid; grid-template-columns: 9px 1fr auto; align-items: center; gap: 7px; color: #b4bfc8; font-size: 10px; }.legend i { width: 8px; height: 8px; border-radius: 50%; }.legend b { color: #dce4e9; font-weight: 500; }.legend-green { background: #45d5a3; }.legend-amber { background: #e0a549; }.legend-red { background: #ed6d73; }.legend-slate { background: #718297; } +.activity-list, .site-list { display: grid; }.activity-row, .site-row { display: flex; align-items: center; min-height: 45px; padding: 0 13px; border-bottom: 1px solid #202c34; font-size: 10px; }.activity-row:last-child, .site-row:last-child { border-bottom: 0; }.activity-row:hover, .site-row:hover { background: #17232b; }.event-icon { display: grid; place-items: center; width: 20px; height: 20px; margin-right: 9px; border-radius: 50%; }.event-good { color: #56d5a7; background: #56d5a71c; }.event-warn { color: #ecb05b; background: #ecb05b1c; }.event-bad { color: #ef7077; background: #ef70771c; }.activity-row time { width: 39px; color: #7d8b96; font: 10px var(--mono); }.event-copy { display: grid; gap: 1px; flex: 1; min-width: 0; }.event-copy strong { color: #cdd6dc; font-size: 10px; font-weight: 530; }.event-copy small { overflow: hidden; color: #7f8d98; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }.status { display: inline-flex; align-items: center; gap: 5px; width: fit-content; padding: 3px 6px; border-radius: 4px; font-size: 9px; white-space: nowrap; }.status-dot { width: 5px; height: 5px; border-radius: 50%; }.status-good { color: #54d3a4; background: #54d3a414; }.status-good .status-dot { background: #54d3a4; }.status-warn { color: #edb15b; background: #edb15b16; }.status-warn .status-dot { background: #edb15b; }.status-bad { color: #ef7077; background: #ef707716; }.status-bad .status-dot { background: #ef7077; }.status-neutral { color: #a8b3bd; background: #a8b3bd16; }.site-row { min-height: 42px; }.site-icon { display: grid; place-items: center; width: 23px; height: 23px; margin-right: 9px; border-radius: 4px; color: #a5b7c7; background: #24323c; }.site-copy { display: grid; gap: 1px; flex: 1; min-width: 0; }.site-copy strong { font-weight: 530; }.site-copy small { color: #7f8c97; font: 9px var(--mono); }.site-leads { margin-right: 12px; color: #778792; font: 9px var(--mono); }.overview-footer { display: flex; justify-content: space-between; padding: 17px 1px 0; color: #6f7d88; font-size: 10px; }.overview-footer span:first-child { display: flex; align-items: center; gap: 7px; }.footer-dot { width: 5px; height: 5px; box-shadow: none; } +.site-rate { margin-right: 12px; color: #778792; font: 9px var(--mono); } + +/* Keep secondary values in flow before the dashboard becomes cramped. */ +@media (max-width: 1120px) { + .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .metric { min-width: 0; } + .metric em { position: static; display: block; margin-top: 5px; line-height: 1.3; } +} + +/* Give the two visual panels and lower lists useful reading width on tablets. */ +@media (max-width: 1100px) { + .overview-sidebar { width: 188px; } + .overview-main { width: calc(100% - 188px); margin-left: 188px; } + .overview-content { padding-inline: 18px; } + .visual-grid, .lower-grid { grid-template-columns: 1fr; } + .delivery-body { min-height: 176px; } + .site-row { min-width: 0; } +} + +@media (max-width: 900px) { + .overview-topbar { padding-inline: 18px; } + .header-filters .filter-button:last-child { display: none; } +} + +@media (max-width: 720px) { + .overview-sidebar { position: static; width: 56px; padding: 18px 8px; } + .overview-brand { padding: 0 10px 24px; } + .overview-brand > span:last-child, .nav-item span, .nav-item b, .sidebar-bottom .nav-item, .workspace-copy, .workspace-switcher > svg { display: none; } + .nav-item, .workspace-switcher { justify-content: center; } + .nav-item { padding-inline: 0; } + .workspace-switcher { padding-inline: 0; } + .overview-main { width: calc(100% - 56px); margin-left: 0; } + .overview-topbar { padding: 0 15px; } + .search-trigger { width: 150px; } + .topbar-actions { gap: 8px; } + .agency-button { display: none; } + .overview-content { padding: 26px 14px; } + .page-header { gap: 15px; flex-direction: column; } + .metric-grid { grid-template-columns: 1fr; } + .metric strong { font-size: 21px; } + .delivery-body { flex-direction: column; gap: 18px; min-height: 250px; padding-block: 18px; } + .donut { flex: 0 0 116px; } + .legend { width: min(100%, 220px); min-width: 0; } + .site-row { padding-inline: 11px; } + .site-copy { min-width: 0; } + .site-copy strong, .site-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .site-leads, .site-rate { display: none; } + .overview-footer { padding-bottom: 10px; } +} + +/* Structural dividers stay visible without making each row feel boxed in. */ +.activity-row, .site-row { border-bottom-color: #1c272d; } + +/* Shared semantic palette for the Overview shell. */ +.overview-shell { background: var(--bg); color: var(--text-primary); } +.overview-sidebar { background: var(--sidebar); border-color: var(--border); } +.overview-main, .panel, .metric { border-color: var(--border); } +.panel, .metric { background: var(--surface); } +.overview-nav .nav-item:hover, .activity-row:hover, .site-row:hover { background: var(--surface-hover); } +.overview-sidebar, .panel-heading, .overview-topbar { border-color: var(--border-subtle); } +.overview-brand, .page-header h1, .panel-heading h2, .metric strong { color: var(--text-primary); } +.portfolio-status, .metric span, .chart-meta, .site-leads, .site-rate { color: var(--text-secondary); } +.pulse-dot, .footer-dot { background: var(--success); } +.trend-positive, .status-good { color: var(--success); } +.trend-attention, .status-warn { color: var(--warning); } +.status-bad { color: var(--danger); } diff --git a/frontend/src/app/overview/page.tsx b/frontend/src/app/overview/page.tsx new file mode 100644 index 0000000..196d609 --- /dev/null +++ b/frontend/src/app/overview/page.tsx @@ -0,0 +1,6 @@ +import "../overview.css"; +import { Overview } from "@/features/overview/overview"; + +export default function OverviewPage() { + return ; +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 5e2ad89..2d0fb1d 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,6 +1,5 @@ -import { loadDesk } from "@/features/desk/load"; -import { Desk } from "@/features/desk/desk"; +import { redirect } from "next/navigation"; -export default async function Page({ searchParams }: PageProps<"/">) { - return ; +export default function Page() { + redirect("/overview"); } diff --git a/frontend/src/app/reports/page.tsx b/frontend/src/app/reports/page.tsx new file mode 100644 index 0000000..362c244 --- /dev/null +++ b/frontend/src/app/reports/page.tsx @@ -0,0 +1,2 @@ +import { ReportsPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx new file mode 100644 index 0000000..79993db --- /dev/null +++ b/frontend/src/app/settings/page.tsx @@ -0,0 +1,2 @@ +import { SettingsPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/app/sites/[siteId]/page.tsx b/frontend/src/app/sites/[siteId]/page.tsx new file mode 100644 index 0000000..342ed5a --- /dev/null +++ b/frontend/src/app/sites/[siteId]/page.tsx @@ -0,0 +1,2 @@ +import { SiteDetailPage } from "@/features/dashboard/pages"; +export default async function Page({ params }: { params: Promise<{ siteId: string }> }) { return ; } diff --git a/frontend/src/app/sites/page.tsx b/frontend/src/app/sites/page.tsx new file mode 100644 index 0000000..e45188a --- /dev/null +++ b/frontend/src/app/sites/page.tsx @@ -0,0 +1,2 @@ +import { SitesPage } from "@/features/dashboard/pages"; +export default function Page() { return ; } diff --git a/frontend/src/components/app-shell.tsx b/frontend/src/components/app-shell.tsx new file mode 100644 index 0000000..7062a72 --- /dev/null +++ b/frontend/src/components/app-shell.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from "react"; +import { Sidebar } from "./sidebar"; +import { TopBar } from "./top-bar"; + +export function AppShell({ children }: { children: ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/frontend/src/components/dashboard-ui.tsx b/frontend/src/components/dashboard-ui.tsx new file mode 100644 index 0000000..cb2ff3a --- /dev/null +++ b/frontend/src/components/dashboard-ui.tsx @@ -0,0 +1,67 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; +import { ChevronRight, CircleAlert, CircleCheck, CircleHelp } from "lucide-react"; +import type { HealthStatus, StatusTone } from "@/lib/mock-data"; + +const healthTone: Record = { Healthy: "good", Attention: "warn", Down: "bad", Unknown: "neutral" }; + +export function DashboardPage({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function Breadcrumbs({ items }: { items: { label: string; href?: string }[] }) { + return ; +} + +export function PageHeader({ title, description, actions, breadcrumbs }: { title: string; description?: string; actions?: ReactNode; breadcrumbs?: { label: string; href?: string }[] }) { + return
{breadcrumbs && }

{title}

{description &&

{description}

}
{actions &&
{actions}
}
; +} + +export function Panel({ children, className = "" }: { children: ReactNode; className?: string }) { + return
{children}
; +} + +export function PanelTitle({ title, action }: { title: string; action?: ReactNode }) { + return

{title}

{action}
; +} + +export function StatusBadge({ label, tone, status }: { label?: string; tone?: StatusTone; status?: HealthStatus }) { + const resolvedTone = status ? healthTone[status] : tone ?? "neutral"; + return {label ?? status}; +} + +export function HealthBadge({ status }: { status: HealthStatus }) { + return ; +} + +export function StatStrip({ items }: { items: { label: string; value: string; detail?: string }[] }) { + return
{items.map((item) =>
{item.label}{item.value}{item.detail && {item.detail}}
)}
; +} + +export function DataTable({ headers, children, empty }: { headers: string[]; children?: ReactNode; empty?: ReactNode }) { + return
{headers.map((header) => )}{children ?? }
{header}
{empty}
; +} + +export function EmptyState({ title, description }: { title: string; description?: string }) { + return
{title}{description &&

{description}

}
; +} + +export function LoadingState() { + return
; +} + +export function DetailFields({ fields }: { fields: { label: string; value: ReactNode; mono?: boolean }[] }) { + return
{fields.map((field) =>
{field.label}
{field.value}
)}
; +} + +export function Timeline({ items }: { items: { label: string; time: string; detail?: string; tone?: StatusTone }[] }) { + return
    {items.map((item) =>
  1. {item.tone === "warn" ? : }
    {item.label}{item.detail &&

    {item.detail}

    }
  2. )}
; +} + +export function CodeBlock({ title, children }: { title: string; children: ReactNode }) { + return
{title}
{children}
; +} + +export function LinkButton({ href, children }: { href: string; children: ReactNode }) { + return {children}; +} diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx new file mode 100644 index 0000000..7cd5318 --- /dev/null +++ b/frontend/src/components/sidebar.tsx @@ -0,0 +1,57 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + Activity, + ChevronDown, + CircleAlert, + FileText, + Globe2, + LayoutGrid, + ListChecks, + Settings, + Users, + Zap, +} from "lucide-react"; + +type NavigationItem = { + label: string; + href: string; + icon: typeof LayoutGrid; + badge?: string; +}; + +const navigation: NavigationItem[] = [ + { label: "Overview", href: "/overview", icon: LayoutGrid }, + { label: "Clients", href: "/clients", icon: Users }, + { label: "Sites", href: "/sites", icon: Globe2 }, + { label: "Leads", href: "/leads", icon: Zap }, + { label: "Incidents", href: "/incidents", icon: CircleAlert, badge: "1" }, + { label: "Deliveries", href: "/deliveries", icon: ListChecks }, + { label: "Monitoring", href: "/monitoring", icon: Activity }, + { label: "Reports", href: "/reports", icon: FileText }, +]; + +function BrandMark() { + return