density
This commit is contained in:
@@ -23,6 +23,9 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
|
|||||||
- Work-order `On Hold` quick status changes now require a recorded hold reason and persist the active blocker on the work-order record and audit trail
|
- Work-order `On Hold` quick status changes now require a recorded hold reason and persist the active blocker on the work-order record and audit trail
|
||||||
- Project milestone cards now support inline quick status actions for start, block, complete, reset, and reopen flows directly from the project detail view
|
- Project milestone cards now support inline quick status actions for start, block, complete, reset, and reopen flows directly from the project detail view
|
||||||
- Project milestones with status, due dates, notes, and edit-time sequencing inside the project workflow
|
- Project milestones with status, due dates, notes, and edit-time sequencing inside the project workflow
|
||||||
|
- UI density standardization pass across app shell, dashboard, finance, project detail, manufacturing detail, and admin surfaces, including tighter panel spacing, more compact shell/navigation spacing, and removal of redundant explanatory subcopy in favor of concise uppercase section labels
|
||||||
|
- Continued density standardization across CRM, inventory, sales, purchasing, and shipping list/detail surfaces so module headers, filter bars, and status panels follow the same tighter uppercase operational pattern
|
||||||
|
- Continued density standardization across CRM, sales, purchasing, shipping, manufacturing, and project form/detail headers so editor and record surfaces now follow the same compact uppercase pattern with less redundant helper copy
|
||||||
- Project-side milestone and work-order rollups surfaced on project list and detail pages
|
- Project-side milestone and work-order rollups surfaced on project list and detail pages
|
||||||
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
|
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
|
||||||
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
|
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
|
||||||
|
|||||||
@@ -55,11 +55,13 @@ This repository implements the platform foundation milestone:
|
|||||||
6. Any non-filter UI that looks up records or items must use a searchable picker/autocomplete, not a long static dropdown.
|
6. Any non-filter UI that looks up records or items must use a searchable picker/autocomplete, not a long static dropdown.
|
||||||
7. Inventory items must carry both `defaultCost` and `defaultPrice`; sales documents should default line pricing from the selected item `defaultPrice`.
|
7. Inventory items must carry both `defaultCost` and `defaultPrice`; sales documents should default line pricing from the selected item `defaultPrice`.
|
||||||
8. Maintain the denser UI baseline on active screens; avoid reintroducing oversized `px-4 py-3` style controls, tall action bars, or overly loose card spacing without a specific reason.
|
8. Maintain the denser UI baseline on active screens; avoid reintroducing oversized `px-4 py-3` style controls, tall action bars, or overly loose card spacing without a specific reason.
|
||||||
9. Treat the landing page as `Dashboard`: a metric-oriented, modular command surface that should accumulate reusable operational panels over time.
|
9. Prefer concise uppercase module and section labels in the live interface, and avoid redundant descriptive subcopy when the surrounding data already makes the purpose clear.
|
||||||
10. Purchase-order item selection must be restricted to inventory items where `isPurchasable = true`.
|
10. When designing operational pages, bias toward information density: tighter panel padding, smaller stack gaps, and fewer explanatory filler blocks.
|
||||||
11. Treat `Projects` as a first-class cross-module domain tying together CRM, sales, inventory, purchasing, shipping, and planning; do not bury it as a one-off manufacturing subfeature.
|
11. Treat the landing page as `Dashboard`: a metric-oriented, modular command surface that should accumulate reusable operational panels over time.
|
||||||
12. Keep `Projects`, `Manufacturing`, and `Planning` distinct: projects are long-running program records, manufacturing is execution, and planning is scheduling/visibility.
|
12. Purchase-order item selection must be restricted to inventory items where `isPurchasable = true`.
|
||||||
13. New top-level modules added to the app shell should include a matching SVG icon in navigation so the module list remains visually scannable.
|
13. Treat `Projects` as a first-class cross-module domain tying together CRM, sales, inventory, purchasing, shipping, and planning; do not bury it as a one-off manufacturing subfeature.
|
||||||
|
14. Keep `Projects`, `Manufacturing`, and `Planning` distinct: projects are long-running program records, manufacturing is execution, and planning is scheduling/visibility.
|
||||||
|
15. New top-level modules added to the app shell should include a matching SVG icon in navigation so the module list remains visually scannable.
|
||||||
|
|
||||||
## Operational notes
|
## Operational notes
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ Navigation direction:
|
|||||||
- module navigation now uses inline SVG icons alongside labels
|
- module navigation now uses inline SVG icons alongside labels
|
||||||
- new modules should add a clear, domain-appropriate SVG icon when they are added to the shell
|
- new modules should add a clear, domain-appropriate SVG icon when they are added to the shell
|
||||||
- icons should stay lightweight, theme-aware, and dependency-free unless there is a strong reason to introduce a shared icon package
|
- icons should stay lightweight, theme-aware, and dependency-free unless there is a strong reason to introduce a shared icon package
|
||||||
|
- active operational screens should default to a denser layout baseline with tighter card padding, smaller inter-panel gaps, and less decorative negative space
|
||||||
|
- module headers and section labels should prefer uppercase naming and concise operational wording instead of redundant explanatory subcopy inside the working interface
|
||||||
|
|
||||||
## Finance Direction
|
## Finance Direction
|
||||||
|
|
||||||
|
|||||||
@@ -200,19 +200,19 @@ export function AppShell() {
|
|||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen px-4 py-5 xl:px-6 2xl:px-8">
|
<div className="min-h-screen px-3 py-3 xl:px-4 2xl:px-5">
|
||||||
<div className="mx-auto flex w-full max-w-[1760px] gap-3 2xl:gap-4">
|
<div className="mx-auto flex w-full max-w-[1760px] gap-2.5 2xl:gap-3">
|
||||||
<aside className="hidden w-72 shrink-0 flex-col rounded-[22px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur md:flex 2xl:w-80">
|
<aside className="hidden w-72 shrink-0 flex-col rounded-[20px] border border-line/70 bg-surface/90 p-3 shadow-panel backdrop-blur md:flex 2xl:w-80">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-extrabold uppercase tracking-[0.24em] text-text">CODEXIUM</h1>
|
<h1 className="text-xl font-extrabold uppercase tracking-[0.24em] text-text">CODEXIUM</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav className="mt-6 space-y-2">
|
<nav className="mt-4 space-y-1.5">
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={link.to}
|
key={link.to}
|
||||||
to={link.to}
|
to={link.to}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex items-center gap-2 rounded-2xl px-2 py-2 text-sm font-semibold transition ${
|
`flex items-center gap-2 rounded-xl px-2.5 py-2 text-[12px] font-semibold uppercase tracking-[0.12em] transition ${
|
||||||
isActive ? "bg-brand text-white" : "text-text hover:bg-page"
|
isActive ? "bg-brand text-white" : "text-text hover:bg-page"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -222,12 +222,12 @@ export function AppShell() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="mt-auto space-y-3">
|
<div className="mt-auto space-y-2.5">
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/70 p-3">
|
<div className="rounded-[16px] border border-line/70 bg-page/70 p-2.5">
|
||||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted">Theme</p>
|
<p className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">Theme</p>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/70 p-4">
|
<div className="rounded-[16px] border border-line/70 bg-page/70 p-3">
|
||||||
<p className="text-sm font-semibold text-text">{user?.firstName} {user?.lastName}</p>
|
<p className="text-sm font-semibold text-text">{user?.firstName} {user?.lastName}</p>
|
||||||
<p className="text-xs text-muted">{user?.email}</p>
|
<p className="text-xs text-muted">{user?.email}</p>
|
||||||
<button
|
<button
|
||||||
@@ -235,7 +235,7 @@ export function AppShell() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
void logout();
|
void logout();
|
||||||
}}
|
}}
|
||||||
className="mt-4 rounded-xl bg-text px-4 py-2 text-sm font-semibold text-page"
|
className="mt-3 rounded-xl bg-text px-3 py-2 text-sm font-semibold text-page"
|
||||||
>
|
>
|
||||||
Sign out
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
@@ -243,13 +243,13 @@ export function AppShell() {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="min-w-0 flex-1">
|
<main className="min-w-0 flex-1">
|
||||||
<nav className="mb-4 flex gap-3 overflow-x-auto rounded-[20px] border border-line/70 bg-surface/85 p-3 shadow-panel backdrop-blur md:hidden">
|
<nav className="mb-3 flex gap-2 overflow-x-auto rounded-[18px] border border-line/70 bg-surface/85 p-2.5 shadow-panel backdrop-blur md:hidden">
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={link.to}
|
key={link.to}
|
||||||
to={link.to}
|
to={link.to}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`inline-flex whitespace-nowrap items-center gap-2 rounded-2xl px-4 py-2 text-sm font-semibold transition ${
|
`inline-flex whitespace-nowrap items-center gap-2 rounded-xl px-3 py-2 text-[12px] font-semibold uppercase tracking-[0.12em] transition ${
|
||||||
isActive ? "bg-brand text-white" : "bg-page/70 text-text"
|
isActive ? "bg-brand text-white" : "bg-page/70 text-text"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ export function AppShell() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="mb-4 md:hidden">
|
<div className="mb-3 md:hidden">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -4,6 +4,32 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.page-stack {
|
||||||
|
@apply space-y-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-panel {
|
||||||
|
@apply rounded-[18px] border border-line/70 bg-surface/90 p-3 shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-panel-tight {
|
||||||
|
@apply rounded-[16px] border border-line/70 bg-page/60 px-3 py-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-kicker {
|
||||||
|
@apply text-[11px] font-semibold uppercase tracking-[0.24em] text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-kicker {
|
||||||
|
@apply text-[11px] font-semibold uppercase tracking-[0.18em] text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
@apply mt-1 text-xl font-bold uppercase tracking-[0.08em] text-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--font-family: "Manrope";
|
--font-family: "Manrope";
|
||||||
|
|||||||
@@ -111,21 +111,19 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Detail</p>
|
<p className="section-kicker">CRM DETAIL</p>
|
||||||
<h3 className="mt-2 text-2xl font-bold text-text">{record.name}</h3>
|
<h3 className="module-title">{record.name}</h3>
|
||||||
<div className="mt-4">
|
<div className="mt-2.5">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-2">
|
||||||
<CrmStatusBadge status={record.status} />
|
<CrmStatusBadge status={record.status} />
|
||||||
{record.lifecycleStage ? <CrmLifecycleBadge stage={record.lifecycleStage} /> : null}
|
{record.lifecycleStage ? <CrmLifecycleBadge stage={record.lifecycleStage} /> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm text-muted">
|
<p className="mt-2 text-sm text-muted">UPDATED {new Date(record.updatedAt).toLocaleString()}</p>
|
||||||
{config.singularLabel} record last updated {new Date(record.updatedAt).toLocaleString()}.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Link
|
<Link
|
||||||
@@ -146,8 +144,8 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 2xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
|
<div className="grid gap-3 2xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
|
||||||
<article className="min-w-0 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel min-w-0">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contact</p>
|
<p className="section-kicker">CONTACT</p>
|
||||||
<dl className="mt-5 grid gap-3 xl:grid-cols-2">
|
<dl className="mt-5 grid gap-3 xl:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt>
|
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt>
|
||||||
@@ -176,8 +174,8 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
<article className="min-w-0 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel min-w-0">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Internal Notes</p>
|
<p className="section-kicker">INTERNAL NOTES</p>
|
||||||
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">
|
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">
|
||||||
{record.notes || "No internal notes recorded for this account yet."}
|
{record.notes || "No internal notes recorded for this account yet."}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -110,17 +110,14 @@ export function CrmFormPage({ entity, mode }: CrmFormPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="page-stack" onSubmit={handleSubmit}>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Editor</p>
|
<p className="section-kicker">CRM EDITOR</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">
|
<h3 className="module-title">
|
||||||
{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}
|
{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">
|
|
||||||
Capture the operational contact and address details needed for quoting, purchasing, and shipping workflows.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
to={mode === "create" ? config.routeBase : `${config.routeBase}/${recordId}`}
|
to={mode === "create" ? config.routeBase : `${config.routeBase}/${recordId}`}
|
||||||
@@ -130,9 +127,9 @@ export function CrmFormPage({ entity, mode }: CrmFormPageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="space-y-3 surface-panel">
|
||||||
<CrmRecordForm entity={entity} form={form} hierarchyOptions={hierarchyOptions} onChange={updateField} />
|
<CrmRecordForm entity={entity} form={form} hierarchyOptions={hierarchyOptions} onChange={updateField} />
|
||||||
<div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-2.5 rounded-[16px] border border-line/70 bg-page/70 px-3 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<span className="min-w-0 text-sm text-muted">{status}</span>
|
<span className="min-w-0 text-sm text-muted">{status}</span>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -55,14 +55,11 @@ export function CrmListPage({ entity }: CrmListPageProps) {
|
|||||||
}, [config.collectionLabel, entity, lifecycleFilter, operationalFilter, searchTerm, stateFilter, statusFilter, token]);
|
}, [config.collectionLabel, entity, lifecycleFilter, operationalFilter, searchTerm, stateFilter, statusFilter, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM</p>
|
<p className="section-kicker">CRM</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">{config.collectionLabel}</h3>
|
<h3 className="module-title">{config.collectionLabel}</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">
|
|
||||||
Operational contact records, shipping addresses, and account context for active {config.collectionLabel.toLowerCase()}.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link
|
<Link
|
||||||
@@ -73,7 +70,7 @@ export function CrmListPage({ entity }: CrmListPageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 grid gap-3 rounded-[18px] border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.35fr_0.8fr_0.8fr_0.9fr_0.9fr]">
|
<div className="mt-4 grid gap-2.5 rounded-[16px] border border-line/70 bg-page/60 p-2.5 xl:grid-cols-[1.35fr_0.8fr_0.8fr_0.9fr_0.9fr]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input
|
<input
|
||||||
@@ -137,13 +134,13 @@ export function CrmListPage({ entity }: CrmListPageProps) {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-4 rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
{records.length === 0 ? (
|
{records.length === 0 ? (
|
||||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-4 py-7 text-center text-sm text-muted">
|
||||||
{config.emptyMessage}
|
{config.emptyMessage}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
|
<div className="mt-4 overflow-hidden rounded-[16px] border border-line/70">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead className="bg-page/80 text-left text-muted">
|
<thead className="bg-page/80 text-left text-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -82,19 +82,16 @@ function StackedBar({
|
|||||||
|
|
||||||
function DashboardCard({
|
function DashboardCard({
|
||||||
eyebrow,
|
eyebrow,
|
||||||
title,
|
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
}: {
|
}: {
|
||||||
eyebrow: string;
|
eyebrow: string;
|
||||||
title: string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<article className={`rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5 ${className}`.trim()}>
|
<article className={`surface-panel ${className}`.trim()}>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{eyebrow}</p>
|
<p className="section-kicker">{eyebrow}</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">{title}</h3>
|
|
||||||
{children}
|
{children}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
@@ -290,30 +287,30 @@ export function DashboardPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="page-stack">
|
||||||
{error ? <div className="rounded-[18px] border border-amber-400/30 bg-amber-500/12 px-3 py-3 text-sm text-amber-700 dark:text-amber-300">{error}</div> : null}
|
{error ? <div className="rounded-[16px] border border-amber-400/30 bg-amber-500/12 px-3 py-2.5 text-sm text-amber-700 dark:text-amber-300">{error}</div> : null}
|
||||||
<section className="grid gap-3 xl:grid-cols-6">
|
<section className="grid gap-3 xl:grid-cols-6">
|
||||||
{metricCards.map((card) => (
|
{metricCards.map((card) => (
|
||||||
<article key={card.label} className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
|
<article key={card.label} className="surface-panel-tight">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{card.label}</p>
|
<p className="metric-kicker">{card.label}</p>
|
||||||
<div className="mt-2 text-xl font-extrabold text-text">{isLoading ? "Loading..." : card.value}</div>
|
<div className="mt-1.5 text-xl font-extrabold text-text">{isLoading ? "Loading..." : card.value}</div>
|
||||||
<div className="mt-2 flex items-center gap-3">
|
<div className="mt-1.5 flex items-center gap-2.5">
|
||||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-page/80">
|
<div className="h-2 flex-1 overflow-hidden rounded-full bg-page/80">
|
||||||
<div className={`h-full rounded-full ${card.tone}`} style={{ width: isLoading ? "35%" : "100%" }} />
|
<div className={`h-full rounded-full ${card.tone}`} style={{ width: isLoading ? "35%" : "100%" }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted">Live</span>
|
<span className="text-xs text-muted">Live</span>
|
||||||
</div>
|
</div>
|
||||||
{card.secondary ? <div className="mt-2 text-xs text-muted">{card.secondary}</div> : null}
|
{card.secondary ? <div className="mt-1.5 text-xs text-muted">{card.secondary}</div> : null}
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
<section className="grid gap-3 xl:grid-cols-[1.2fr_0.8fr]">
|
<section className="grid gap-3 xl:grid-cols-[1.2fr_0.8fr]">
|
||||||
<DashboardCard eyebrow="Commercial Surface" title="Revenue and document mix">
|
<DashboardCard eyebrow="COMMERCIAL">
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quotes</div>
|
<div className="metric-kicker">Quotes</div>
|
||||||
<div className="mt-2 text-2xl font-bold text-text">{snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access"}</div>
|
<div className="mt-1.5 text-2xl font-bold text-text">{snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access"}</div>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-2.5 space-y-2">
|
||||||
<div className="flex items-center justify-between text-xs text-muted">
|
<div className="flex items-center justify-between text-xs text-muted">
|
||||||
<span>Draft</span>
|
<span>Draft</span>
|
||||||
<span>{draftQuoteCount}</span>
|
<span>{draftQuoteCount}</span>
|
||||||
@@ -326,10 +323,10 @@ export function DashboardPage() {
|
|||||||
<ProgressBar value={approvedQuoteCount} total={Math.max(quoteCount, 1)} tone="bg-emerald-500" />
|
<ProgressBar value={approvedQuoteCount} total={Math.max(quoteCount, 1)} tone="bg-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Orders</div>
|
<div className="metric-kicker">Orders</div>
|
||||||
<div className="mt-2 text-2xl font-bold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</div>
|
<div className="mt-1.5 text-2xl font-bold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</div>
|
||||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
<div className="mt-2.5 grid gap-2.5 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted">Issued / approved</div>
|
<div className="text-xs text-muted">Issued / approved</div>
|
||||||
<div className="mt-1 text-lg font-semibold text-text">{issuedOrderCount}</div>
|
<div className="mt-1 text-lg font-semibold text-text">{issuedOrderCount}</div>
|
||||||
@@ -339,14 +336,14 @@ export function DashboardPage() {
|
|||||||
<div className="mt-1 text-lg font-semibold text-text">{orderCount}</div>
|
<div className="mt-1 text-lg font-semibold text-text">{orderCount}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2.5">
|
||||||
<ProgressBar value={issuedOrderCount} total={Math.max(orderCount, 1)} tone="bg-brand" />
|
<ProgressBar value={issuedOrderCount} total={Math.max(orderCount, 1)} tone="bg-brand" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
<DashboardCard eyebrow="CRM Footprint" title="Customer and vendor balance">
|
<DashboardCard eyebrow="CRM">
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-3 space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted">Active customers</span>
|
<span className="text-muted">Active customers</span>
|
||||||
@@ -356,21 +353,21 @@ export function DashboardPage() {
|
|||||||
<ProgressBar value={activeCustomerCount} total={Math.max(customerCount, 1)} tone="bg-emerald-500" />
|
<ProgressBar value={activeCustomerCount} total={Math.max(customerCount, 1)} tone="bg-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-2.5 sm:grid-cols-3">
|
||||||
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Customers</div>
|
<div className="metric-kicker">Customers</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{customerCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{customerCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Resellers</div>
|
<div className="metric-kicker">Resellers</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{resellerCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{resellerCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Vendors</div>
|
<div className="metric-kicker">Vendors</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{vendorCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{vendorCount}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted">Strategic accounts</span>
|
<span className="text-muted">Strategic accounts</span>
|
||||||
<span className="font-semibold text-text">{strategicCustomerCount}</span>
|
<span className="font-semibold text-text">{strategicCustomerCount}</span>
|
||||||
@@ -383,11 +380,11 @@ export function DashboardPage() {
|
|||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
</section>
|
</section>
|
||||||
<section className="grid gap-3 xl:grid-cols-3">
|
<section className="grid gap-3 xl:grid-cols-3">
|
||||||
<DashboardCard eyebrow="Inventory and Supply" title="Stock posture">
|
<DashboardCard eyebrow="INVENTORY">
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Item mix</div>
|
<div className="metric-kicker">Item Mix</div>
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-2.5 space-y-2.5">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between text-xs text-muted">
|
<div className="flex items-center justify-between text-xs text-muted">
|
||||||
<span>Active items</span>
|
<span>Active items</span>
|
||||||
@@ -417,14 +414,14 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Storage surface</div>
|
<div className="metric-kicker">Storage</div>
|
||||||
<div className="mt-3 grid gap-3">
|
<div className="mt-2.5 grid gap-2.5">
|
||||||
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
|
<div className="surface-panel-tight bg-surface/80">
|
||||||
<div className="text-xs text-muted">Warehouses</div>
|
<div className="text-xs text-muted">Warehouses</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{warehouseCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{warehouseCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
|
<div className="surface-panel-tight bg-surface/80">
|
||||||
<div className="text-xs text-muted">Locations</div>
|
<div className="text-xs text-muted">Locations</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{locationCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{locationCount}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,10 +429,10 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
<DashboardCard eyebrow="Supply Execution" title="Purchasing and manufacturing flow">
|
<DashboardCard eyebrow="SUPPLY">
|
||||||
<div className="mt-4 rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="mt-3 surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Open workload split</div>
|
<div className="metric-kicker">Open Workload</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2.5">
|
||||||
<StackedBar
|
<StackedBar
|
||||||
segments={[
|
segments={[
|
||||||
{ value: openPurchaseOrderCount, tone: "bg-teal-500" },
|
{ value: openPurchaseOrderCount, tone: "bg-teal-500" },
|
||||||
@@ -445,31 +442,31 @@ export function DashboardPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
|
||||||
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
|
<div className="surface-panel-tight bg-surface/80">
|
||||||
<div className="text-xs text-muted">Open PO queue</div>
|
<div className="text-xs text-muted">Open PO queue</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{openPurchaseOrderCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{openPurchaseOrderCount}</div>
|
||||||
<div className="mt-1 text-xs text-muted">{formatCurrency(purchaseOrderValue)} committed</div>
|
<div className="mt-1 text-xs text-muted">{formatCurrency(purchaseOrderValue)} committed</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
|
<div className="surface-panel-tight bg-surface/80">
|
||||||
<div className="text-xs text-muted">Active work orders</div>
|
<div className="text-xs text-muted">Active work orders</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{activeWorkOrderCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{activeWorkOrderCount}</div>
|
||||||
<div className="mt-1 text-xs text-muted">{overdueWorkOrderCount} overdue</div>
|
<div className="mt-1 text-xs text-muted">{overdueWorkOrderCount} overdue</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
|
<div className="surface-panel-tight bg-surface/80">
|
||||||
<div className="text-xs text-muted">Issued / approved POs</div>
|
<div className="text-xs text-muted">Issued / approved POs</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{issuedPurchaseOrderCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{issuedPurchaseOrderCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
|
<div className="surface-panel-tight bg-surface/80">
|
||||||
<div className="text-xs text-muted">Released WOs</div>
|
<div className="text-xs text-muted">Released WOs</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{releasedWorkOrderCount}</div>
|
<div className="mt-1 text-lg font-bold text-text">{releasedWorkOrderCount}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
<DashboardCard eyebrow="Readiness" title="Planning pressure">
|
<DashboardCard eyebrow="READINESS">
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-3 space-y-2.5">
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted">Shortage items</span>
|
<span className="text-muted">Shortage items</span>
|
||||||
<span className="font-semibold text-text">{planningRollup ? shortageItemCount : "No access"}</span>
|
<span className="font-semibold text-text">{planningRollup ? shortageItemCount : "No access"}</span>
|
||||||
@@ -478,9 +475,9 @@ export function DashboardPage() {
|
|||||||
<ProgressBar value={shortageItemCount} total={Math.max(planningItemCount, 1)} tone="bg-rose-500" />
|
<ProgressBar value={shortageItemCount} total={Math.max(planningItemCount, 1)} tone="bg-rose-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Build vs buy</div>
|
<div className="metric-kicker">Build Vs Buy</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2.5">
|
||||||
<StackedBar
|
<StackedBar
|
||||||
segments={[
|
segments={[
|
||||||
{ value: buildRecommendationCount, tone: "bg-indigo-500" },
|
{ value: buildRecommendationCount, tone: "bg-indigo-500" },
|
||||||
@@ -488,7 +485,7 @@ export function DashboardPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
<div className="mt-2.5 grid gap-2.5 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted">Build recommendations</div>
|
<div className="text-xs text-muted">Build recommendations</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? buildRecommendationCount : "No access"}</div>
|
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? buildRecommendationCount : "No access"}</div>
|
||||||
@@ -499,7 +496,7 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs text-muted">Uncovered quantity</div>
|
<div className="text-xs text-muted">Uncovered quantity</div>
|
||||||
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? totalUncoveredQuantity : "No access"}</div>
|
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? totalUncoveredQuantity : "No access"}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -507,11 +504,11 @@ export function DashboardPage() {
|
|||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
</section>
|
</section>
|
||||||
<section className="grid gap-3 xl:grid-cols-[0.95fr_1.05fr]">
|
<section className="grid gap-3 xl:grid-cols-[0.95fr_1.05fr]">
|
||||||
<DashboardCard eyebrow="Programs" title="Project and shipment execution">
|
<DashboardCard eyebrow="PROGRAMS">
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Projects</div>
|
<div className="metric-kicker">Projects</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2.5">
|
||||||
<StackedBar
|
<StackedBar
|
||||||
segments={[
|
segments={[
|
||||||
{ value: activeProjectCount, tone: "bg-violet-500" },
|
{ value: activeProjectCount, tone: "bg-violet-500" },
|
||||||
@@ -520,7 +517,7 @@ export function DashboardPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
<div className="mt-3 grid gap-2.5 sm:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted">Active</div>
|
<div className="text-xs text-muted">Active</div>
|
||||||
<div className="mt-1 font-semibold text-text">{activeProjectCount}</div>
|
<div className="mt-1 font-semibold text-text">{activeProjectCount}</div>
|
||||||
@@ -535,9 +532,9 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="surface-panel-tight">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Shipping</div>
|
<div className="metric-kicker">Shipping</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2.5">
|
||||||
<StackedBar
|
<StackedBar
|
||||||
segments={[
|
segments={[
|
||||||
{ value: activeShipmentCount, tone: "bg-brand" },
|
{ value: activeShipmentCount, tone: "bg-brand" },
|
||||||
@@ -546,7 +543,7 @@ export function DashboardPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
<div className="mt-3 grid gap-2.5 sm:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted">Open</div>
|
<div className="text-xs text-muted">Open</div>
|
||||||
<div className="mt-1 font-semibold text-text">{activeShipmentCount}</div>
|
<div className="mt-1 font-semibold text-text">{activeShipmentCount}</div>
|
||||||
@@ -563,8 +560,8 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
<DashboardCard eyebrow="Operations Mix" title="Cross-module volume">
|
<DashboardCard eyebrow="OPERATIONS">
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-3 space-y-2.5">
|
||||||
{[
|
{[
|
||||||
{ label: "Customers", value: customerCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-emerald-500" },
|
{ label: "Customers", value: customerCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-emerald-500" },
|
||||||
{ label: "Inventory items", value: itemCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-sky-500" },
|
{ label: "Inventory items", value: itemCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-sky-500" },
|
||||||
@@ -574,7 +571,7 @@ export function DashboardPage() {
|
|||||||
{ label: "Shipments", value: shipmentCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-brand" },
|
{ label: "Shipments", value: shipmentCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-brand" },
|
||||||
{ label: "Projects", value: projectCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-violet-500" },
|
{ label: "Projects", value: projectCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-violet-500" },
|
||||||
].map((row) => (
|
].map((row) => (
|
||||||
<div key={row.label} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div key={row.label} className="surface-panel-tight">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted">{row.label}</span>
|
<span className="text-muted">{row.label}</span>
|
||||||
<span className="font-semibold text-text">{row.value}</span>
|
<span className="font-semibold text-text">{row.value}</span>
|
||||||
@@ -585,7 +582,7 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{snapshot ? <div className="mt-4 text-xs text-muted">Refreshed {new Date(snapshot.refreshedAt).toLocaleString()}</div> : null}
|
{snapshot ? <div className="mt-3 text-xs text-muted">REFRESHED {new Date(snapshot.refreshedAt).toLocaleString()}</div> : null}
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,18 +192,15 @@ export function FinancePage() {
|
|||||||
const currencyCode = profile.currencyCode || "USD";
|
const currencyCode = profile.currencyCode || "USD";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Finance</p>
|
<p className="section-kicker">FINANCE</p>
|
||||||
<h2 className="mt-2 text-2xl font-bold text-text">Cash, spend, and CapEx control</h2>
|
<h2 className="module-title">CASH SPEND CAPEX</h2>
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
||||||
Track customer payments against sales orders, compare them to linked purchasing and manufacturing spend, and manage capital purchases for equipment, tooling, and consumables.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3 text-sm text-muted">
|
<div className="surface-panel-tight text-sm text-muted">
|
||||||
Live snapshot generated {new Date(dashboard.generatedAt).toLocaleString()}
|
SNAPSHOT {new Date(dashboard.generatedAt).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,22 +214,19 @@ export function FinancePage() {
|
|||||||
{ label: "Mfg Cost", value: formatCurrency(summary.manufacturingTotalCost, currencyCode) },
|
{ label: "Mfg Cost", value: formatCurrency(summary.manufacturingTotalCost, currencyCode) },
|
||||||
{ label: "CapEx Actual", value: formatCurrency(summary.capexActual, currencyCode) },
|
{ label: "CapEx Actual", value: formatCurrency(summary.capexActual, currencyCode) },
|
||||||
].map((card) => (
|
].map((card) => (
|
||||||
<article key={card.label} className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
|
<article key={card.label} className="surface-panel-tight bg-surface/90 shadow-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{card.label}</p>
|
<p className="metric-kicker">{card.label}</p>
|
||||||
<div className="mt-2 text-xl font-extrabold text-text">{card.value}</div>
|
<div className="mt-1.5 text-xl font-extrabold text-text">{card.value}</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(360px,0.85fr)]">
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(360px,0.85fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<p className="section-kicker">SALES ORDER LEDGER</p>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Sales Order Ledger</p>
|
|
||||||
<p className="mt-2 text-sm text-muted">Revenue, receipts, purchasing, and manufacturing cost by order.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-3 overflow-x-auto">
|
||||||
<div className="mt-5 overflow-x-auto">
|
|
||||||
<table className="min-w-full divide-y divide-line/60 text-sm">
|
<table className="min-w-full divide-y divide-line/60 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs uppercase tracking-[0.16em] text-muted">
|
<tr className="text-left text-xs uppercase tracking-[0.16em] text-muted">
|
||||||
@@ -290,9 +284,9 @@ export function FinancePage() {
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Costing Assumptions</p>
|
<p className="section-kicker">COSTING ASSUMPTIONS</p>
|
||||||
<div className="mt-4 grid gap-3">
|
<div className="mt-3 grid gap-2.5">
|
||||||
<label className="text-sm text-text">
|
<label className="text-sm text-text">
|
||||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Currency</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Currency</span>
|
||||||
<input value={profileForm.currencyCode} onChange={(event) => setProfileForm((current) => ({ ...current, currencyCode: event.target.value }))} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 outline-none" />
|
<input value={profileForm.currencyCode} onChange={(event) => setProfileForm((current) => ({ ...current, currencyCode: event.target.value }))} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 outline-none" />
|
||||||
@@ -307,15 +301,15 @@ export function FinancePage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<button type="button" onClick={() => void handleSaveProfile()} disabled={isSavingProfile} className="mt-4 inline-flex rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
|
<button type="button" onClick={() => void handleSaveProfile()} disabled={isSavingProfile} className="mt-3 inline-flex rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
|
||||||
{isSavingProfile ? "Saving..." : "Save assumptions"}
|
{isSavingProfile ? "Saving..." : "Save assumptions"}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Post Payment</p>
|
<p className="section-kicker">POST PAYMENT</p>
|
||||||
<div className="mt-4 grid gap-3">
|
<div className="mt-3 grid gap-2.5">
|
||||||
<select value={paymentForm.salesOrderId} onChange={(event) => setPaymentForm((current) => ({ ...current, salesOrderId: event.target.value }))} className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
<select value={paymentForm.salesOrderId} onChange={(event) => setPaymentForm((current) => ({ ...current, salesOrderId: event.target.value }))} className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
||||||
{salesOrders.map((order) => (
|
{salesOrders.map((order) => (
|
||||||
<option key={order.id} value={order.id}>
|
<option key={order.id} value={order.id}>
|
||||||
@@ -339,7 +333,7 @@ export function FinancePage() {
|
|||||||
<textarea value={paymentForm.notes} onChange={(event) => setPaymentForm((current) => ({ ...current, notes: event.target.value }))} placeholder="Notes" rows={3} className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none" />
|
<textarea value={paymentForm.notes} onChange={(event) => setPaymentForm((current) => ({ ...current, notes: event.target.value }))} placeholder="Notes" rows={3} className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none" />
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<button type="button" onClick={() => void handlePostPayment()} disabled={isPostingPayment || !paymentForm.salesOrderId || paymentForm.amount <= 0} className="mt-4 inline-flex rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
|
<button type="button" onClick={() => void handlePostPayment()} disabled={isPostingPayment || !paymentForm.salesOrderId || paymentForm.amount <= 0} className="mt-3 inline-flex rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
|
||||||
{isPostingPayment ? "Posting..." : "Post payment"}
|
{isPostingPayment ? "Posting..." : "Post payment"}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -348,19 +342,16 @@ export function FinancePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div><p className="section-kicker">RECENT PAYMENTS</p></div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Recent Payments</p>
|
|
||||||
<p className="mt-2 text-sm text-muted">Posted receipts linked directly to sales orders.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-3 space-y-2.5">
|
||||||
<div className="mt-5 space-y-3">
|
|
||||||
{payments.length === 0 ? (
|
{payments.length === 0 ? (
|
||||||
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No payments posted yet.</div>
|
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No payments posted yet.</div>
|
||||||
) : (
|
) : (
|
||||||
payments.map((payment) => (
|
payments.map((payment) => (
|
||||||
<div key={payment.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div key={payment.id} className="surface-panel-tight">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<Link to={`/sales/orders/${payment.salesOrderId}`} className="font-semibold text-brand hover:underline">
|
<Link to={`/sales/orders/${payment.salesOrderId}`} className="font-semibold text-brand hover:underline">
|
||||||
@@ -381,12 +372,9 @@ export function FinancePage() {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div><p className="section-kicker">CAPEX TRACKER</p></div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CapEx Tracker</p>
|
|
||||||
<p className="mt-2 text-sm text-muted">Manage equipment, tooling, and consumable capital plans with optional PO linkage.</p>
|
|
||||||
</div>
|
|
||||||
{editingCapexId ? (
|
{editingCapexId ? (
|
||||||
<button type="button" onClick={resetCapexForm} className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
<button type="button" onClick={resetCapexForm} className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||||
Clear edit
|
Clear edit
|
||||||
@@ -394,7 +382,7 @@ export function FinancePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-3 lg:grid-cols-2">
|
<div className="mt-3 grid gap-2.5 lg:grid-cols-2">
|
||||||
<input value={capexForm.title} onChange={(event) => setCapexForm((current) => ({ ...current, title: event.target.value }))} placeholder="CapEx title" className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none" />
|
<input value={capexForm.title} onChange={(event) => setCapexForm((current) => ({ ...current, title: event.target.value }))} placeholder="CapEx title" className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none" />
|
||||||
<select value={capexForm.category} onChange={(event) => setCapexForm((current) => ({ ...current, category: event.target.value as CapexCategory }))} className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
<select value={capexForm.category} onChange={(event) => setCapexForm((current) => ({ ...current, category: event.target.value as CapexCategory }))} className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
||||||
{capexCategories.map((category) => <option key={category} value={category}>{category}</option>)}
|
{capexCategories.map((category) => <option key={category} value={category}>{category}</option>)}
|
||||||
@@ -421,12 +409,12 @@ export function FinancePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<button type="button" onClick={() => void handleSaveCapex()} disabled={isSavingCapex || !capexForm.title.trim()} className="mt-4 inline-flex rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
|
<button type="button" onClick={() => void handleSaveCapex()} disabled={isSavingCapex || !capexForm.title.trim()} className="mt-3 inline-flex rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
|
||||||
{isSavingCapex ? "Saving..." : editingCapexId ? "Update CapEx" : "Create CapEx"}
|
{isSavingCapex ? "Saving..." : editingCapexId ? "Update CapEx" : "Create CapEx"}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-5 space-y-3">
|
<div className="mt-3 space-y-2.5">
|
||||||
{capex.length === 0 ? (
|
{capex.length === 0 ? (
|
||||||
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No CapEx entries yet.</div>
|
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No CapEx entries yet.</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -450,7 +438,7 @@ export function FinancePage() {
|
|||||||
notes: entry.notes,
|
notes: entry.notes,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="block w-full rounded-[18px] border border-line/70 bg-page/60 p-3 text-left transition hover:bg-page/80"
|
className="block w-full rounded-[16px] border border-line/70 bg-page/60 px-3 py-2.5 text-left transition hover:bg-page/80"
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -473,7 +461,7 @@ export function FinancePage() {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">
|
<div className="surface-panel text-sm text-muted">
|
||||||
{status}
|
{status}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -308,18 +308,18 @@ export function InventoryDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Detail</p>
|
<p className="section-kicker">INVENTORY DETAIL</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{item.sku}</h3>
|
<h3 className="module-title">{item.sku}</h3>
|
||||||
<p className="mt-1 text-sm text-text">{item.name}</p>
|
<p className="mt-1 text-sm text-text">{item.name}</p>
|
||||||
<div className="mt-4 flex flex-wrap gap-3">
|
<div className="mt-2.5 flex flex-wrap gap-2">
|
||||||
<InventoryTypeBadge type={item.type} />
|
<InventoryTypeBadge type={item.type} />
|
||||||
<InventoryStatusBadge status={item.status} />
|
<InventoryStatusBadge status={item.status} />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-sm text-muted">Last updated {new Date(item.updatedAt).toLocaleString()}.</p>
|
<p className="mt-2 text-sm text-muted">UPDATED {new Date(item.updatedAt).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Link to="/inventory/items" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to="/inventory/items" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
@@ -366,8 +366,8 @@ export function InventoryDetailPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]">
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Item Definition</p>
|
<p className="section-kicker">ITEM DEFINITION</p>
|
||||||
<dl className="mt-5 grid gap-3 xl:grid-cols-2">
|
<dl className="mt-5 grid gap-3 xl:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Description</dt>
|
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Description</dt>
|
||||||
@@ -397,9 +397,9 @@ export function InventoryDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Thumbnail</p>
|
<p className="section-kicker">THUMBNAIL</p>
|
||||||
<div className="mt-4 overflow-hidden rounded-[18px] border border-line/70 bg-page/70">
|
<div className="mt-3 overflow-hidden rounded-[18px] border border-line/70 bg-page/70">
|
||||||
{thumbnailPreviewUrl ? (
|
{thumbnailPreviewUrl ? (
|
||||||
<img src={thumbnailPreviewUrl} alt={`${item.sku} thumbnail`} className="aspect-square w-full object-cover" />
|
<img src={thumbnailPreviewUrl} alt={`${item.sku} thumbnail`} className="aspect-square w-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
@@ -408,19 +408,19 @@ export function InventoryDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-xs text-muted">
|
<div className="mt-2 text-xs text-muted">
|
||||||
{thumbnailAttachment ? `Current file: ${thumbnailAttachment.originalName}` : "Add or replace the thumbnail from the item edit page."}
|
{thumbnailAttachment ? `Current file: ${thumbnailAttachment.originalName}` : "Add or replace the thumbnail from the item edit page."}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]">
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Stock By Location</p>
|
<p className="section-kicker">STOCK BY LOCATION</p>
|
||||||
{item.stockBalances.length === 0 ? (
|
{item.stockBalances.length === 0 ? (
|
||||||
<p className="mt-4 text-sm text-muted">No stock or reservation balances have been posted for this item yet.</p>
|
<p className="mt-3 text-sm text-muted">No stock or reservation balances have been posted for this item yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{item.stockBalances.map((balance) => (
|
{item.stockBalances.map((balance) => (
|
||||||
<div key={`${balance.warehouseId}-${balance.locationId}`} className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
|
<div key={`${balance.warehouseId}-${balance.locationId}`} className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|||||||
@@ -41,14 +41,11 @@ export function InventoryListPage() {
|
|||||||
}, [searchTerm, statusFilter, token, typeFilter]);
|
}, [searchTerm, statusFilter, token, typeFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p>
|
<p className="section-kicker">INVENTORY</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Item Master</h3>
|
<h3 className="module-title">ITEM MASTER</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">
|
|
||||||
Core item and BOM definitions for purchased parts, manufactured items, assemblies, and service SKUs.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -61,7 +58,7 @@ export function InventoryListPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 grid gap-3 rounded-[18px] border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.3fr_0.8fr_0.8fr]">
|
<div className="mt-4 grid gap-2.5 rounded-[16px] border border-line/70 bg-page/60 p-2.5 xl:grid-cols-[1.3fr_0.8fr_0.8fr]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input
|
<input
|
||||||
@@ -100,13 +97,13 @@ export function InventoryListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-4 rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-4 py-7 text-center text-sm text-muted">
|
||||||
No inventory items have been added yet.
|
No inventory items have been added yet.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
|
<div className="mt-4 overflow-hidden rounded-[16px] border border-line/70">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead className="bg-page/80 text-left text-muted">
|
<thead className="bg-page/80 text-left text-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -391,17 +391,17 @@ export function WorkOrderDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Work Order</p>
|
<p className="section-kicker">WORK ORDER</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{workOrder.workOrderNumber}</h3>
|
<h3 className="module-title">{workOrder.workOrderNumber}</h3>
|
||||||
<p className="mt-1 text-sm text-text">{workOrder.itemSku} - {workOrder.itemName}</p>
|
<p className="mt-1 text-sm text-text">{workOrder.itemSku} - {workOrder.itemName}</p>
|
||||||
<div className="mt-3"><WorkOrderStatusBadge status={workOrder.status} /></div>
|
<div className="mt-2.5"><WorkOrderStatusBadge status={workOrder.status} /></div>
|
||||||
{workOrder.status === "ON_HOLD" && workOrder.holdReason ? (
|
{workOrder.status === "ON_HOLD" && workOrder.holdReason ? (
|
||||||
<div className="mt-3 max-w-2xl rounded-[18px] border border-amber-300/60 bg-amber-50 px-3 py-3 text-sm text-amber-900">
|
<div className="mt-2.5 max-w-2xl rounded-[16px] border border-amber-300/60 bg-amber-50 px-3 py-2.5 text-sm text-amber-900">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.18em]">Current Hold Reason</div>
|
<div className="metric-kicker text-amber-900">Current Hold Reason</div>
|
||||||
<div className="mt-2 whitespace-pre-line">{workOrder.holdReason}</div>
|
<div className="mt-2 whitespace-pre-line">{workOrder.holdReason}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -416,11 +416,10 @@ export function WorkOrderDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
|
<p className="section-kicker">QUICK ACTIONS</p>
|
||||||
<p className="mt-2 text-sm text-muted">Release, hold, or close administrative status from the work-order record.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{workOrderStatusOptions.map((option) => (
|
{workOrderStatusOptions.map((option) => (
|
||||||
|
|||||||
@@ -35,13 +35,12 @@ export function WorkOrderListPage() {
|
|||||||
}, [query, statusFilter, token]);
|
}, [query, statusFilter, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing</p>
|
<p className="section-kicker">MANUFACTURING</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">Work Orders</h3>
|
<h3 className="module-title">WORK ORDERS</h3>
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">Release and execute build work against manufactured or assembly inventory items, with project linkage and real inventory posting.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link to="/manufacturing/work-orders/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
|
<Link to="/manufacturing/work-orders/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
|
||||||
@@ -50,8 +49,8 @@ export function WorkOrderListPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_240px]">
|
<div className="grid gap-2.5 xl:grid-cols-[minmax(0,1fr)_240px]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
|
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
|
||||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search work order, item, or project" className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search work order, item, or project" className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||||
@@ -63,7 +62,7 @@ export function WorkOrderListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-3 rounded-[16px] border border-line/70 bg-page/70 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
</section>
|
</section>
|
||||||
{workOrders.length === 0 ? (
|
{workOrders.length === 0 ? (
|
||||||
<div className="rounded-[20px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No work orders are available yet.</div>
|
<div className="rounded-[20px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No work orders are available yet.</div>
|
||||||
|
|||||||
@@ -153,14 +153,14 @@ export function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project</p>
|
<p className="section-kicker">PROJECT</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{project.projectNumber}</h3>
|
<h3 className="module-title">{project.projectNumber}</h3>
|
||||||
<p className="mt-1 text-sm text-text">{project.name}</p>
|
<p className="mt-1 text-sm text-text">{project.name}</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-2.5 flex flex-wrap gap-2">
|
||||||
<ProjectStatusBadge status={project.status} />
|
<ProjectStatusBadge status={project.status} />
|
||||||
<ProjectPriorityBadge priority={project.priority} />
|
<ProjectPriorityBadge priority={project.priority} />
|
||||||
</div>
|
</div>
|
||||||
@@ -183,36 +183,34 @@ export function ProjectDetailPage() {
|
|||||||
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Linked Work Orders</p><div className="mt-2 text-base font-bold text-text">{project.rollups.workOrderCount}</div><div className="mt-1 text-xs text-muted">{project.rollups.activeWorkOrderCount} active</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Linked Work Orders</p><div className="mt-2 text-base font-bold text-text">{project.rollups.workOrderCount}</div><div className="mt-1 text-xs text-muted">{project.rollups.activeWorkOrderCount} active</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Overdue Work Orders</p><div className="mt-2 text-base font-bold text-text">{project.rollups.overdueWorkOrderCount}</div><div className="mt-1 text-xs text-muted">{project.rollups.completedWorkOrderCount} complete</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Overdue Work Orders</p><div className="mt-2 text-base font-bold text-text">{project.rollups.overdueWorkOrderCount}</div><div className="mt-1 text-xs text-muted">{project.rollups.completedWorkOrderCount} complete</div></article>
|
||||||
</section>
|
</section>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project Cockpit</p>
|
<p className="section-kicker">PROJECT COCKPIT</p>
|
||||||
<h4 className="mt-2 text-lg font-bold text-text">Cross-functional execution view</h4>
|
|
||||||
<p className="mt-2 text-sm text-muted">Commercial, supply, execution, purchasing, and delivery signals for this program in one place.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-right">
|
<div className="surface-panel-tight text-right">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Milestone Progress</div>
|
<div className="metric-kicker">Milestone Progress</div>
|
||||||
<div className="mt-1 text-2xl font-bold text-text">{completionPercent}%</div>
|
<div className="mt-1 text-2xl font-bold text-text">{completionPercent}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-4">
|
<div className="mt-3 grid gap-2.5 xl:grid-cols-4">
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Commercial</p><div className="mt-2 text-base font-bold text-text">{formatCurrency(project.cockpit.commercial.activeDocumentTotal)}</div><div className="mt-1 text-xs text-muted">{project.cockpit.commercial.activeDocumentNumber ? `${project.cockpit.commercial.activeDocumentNumber} - ${project.cockpit.commercial.activeDocumentStatus}` : "Link a quote or sales order"}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Commercial</p><div className="mt-2 text-base font-bold text-text">{formatCurrency(project.cockpit.commercial.activeDocumentTotal)}</div><div className="mt-1 text-xs text-muted">{project.cockpit.commercial.activeDocumentNumber ? `${project.cockpit.commercial.activeDocumentNumber} - ${project.cockpit.commercial.activeDocumentStatus}` : "Link a quote or sales order"}</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Supply</p><div className="mt-2 text-base font-bold text-text">{planning ? planning.summary.uncoveredItemCount : project.cockpit.risk.shortageItemCount} shortage items</div><div className="mt-1 text-xs text-muted">{planning ? `Build ${planning.summary.totalBuildQuantity} - Buy ${planning.summary.totalPurchaseQuantity}` : `Uncovered qty ${project.cockpit.risk.totalUncoveredQuantity}`}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Supply</p><div className="mt-2 text-base font-bold text-text">{planning ? planning.summary.uncoveredItemCount : project.cockpit.risk.shortageItemCount} shortage items</div><div className="mt-1 text-xs text-muted">{planning ? `Build ${planning.summary.totalBuildQuantity} - Buy ${planning.summary.totalPurchaseQuantity}` : `Uncovered qty ${project.cockpit.risk.totalUncoveredQuantity}`}</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Execution</p><div className="mt-2 text-base font-bold text-text">{project.rollups.activeWorkOrderCount} active work orders</div><div className="mt-1 text-xs text-muted">{nextWorkOrder ? `${nextWorkOrder.workOrderNumber} due ${nextWorkOrder.dueDate ? new Date(nextWorkOrder.dueDate).toLocaleDateString() : "unscheduled"}` : "No active work order due date"}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Execution</p><div className="mt-2 text-base font-bold text-text">{project.rollups.activeWorkOrderCount} active work orders</div><div className="mt-1 text-xs text-muted">{nextWorkOrder ? `${nextWorkOrder.workOrderNumber} due ${nextWorkOrder.dueDate ? new Date(nextWorkOrder.dueDate).toLocaleDateString() : "unscheduled"}` : "No active work order due date"}</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Delivery</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.delivery.shipmentStatus ? project.cockpit.delivery.shipmentStatus.replaceAll("_", " ") : "Not linked"}</div><div className="mt-1 text-xs text-muted">{project.cockpit.delivery.shipmentNumber ? `${project.cockpit.delivery.shipmentNumber} - ${project.cockpit.delivery.packageCount} package(s)` : "Link a shipment to track delivery"}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Delivery</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.delivery.shipmentStatus ? project.cockpit.delivery.shipmentStatus.replaceAll("_", " ") : "Not linked"}</div><div className="mt-1 text-xs text-muted">{project.cockpit.delivery.shipmentNumber ? `${project.cockpit.delivery.shipmentNumber} - ${project.cockpit.delivery.packageCount} package(s)` : "Link a shipment to track delivery"}</div></article>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-3">
|
<div className="mt-3 grid gap-2.5 xl:grid-cols-3">
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Purchasing Coverage</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.purchasing.totalReceivedQuantity}/{project.cockpit.purchasing.totalOrderedQuantity} received</div><div className="mt-1 text-xs text-muted">{project.cockpit.purchasing.linkedPurchaseOrderCount} linked PO(s) - {project.cockpit.purchasing.totalOutstandingQuantity} outstanding</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Purchasing Coverage</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.purchasing.totalReceivedQuantity}/{project.cockpit.purchasing.totalOrderedQuantity} received</div><div className="mt-1 text-xs text-muted">{project.cockpit.purchasing.linkedPurchaseOrderCount} linked PO(s) - {project.cockpit.purchasing.totalOutstandingQuantity} outstanding</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Readiness Score</p><div className={`mt-2 text-base font-bold ${riskTone}`}>{readinessScore}%</div><div className="mt-1 text-xs text-muted">{project.cockpit.risk.riskLevel} risk - {project.cockpit.risk.shortageItemCount} shortage item(s)</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Readiness Score</p><div className={`mt-2 text-base font-bold ${riskTone}`}>{readinessScore}%</div><div className="mt-1 text-xs text-muted">{project.cockpit.risk.riskLevel} risk - {project.cockpit.risk.shortageItemCount} shortage item(s)</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Material Spend</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.purchasing.linkedLineValue.toFixed(2)}</div><div className="mt-1 text-xs text-muted">{project.cockpit.purchasing.vendorCount} vendor(s) across {project.cockpit.purchasing.linkedLineCount} linked line(s)</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Material Spend</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.purchasing.linkedLineValue.toFixed(2)}</div><div className="mt-1 text-xs text-muted">{project.cockpit.purchasing.vendorCount} vendor(s) across {project.cockpit.purchasing.linkedLineCount} linked line(s)</div></article>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-4">
|
<div className="mt-3 grid gap-2.5 xl:grid-cols-4">
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Booked Revenue</p><div className="mt-2 text-base font-bold text-text">{formatCurrency(project.cockpit.costs.bookedRevenue)}</div><div className="mt-1 text-xs text-muted">Quoted baseline {formatCurrency(project.cockpit.costs.quotedRevenue)}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Booked Revenue</p><div className="mt-2 text-base font-bold text-text">{formatCurrency(project.cockpit.costs.bookedRevenue)}</div><div className="mt-1 text-xs text-muted">Quoted baseline {formatCurrency(project.cockpit.costs.quotedRevenue)}</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Purchase Commitment</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.costs.linkedPurchaseCommitment.toFixed(2)}</div><div className="mt-1 text-xs text-muted">Linked PO line value already committed</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Purchase Commitment</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.costs.linkedPurchaseCommitment.toFixed(2)}</div><div className="mt-1 text-xs text-muted">Linked PO line value already committed</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Planned Material Cost</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.costs.plannedMaterialCost.toFixed(2)}</div><div className="mt-1 text-xs text-muted">Issued so far ${project.cockpit.costs.issuedMaterialCost.toFixed(2)}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Planned Material Cost</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.costs.plannedMaterialCost.toFixed(2)}</div><div className="mt-1 text-xs text-muted">Issued so far ${project.cockpit.costs.issuedMaterialCost.toFixed(2)}</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Load</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.costs.completedBuildQuantity}/{project.cockpit.costs.buildQuantity}</div><div className="mt-1 text-xs text-muted">{project.cockpit.costs.plannedOperationHours.toFixed(1)} planned operation hours</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Load</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.costs.completedBuildQuantity}/{project.cockpit.costs.buildQuantity}</div><div className="mt-1 text-xs text-muted">{project.cockpit.costs.plannedOperationHours.toFixed(1)} planned operation hours</div></article>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
<div className="mt-3 grid gap-2.5 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||||
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Next Checkpoints</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Next Checkpoints</p>
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
@@ -227,17 +225,16 @@ export function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]">
|
<section className="grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Actionable Cockpit</p>
|
<p className="section-kicker">ACTIONABLE COCKPIT</p>
|
||||||
<p className="mt-2 text-sm text-muted">Turn current exceptions into purchasing, manufacturing, and planning follow-through.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Link to="/planning/workbench" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to="/planning/workbench" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
Open workbench
|
Open workbench
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-2">
|
<div className="mt-3 grid gap-2.5 xl:grid-cols-2">
|
||||||
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Follow-Through</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Follow-Through</p>
|
||||||
<div className="mt-2 text-base font-bold text-text">{topBuildRecommendation ? topBuildRecommendation.itemSku : "No build recommendation"}</div>
|
<div className="mt-2 text-base font-bold text-text">{topBuildRecommendation ? topBuildRecommendation.itemSku : "No build recommendation"}</div>
|
||||||
@@ -269,7 +266,7 @@ export function ProjectDetailPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-wrap gap-3">
|
<div className="mt-3 flex flex-wrap gap-3">
|
||||||
<Link to={`/manufacturing/work-orders/new?projectId=${project.id}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to={`/manufacturing/work-orders/new?projectId=${project.id}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
New project work order
|
New project work order
|
||||||
</Link>
|
</Link>
|
||||||
@@ -283,9 +280,9 @@ export function ProjectDetailPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3"><div><p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Linked Purchasing</p><p className="mt-2 text-sm text-muted">Purchase orders and receipts tied back to the project sales order.</p></div>{project.salesOrderId ? <Link to="/purchasing/orders" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open purchasing</Link> : null}</div>
|
<div className="flex items-center justify-between gap-3"><div><p className="section-kicker">LINKED PURCHASING</p></div>{project.salesOrderId ? <Link to="/purchasing/orders" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Open purchasing</Link> : null}</div>
|
||||||
{project.cockpit.purchasing.purchaseOrders.length === 0 ? <div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No linked purchase orders are tied to this project yet.</div> : <div className="mt-6 space-y-3">{project.cockpit.purchasing.purchaseOrders.slice(0, 5).map((purchaseOrder) => (<Link key={purchaseOrder.id} to={`/purchasing/orders/${purchaseOrder.id}`} className="block rounded-[18px] border border-line/70 bg-page/60 p-3 transition hover:bg-page/80"><div className="flex flex-wrap items-start justify-between gap-3"><div><div className="font-semibold text-text">{purchaseOrder.documentNumber}</div><div className="mt-1 text-xs text-muted">{purchaseOrder.vendorName} - {purchaseOrder.status.replaceAll("_", " ")}</div></div><div className="text-right text-xs text-muted"><div>${purchaseOrder.linkedLineValue.toFixed(2)} linked value</div><div>{purchaseOrder.totalReceivedQuantity}/{purchaseOrder.totalOrderedQuantity} received</div></div></div></Link>))}</div>}
|
{project.cockpit.purchasing.purchaseOrders.length === 0 ? <div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No linked purchase orders are tied to this project yet.</div> : <div className="mt-4 space-y-2.5">{project.cockpit.purchasing.purchaseOrders.slice(0, 5).map((purchaseOrder) => (<Link key={purchaseOrder.id} to={`/purchasing/orders/${purchaseOrder.id}`} className="block rounded-[16px] border border-line/70 bg-page/60 px-3 py-2.5 transition hover:bg-page/80"><div className="flex flex-wrap items-start justify-between gap-3"><div><div className="font-semibold text-text">{purchaseOrder.documentNumber}</div><div className="mt-1 text-xs text-muted">{purchaseOrder.vendorName} - {purchaseOrder.status.replaceAll("_", " ")}</div></div><div className="text-right text-xs text-muted"><div>${purchaseOrder.linkedLineValue.toFixed(2)} linked value</div><div>{purchaseOrder.totalReceivedQuantity}/{purchaseOrder.totalOrderedQuantity} received</div></div></div></Link>))}</div>}
|
||||||
</article>
|
</article>
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Readiness Drivers</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Readiness Drivers</p>
|
||||||
|
|||||||
@@ -235,20 +235,19 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="page-stack" onSubmit={handleSubmit}>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Projects Editor</p>
|
<p className="section-kicker">PROJECTS EDITOR</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Project" : "Edit Project"}</h3>
|
<h3 className="module-title">{mode === "create" ? "New Project" : "Edit Project"}</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">Create a customer-linked program record that can anchor commercial documents, delivery work, and project files.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Link to={mode === "create" ? "/projects" : `/projects/${projectId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to={mode === "create" ? "/projects" : `/projects/${projectId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
Cancel
|
Cancel
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="space-y-3 surface-panel">
|
||||||
<div className="grid gap-3 xl:grid-cols-2">
|
<div className="grid gap-3 xl:grid-cols-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Project name</span>
|
<span className="mb-2 block text-sm font-semibold text-text">Project name</span>
|
||||||
|
|||||||
@@ -42,13 +42,12 @@ export function ProjectListPage() {
|
|||||||
}, [priorityFilter, query, statusFilter, token]);
|
}, [priorityFilter, query, statusFilter, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="page-stack">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Projects</p>
|
<p className="section-kicker">PROJECTS</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">Program records</h3>
|
<h3 className="module-title">PROGRAM RECORDS</h3>
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">Track long-running customer programs across commercial commitments, shipment deliverables, ownership, and due dates.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link to="/projects/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
|
<Link to="/projects/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
|
||||||
@@ -57,8 +56,8 @@ export function ProjectListPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.2fr)_0.45fr_0.45fr]">
|
<div className="grid gap-2.5 xl:grid-cols-[minmax(0,1.2fr)_0.45fr_0.45fr]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
|
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
|
||||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Project number, name, customer" className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Project number, name, customer" className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||||
@@ -76,11 +75,11 @@ export function ProjectListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-3 rounded-[16px] border border-line/70 bg-page/70 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
<div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No projects are available for the current filters.</div>
|
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-4 py-7 text-center text-sm text-muted">No projects are available for the current filters.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-5 overflow-hidden rounded-2xl border border-line/70">
|
<div className="mt-4 overflow-hidden rounded-[16px] border border-line/70">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead className="bg-page/80 text-left text-muted">
|
<thead className="bg-page/80 text-left text-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -292,11 +292,11 @@ export function PurchaseDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchase Order</p>
|
<p className="section-kicker">PURCHASE ORDER</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{activeDocument.documentNumber}</h3>
|
<h3 className="module-title">{activeDocument.documentNumber}</h3>
|
||||||
<p className="mt-1 text-sm text-text">{activeDocument.vendorName}</p>
|
<p className="mt-1 text-sm text-text">{activeDocument.vendorName}</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<PurchaseStatusBadge status={activeDocument.status} />
|
<PurchaseStatusBadge status={activeDocument.status} />
|
||||||
@@ -326,11 +326,10 @@ export function PurchaseDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
|
<p className="section-kicker">QUICK ACTIONS</p>
|
||||||
<p className="mt-2 text-sm text-muted">Update purchase-order status without opening the full editor.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{purchaseStatusOptions.map((option) => (
|
{purchaseStatusOptions.map((option) => (
|
||||||
@@ -356,11 +355,10 @@ export function PurchaseDetailPage() {
|
|||||||
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Payment Terms</p><div className="mt-2 text-base font-bold text-text">{activeDocument.paymentTerms || "N/A"}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Payment Terms</p><div className="mt-2 text-base font-bold text-text">{activeDocument.paymentTerms || "N/A"}</div></article>
|
||||||
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Currency</p><div className="mt-2 text-base font-bold text-text">{activeDocument.currencyCode || "USD"}</div></article>
|
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Currency</p><div className="mt-2 text-base font-bold text-text">{activeDocument.currencyCode || "USD"}</div></article>
|
||||||
</section>
|
</section>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Revision History</p>
|
<p className="section-kicker">REVISION HISTORY</p>
|
||||||
<p className="mt-2 text-sm text-muted">Automatic snapshots are recorded when the purchase order changes or receipts are posted.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{activeDocument.revisions.length === 0 ? (
|
{activeDocument.revisions.length === 0 ? (
|
||||||
@@ -499,10 +497,8 @@ export function PurchaseDetailPage() {
|
|||||||
</section>
|
</section>
|
||||||
<section className="grid gap-3 2xl:grid-cols-[minmax(360px,0.82fr)_minmax(0,1.18fr)]">
|
<section className="grid gap-3 2xl:grid-cols-[minmax(360px,0.82fr)_minmax(0,1.18fr)]">
|
||||||
{canReceive ? (
|
{canReceive ? (
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchase Receiving</p>
|
<p className="section-kicker">PURCHASE RECEIVING</p>
|
||||||
<h4 className="mt-2 text-lg font-bold text-text">Receive material</h4>
|
|
||||||
<p className="mt-2 text-sm text-muted">Post received quantities to inventory and retain a receipt record against this order.</p>
|
|
||||||
{openLines.length === 0 ? (
|
{openLines.length === 0 ? (
|
||||||
<div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
<div className="mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
||||||
All ordered quantities have been received for this purchase order.
|
All ordered quantities have been received for this purchase order.
|
||||||
@@ -594,9 +590,8 @@ export function PurchaseDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
) : null}
|
) : null}
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Receipt History</p>
|
<p className="section-kicker">RECEIPT HISTORY</p>
|
||||||
<h4 className="mt-2 text-lg font-bold text-text">Received material log</h4>
|
|
||||||
{activeDocument.receipts.length === 0 ? (
|
{activeDocument.receipts.length === 0 ? (
|
||||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
||||||
No purchase receipts have been recorded for this order yet.
|
No purchase receipts have been recorded for this order yet.
|
||||||
|
|||||||
@@ -264,19 +264,19 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="page-stack" onSubmit={handleSubmit}>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchasing Editor</p>
|
<p className="section-kicker">PURCHASING EDITOR</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Purchase Order" : "Edit Purchase Order"}</h3>
|
<h3 className="module-title">{mode === "create" ? "New Purchase Order" : "Edit Purchase Order"}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Link to={mode === "create" ? "/purchasing/orders" : `/purchasing/orders/${orderId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to={mode === "create" ? "/purchasing/orders" : `/purchasing/orders/${orderId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
Cancel
|
Cancel
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="space-y-3 surface-panel">
|
||||||
<div className="grid gap-3 xl:grid-cols-4">
|
<div className="grid gap-3 xl:grid-cols-4">
|
||||||
<label className="block xl:col-span-2">
|
<label className="block xl:col-span-2">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Vendor</span>
|
<span className="mb-2 block text-sm font-semibold text-text">Vendor</span>
|
||||||
|
|||||||
@@ -34,12 +34,11 @@ export function PurchaseListPage() {
|
|||||||
}, [searchTerm, statusFilter, token]);
|
}, [searchTerm, statusFilter, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchasing</p>
|
<p className="section-kicker">PURCHASING</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Purchase Orders</h3>
|
<h3 className="module-title">PURCHASE ORDERS</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">Vendor-facing procurement documents for material replenishment and bought-in components.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link to="/purchasing/orders/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
|
<Link to="/purchasing/orders/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
|
||||||
@@ -47,7 +46,7 @@ export function PurchaseListPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 grid gap-3 rounded-[18px] border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.35fr_0.8fr]">
|
<div className="mt-4 grid gap-2.5 rounded-[16px] border border-line/70 bg-page/60 p-2.5 xl:grid-cols-[1.35fr_0.8fr]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input value={searchTerm} onChange={(event) => setSearchTerm(event.target.value)} placeholder="Search purchase orders by document number or vendor" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
|
<input value={searchTerm} onChange={(event) => setSearchTerm(event.target.value)} placeholder="Search purchase orders by document number or vendor" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||||
@@ -63,11 +62,11 @@ export function PurchaseListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-4 rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
{documents.length === 0 ? (
|
{documents.length === 0 ? (
|
||||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No purchase orders have been added yet.</div>
|
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-4 py-7 text-center text-sm text-muted">No purchase orders have been added yet.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
|
<div className="mt-4 overflow-hidden rounded-[16px] border border-line/70">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead className="bg-page/80 text-left text-muted">
|
<thead className="bg-page/80 text-left text-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -335,11 +335,11 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{config.detailEyebrow}</p>
|
<p className="section-kicker">{config.detailEyebrow.toUpperCase()}</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{activeDocument.documentNumber}</h3>
|
<h3 className="module-title">{activeDocument.documentNumber}</h3>
|
||||||
<p className="mt-1 text-sm text-text">{activeDocument.customerName}</p>
|
<p className="mt-1 text-sm text-text">{activeDocument.customerName}</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<SalesStatusBadge status={activeDocument.status} />
|
<SalesStatusBadge status={activeDocument.status} />
|
||||||
@@ -396,11 +396,10 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
|
<p className="section-kicker">QUICK ACTIONS</p>
|
||||||
<p className="mt-2 text-sm text-muted">Update document status without opening the full editor.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{salesStatusOptions.map((option) => (
|
{salesStatusOptions.map((option) => (
|
||||||
@@ -457,11 +456,10 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
<div className="mt-2 text-base font-bold text-text">${activeDocument.total.toFixed(2)}</div>
|
<div className="mt-2 text-base font-bold text-text">${activeDocument.total.toFixed(2)}</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Revision History</p>
|
<p className="section-kicker">REVISION HISTORY</p>
|
||||||
<p className="mt-2 text-sm text-muted">Automatic snapshots are recorded when the document changes status, content, or approval state.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{activeDocument.revisions.length === 0 ? (
|
{activeDocument.revisions.length === 0 ? (
|
||||||
@@ -587,14 +585,10 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{entity === "order" && planning ? (
|
{entity === "order" && planning ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Demand Planning</p>
|
<p className="section-kicker">DEMAND PLANNING</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Net build and buy requirements</h3>
|
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
||||||
Sales-order demand is netted against available stock, active reservations, open work orders, and open purchase orders before new build or buy quantities are recommended.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs text-muted">
|
<div className="text-right text-xs text-muted">
|
||||||
<div>Generated {new Date(planning.generatedAt).toLocaleString()}</div>
|
<div>Generated {new Date(planning.generatedAt).toLocaleString()}</div>
|
||||||
@@ -706,11 +700,10 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
{entity === "order" && canReadShipping ? (
|
{entity === "order" && canReadShipping ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping</p>
|
<p className="section-kicker">SHIPPING</p>
|
||||||
<p className="mt-2 text-sm text-muted">Shipment records currently tied to this sales order.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManageShipping ? (
|
{canManageShipping ? (
|
||||||
<Link to={`/shipping/shipments/new?orderId=${activeDocument.id}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to={`/shipping/shipments/new?orderId=${activeDocument.id}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
|
|||||||
@@ -168,19 +168,19 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="page-stack" onSubmit={handleSubmit}>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{config.detailEyebrow} Editor</p>
|
<p className="section-kicker">{`${config.detailEyebrow} EDITOR`.toUpperCase()}</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}</h3>
|
<h3 className="module-title">{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Link to={mode === "create" ? config.routeBase : `${config.routeBase}/${documentId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to={mode === "create" ? config.routeBase : `${config.routeBase}/${documentId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
Cancel
|
Cancel
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="space-y-3 surface-panel">
|
||||||
<div className="grid gap-3 xl:grid-cols-4">
|
<div className="grid gap-3 xl:grid-cols-4">
|
||||||
<label className="block xl:col-span-2">
|
<label className="block xl:col-span-2">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Customer</span>
|
<span className="mb-2 block text-sm font-semibold text-text">Customer</span>
|
||||||
|
|||||||
@@ -40,14 +40,11 @@ export function SalesListPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
}, [config.collectionLabel, entity, searchTerm, statusFilter, token]);
|
}, [config.collectionLabel, entity, searchTerm, statusFilter, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{config.listEyebrow}</p>
|
<p className="section-kicker">{config.listEyebrow.toUpperCase()}</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">{config.collectionLabel}</h3>
|
<h3 className="module-title">{config.collectionLabel}</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">
|
|
||||||
Customer-facing commercial documents for pricing, commitment, and downstream fulfillment planning.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link to={`${config.routeBase}/new`} className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
|
<Link to={`${config.routeBase}/new`} className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
|
||||||
@@ -55,7 +52,7 @@ export function SalesListPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 grid gap-3 rounded-[18px] border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.35fr_0.8fr]">
|
<div className="mt-4 grid gap-2.5 rounded-[16px] border border-line/70 bg-page/60 p-2.5 xl:grid-cols-[1.35fr_0.8fr]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input
|
<input
|
||||||
@@ -80,13 +77,13 @@ export function SalesListPage({ entity }: { entity: SalesDocumentEntity }) {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-4 rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
{documents.length === 0 ? (
|
{documents.length === 0 ? (
|
||||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-4 py-7 text-center text-sm text-muted">
|
||||||
No {config.collectionLabel.toLowerCase()} have been added yet.
|
No {config.collectionLabel.toLowerCase()} have been added yet.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
|
<div className="mt-4 overflow-hidden rounded-[16px] border border-line/70">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead className="bg-page/80 text-left text-muted">
|
<thead className="bg-page/80 text-left text-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -157,14 +157,11 @@ export function AdminDiagnosticsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Admin Diagnostics</p>
|
<p className="section-kicker">ADMIN DIAGNOSTICS</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Operational runtime and audit visibility</h3>
|
<h3 className="module-title">RUNTIME AUDIT SUPPORT</h3>
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
||||||
This view surfaces environment footprint, record counts, and recent change activity so admin review does not require direct database access.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -199,14 +196,10 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Backup And Restore</p>
|
<p className="section-kicker">BACKUP AND RESTORE</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Operational backup workflow</h3>
|
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
||||||
Use these paths and steps as the support baseline for manual backup and restore procedures.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-muted">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-muted">
|
||||||
<div>Data: {backupGuidance.dataPath}</div>
|
<div>Data: {backupGuidance.dataPath}</div>
|
||||||
@@ -266,11 +259,10 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Startup Validation</p>
|
<p className="section-kicker">STARTUP VALIDATION</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Boot-time readiness checks</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<span className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${startupStatusTone}`}>
|
<span className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${startupStatusTone}`}>
|
||||||
{diagnostics.startup.status}
|
{diagnostics.startup.status}
|
||||||
@@ -297,8 +289,8 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">System Footprint</p>
|
<p className="section-kicker">SYSTEM FOOTPRINT</p>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-2">
|
<div className="mt-5 grid gap-3 xl:grid-cols-2">
|
||||||
{footprintCards.map(([label, value]) => (
|
{footprintCards.map(([label, value]) => (
|
||||||
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
@@ -309,11 +301,10 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Support Logs</p>
|
<p className="section-kicker">SUPPORT LOGS</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Recent runtime warnings and failures</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
{supportLogSummary ? `${supportLogSummary.filteredCount} of ${supportLogSummary.totalCount} entries` : "No entries loaded"}
|
{supportLogSummary ? `${supportLogSummary.filteredCount} of ${supportLogSummary.totalCount} entries` : "No entries loaded"}
|
||||||
@@ -404,11 +395,10 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Recent Audit Trail</p>
|
<p className="section-kicker">RECENT AUDIT TRAIL</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Latest cross-module write activity</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted">{status}</p>
|
<p className="text-sm text-muted">{status}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -273,14 +273,11 @@ export function UserManagementPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">User Management</p>
|
<p className="section-kicker">ADMIN</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Accounts, roles, and permission assignment</h3>
|
<h3 className="module-title">USERS ROLES SESSIONS</h3>
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
||||||
Manage user accounts and the role-permission model from one admin surface so onboarding and access control stay tied together.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Link to="/settings/company" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
<Link to="/settings/company" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||||
@@ -294,11 +291,10 @@ export function UserManagementPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-6 xl:grid-cols-2">
|
<section className="grid gap-6 xl:grid-cols-2">
|
||||||
<form className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5" onSubmit={handleUserSave}>
|
<form className="surface-panel backdrop-blur" onSubmit={handleUserSave}>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Users</p>
|
<p className="section-kicker">USERS</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Account generation and role assignment</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
value={selectedUserId}
|
value={selectedUserId}
|
||||||
@@ -387,11 +383,10 @@ export function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5" onSubmit={handleRoleSave}>
|
<form className="surface-panel backdrop-blur" onSubmit={handleRoleSave}>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Roles</p>
|
<p className="section-kicker">ROLES</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Permission assignment administration</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
value={selectedRoleId}
|
value={selectedRoleId}
|
||||||
@@ -465,14 +460,10 @@ export function UserManagementPage() {
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Sessions</p>
|
<p className="section-kicker">SESSIONS</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Active sign-ins and revocation control</h3>
|
|
||||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
||||||
Review recent authenticated sessions, see their current state, and revoke stale or risky access without changing the user record.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
|
|||||||
@@ -218,11 +218,11 @@ export function ShipmentDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<div className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipment</p>
|
<p className="section-kicker">SHIPMENT</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{shipment.shipmentNumber}</h3>
|
<h3 className="module-title">{shipment.shipmentNumber}</h3>
|
||||||
<p className="mt-1 text-sm text-text">{shipment.salesOrderNumber} / {shipment.customerName}</p>
|
<p className="mt-1 text-sm text-text">{shipment.salesOrderNumber} / {shipment.customerName}</p>
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-3">
|
<div className="mt-3 flex flex-wrap items-center gap-3">
|
||||||
<ShipmentStatusBadge status={shipment.status} />
|
<ShipmentStatusBadge status={shipment.status} />
|
||||||
@@ -249,11 +249,10 @@ export function ShipmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
|
<p className="section-kicker">QUICK ACTIONS</p>
|
||||||
<p className="mt-2 text-sm text-muted">Use inventory-backed picking before marking the shipment packed or shipped.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{shipmentStatusOptions.map((option) => (
|
{shipmentStatusOptions.map((option) => (
|
||||||
@@ -286,11 +285,10 @@ export function ShipmentDetailPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.3fr)_minmax(340px,0.9fr)]">
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.3fr)_minmax(340px,0.9fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipment Lines</p>
|
<p className="section-kicker">SHIPMENT LINES</p>
|
||||||
<p className="mt-2 text-sm text-muted">Track ordered, picked, and remaining quantity before shipment closeout.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 overflow-x-auto">
|
<div className="mt-5 overflow-x-auto">
|
||||||
@@ -350,13 +348,10 @@ export function ShipmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Pick And Issue From Stock</p>
|
<p className="section-kicker">PICK AND ISSUE FROM STOCK</p>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">
|
|
||||||
Posting a pick immediately creates an inventory issue transaction against the selected warehouse location and advances draft shipments into picking.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-xs text-muted">
|
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-xs text-muted">
|
||||||
Select the sales-order line, source location, and quantity you are physically picking.
|
Select the sales-order line, source location, and quantity you are physically picking.
|
||||||
@@ -468,11 +463,10 @@ export function ShipmentDetailPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(320px,0.9fr)]">
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(320px,0.9fr)]">
|
||||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Pick History</p>
|
<p className="section-kicker">PICK HISTORY</p>
|
||||||
<p className="mt-2 text-sm text-muted">Every pick here already issued stock from a specific inventory location.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shipment.picks.length === 0 ? (
|
{shipment.picks.length === 0 ? (
|
||||||
@@ -508,11 +502,10 @@ export function ShipmentDetailPage() {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Related Shipments</p>
|
<p className="section-kicker">RELATED SHIPMENTS</p>
|
||||||
<p className="mt-2 text-sm text-muted">Other shipments already tied to this sales order.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link to={`/shipping/shipments/new?orderId=${shipment.salesOrderId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Add another shipment</Link>
|
<Link to={`/shipping/shipments/new?orderId=${shipment.salesOrderId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Add another shipment</Link>
|
||||||
|
|||||||
@@ -86,19 +86,19 @@ export function ShipmentFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="page-stack" onSubmit={handleSubmit}>
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping Editor</p>
|
<p className="section-kicker">SHIPPING EDITOR</p>
|
||||||
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Shipment" : "Edit Shipment"}</h3>
|
<h3 className="module-title">{mode === "create" ? "New Shipment" : "Edit Shipment"}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Link to={mode === "create" ? "/shipping/shipments" : `/shipping/shipments/${shipmentId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
<Link to={mode === "create" ? "/shipping/shipments" : `/shipping/shipments/${shipmentId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
||||||
Cancel
|
Cancel
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<section className="space-y-3 surface-panel">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Sales Order</span>
|
<span className="mb-2 block text-sm font-semibold text-text">Sales Order</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -38,12 +38,11 @@ export function ShipmentListPage() {
|
|||||||
}, [searchTerm, statusFilter, token]);
|
}, [searchTerm, statusFilter, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel">
|
<section className="surface-panel">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping</p>
|
<p className="section-kicker">SHIPPING</p>
|
||||||
<h3 className="mt-2 text-lg font-bold text-text">Shipments</h3>
|
<h3 className="module-title">SHIPMENTS</h3>
|
||||||
<p className="mt-2 max-w-2xl text-sm text-muted">Outbound shipment records tied to sales orders, carriers, and tracking details.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<Link to="/shipping/shipments/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
|
<Link to="/shipping/shipments/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
|
||||||
@@ -51,7 +50,7 @@ export function ShipmentListPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 grid gap-3 rounded-[18px] border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.35fr_0.8fr]">
|
<div className="mt-4 grid gap-2.5 rounded-[16px] border border-line/70 bg-page/60 p-2.5 xl:grid-cols-[1.35fr_0.8fr]">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input
|
<input
|
||||||
@@ -76,11 +75,11 @@ export function ShipmentListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
|
<div className="mt-4 rounded-[16px] border border-line/70 bg-page/60 px-3 py-2 text-sm text-muted">{status}</div>
|
||||||
{shipments.length === 0 ? (
|
{shipments.length === 0 ? (
|
||||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">No shipments have been added yet.</div>
|
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-page/60 px-4 py-7 text-center text-sm text-muted">No shipments have been added yet.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
|
<div className="mt-4 overflow-hidden rounded-[16px] border border-line/70">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead className="bg-page/80 text-left text-muted">
|
<thead className="bg-page/80 text-left text-muted">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user