Compare commits

..

26 Commits

Author SHA1 Message Date
e65ed892f1 shipping label fix - codex 2026-03-19 21:47:26 -05:00
jason
ce2d52db53 hopeful 2026-03-19 16:38:41 -05:00
jason
39fd876d51 fixing 2026-03-19 16:34:18 -05:00
jason
0c3b2cf6fe fixes 2026-03-19 16:19:48 -05:00
jason
6423dfb91b again 2026-03-19 16:14:35 -05:00
jason
26b188de87 fix 2026-03-19 16:12:10 -05:00
jason
0b43b4ebf5 shipping fix 2026-03-19 16:06:50 -05:00
jason
3c312733ca shipping label 2 2026-03-19 16:01:32 -05:00
jason
9d54dc2ecd shipping label 2026-03-19 15:57:39 -05:00
jason
b762c70238 clean up usage guide 2026-03-19 15:37:51 -05:00
jason
9562c1cc9c usage guide 2026-03-19 13:09:29 -05:00
3eba7c5fa6 workbench 2026-03-19 07:41:06 -05:00
4949b6033f more workbench usability 2026-03-19 07:38:08 -05:00
cf54e4ba58 usability workbench 2026-03-18 23:48:14 -05:00
061057339b more 2026-03-18 23:42:30 -05:00
7b65fe06cf more workbench 2026-03-18 23:32:12 -05:00
d22e715f00 workbench 2026-03-18 23:28:27 -05:00
5fdd366bc3 last cleanup 2026-03-18 23:22:11 -05:00
afad00bf46 1 2026-03-18 23:17:44 -05:00
28ea1ee6b9 cleanup 2026-03-18 23:14:47 -05:00
00a4da346f cleanup 2026-03-18 23:10:28 -05:00
52bc98c16e cleanup 2026-03-18 23:06:44 -05:00
17b73a4597 cleanup 2026-03-18 22:51:17 -05:00
dc07bfc8e0 cleanup 2026-03-18 22:44:01 -05:00
1e408d5316 density 2026-03-18 20:36:30 -05:00
69dfec98ad fixes 2026-03-18 12:05:28 -05:00
58 changed files with 2401 additions and 1050 deletions

View File

@@ -20,7 +20,33 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
- Workbench station-to-station rebalance so planners can move an operation onto another active work center and rebuild the downstream chain from the same dispatch surface
- Workbench drag scheduling in station grouping mode, with draggable operation cards, station drop targets, heatmap-day-aware drop timing, and projected post-drop load cues before the move is committed
- Workbench station cards now show planned-vs-actual load so planners can compare schedule intent against recorded execution time
- 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 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
- Continued density standardization across CRM detail internals and inventory item editing so secondary cards, timeline/history panels, thumbnail panels, BOM/routing editors, and empty states use the tighter shared surface treatment with less filler copy
- Continued density standardization across inventory detail transaction/transfer/reservation surfaces, and fixed item-editor navigation controls so SKU master and cancel actions navigate reliably from the create-item form
- Continued density standardization across sales, purchasing, shipping, and manufacturing editor internals, and standardized form-header cancel actions onto button-driven navigation to avoid in-form route-transition edge cases
- Continued density standardization across sales, purchasing, shipping, and manufacturing detail internals, including denser KPI strips, tighter side panels, shorter empty states, and less redundant context copy on high-traffic record views
- Continued density standardization across shared attachment and revision-comparison surfaces, and changed inventory item-editor exit actions to hard navigation so SKU master and cancel transitions no longer depend on client-side router state
- Continued density standardization across the SKU master builder and planning workbench, including tighter tree and board panels, denser exception and focus surfaces, shorter empty states, and less helper copy on those operational screens
- Continued density standardization across warehouse list/detail/editor screens and the manufacturing station surface, including tighter status blocks, denser location/station cards, and removal of older roomy header patterns
- Continued density standardization across company settings and deeper manufacturing detail surfaces, including tighter admin/profile/theme sections, denser work-order execution panels, and compact issue/completion history cards
- Continued density standardization across project cockpit/detail internals, including tighter cockpit cards, denser purchasing and readiness panels, and compact milestone, manufacturing-link, and activity-timeline surfaces
- Continued density standardization across admin diagnostics, user management, and CRM contacts, including tighter filter/forms, denser summary cards, and compact contact/account management surfaces
- Workbench usability pass with sticky planner controls, stronger selected-row and selected-day state, clearer heatmap/day context, and more explicit dispatch-oriented action affordances
- Workbench usability depth with keyboard row navigation, enter-to-open behavior, escape-to-clear, and inline readiness/shortage/hold signal pills across planner rows and day-detail cards
- Workbench dispatch workflow depth with saved planner views, a release queue for visible ready work, queued-record visibility in the sticky control bar, and batch release directly from the workbench
- Workbench batch operation rebalance with multi-operation selection, sticky-bar batch reschedule controls, station reassignment across selected operations, and selected-operation visibility in row signals and focus context
- Workbench conflict-intelligence pass with projected batch target load, overload warnings before batch station moves, and best-alternate-station suggestions inside the sticky rebalance controls
- Workbench date-aware slot guidance using station working-day calendars and queue settings to suggest the next workable batch landing dates directly from the sticky rebalance controls
- Planning timeline now includes station day-load rollups, and Workbench slot suggestions use that server-backed per-day capacity data instead of only summary-level utilization heuristics
- Workbench now surfaces day-level capacity directly in the planner, including hot-station day counts on heatmap cells, selected-day station load breakdowns, and per-station hot-day chips in station grouping mode
- Workbench exception prioritization now scores and ranks projects, work orders, agenda rows, and dispatch exceptions by lateness, blockage, shortage, readiness, and overload pressure, with inline priority chips for faster triage
- Workbench now surfaces top-priority action lanes for `DO NOW`, `UNBLOCK`, and `RELEASE READY` records so planners can jump straight into ranked dispatch queues before working deeper lists
- Workbench action lanes now support direct follow-through from the lane cards themselves, including queue-release and the first inline build/buy/open actions without requiring a second step into the focus drawer
- 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
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
@@ -77,6 +103,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
### Changed
- Shipping-label PDFs now render inside an explicit single-page 4x6 canvas with tighter print-safe spacing and overflow-safe text wrapping to prevent second-sheet runover on label printers
- Project records now persist milestone plans directly on create/edit instead of treating schedule checkpoints as freeform notes only
- Company theme colors and font now persist correctly across refresh through startup brand-profile hydration in the frontend theme provider
- Demand-planning purchase-order draft generation now links sales-order lines only when the purchase item matches the originating sales item

View File

@@ -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.
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.
9. Treat the landing page as `Dashboard`: a metric-oriented, modular command surface that should accumulate reusable operational panels over time.
10. Purchase-order item selection must be restricted to inventory items where `isPurchasable = true`.
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.
12. Keep `Projects`, `Manufacturing`, and `Planning` distinct: projects are long-running program records, manufacturing is execution, and planning is scheduling/visibility.
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.
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. When designing operational pages, bias toward information density: tighter panel padding, smaller stack gaps, and fewer explanatory filler blocks.
11. Treat the landing page as `Dashboard`: a metric-oriented, modular command surface that should accumulate reusable operational panels over time.
12. Purchase-order item selection must be restricted to inventory items where `isPurchasable = true`.
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

View File

@@ -27,8 +27,8 @@ Current foundation scope includes:
- branded quote, sales-order, and purchase-order PDFs through the shared backend document pipeline
- purchase-order supporting documents for vendor invoices, acknowledgements, certifications, and backup files
- shipping shipments linked to sales orders with inventory-backed picking, stock issue posting, packing slips, shipping labels, bills of lading, and logistics attachments
- projects with customer/commercial/shipment linkage, owners, due dates, milestones, rollups, notes, attachments, reverse-linked quote/sales-order visibility, and downstream project-context carry-through into generated work orders and purchase orders
- manufacturing work orders with project linkage, station-based operation templates, editable station calendars/capacity settings, calendar-aware operation scheduling, operation execution controls, operator assignment, timer-based and manual labor posting, material issue posting, completion posting, operation rescheduling, and work-order attachments
- projects with customer/commercial/shipment linkage, owners, due dates, milestones, rollups, inline milestone quick-status actions, notes, attachments, reverse-linked quote/sales-order visibility, and downstream project-context carry-through into generated work orders and purchase orders
- manufacturing work orders with project linkage, station-based operation templates, editable station calendars/capacity settings, calendar-aware operation scheduling, operation execution controls, operator assignment, timer-based and manual labor posting, required hold reasons for `On Hold` status changes, material issue posting, completion posting, operation rescheduling, and work-order attachments
- planning workbench with live project/manufacturing schedule data, exception rail, heatmap load view, agenda view, focus drawer, station load grouping, readiness filters, overload visibility, inline dispatch actions, planner-side operation rebalance controls including station-to-station moves, and station-lane drag scheduling
- sales-order demand planning with multi-level BOM explosion, stock/open-supply netting, and build/buy recommendations
- planner-assisted conversion of demand-planning recommendations into prefilled work-order and purchase-order drafts
@@ -89,6 +89,8 @@ Navigation direction:
- 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
- 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

View File

@@ -112,6 +112,7 @@ This file tracks work that still needs to be completed. Shipped phase history an
### Planning and scheduling
- Standardize dense UI primitives and shared page shells so future Workbench, dashboard, and operational screens reuse the same cards, filter bars, empty states, and section wrappers instead of reintroducing ad hoc layout patterns
- Task dependencies, milestones, and progress updates
- Manufacturing calendar views and deeper bottleneck visibility beyond the shipped station load and overload workbench summaries
- Labor and machine scheduling support beyond the shipped station calendar/capacity foundation

View File

@@ -36,10 +36,10 @@ This file tracks roadmap phases, slices, and major foundations that have already
- Logistics attachments directly on shipment records
- Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage
- Reverse project linkage visibility on quote and sales-order detail pages, plus project-context carry-through into generated work orders and purchase orders with sales-order-driven backfill for existing records
- Project milestones and project-side milestone/work-order rollups
- Project milestones, inline milestone quick-status actions, and project-side milestone/work-order rollups
- Project cockpit section on detail pages for commercial, supply, execution, delivery, purchasing, readiness-risk, and cost-snapshot visibility, with direct launch paths into prefilled project work orders and demand-linked purchase orders plus a project activity timeline
- Project list/detail/create/edit workflows and dashboard program widgets
- Manufacturing foundation with work orders, project linkage, operation execution controls, operator assignment, timer-based and manual labor posting, material issue posting, completion posting, and work-order attachments
- Manufacturing foundation with work orders, project linkage, operation execution controls, operator assignment, timer-based and manual labor posting, required hold reasons for `On Hold` status changes, material issue posting, completion posting, and work-order attachments
- Manufacturing stations, item routing templates, editable station calendars/capacity settings, automatic work-order operation planning, and operation-level rescheduling for the workbench schedule
- Vendor invoice/supporting-document attachments directly on purchase orders
- Vendor-detail purchasing visibility with recent purchase-order activity

View File

@@ -200,19 +200,19 @@ export function AppShell() {
const { user, logout } = useAuth();
return (
<div className="min-h-screen px-4 py-5 xl:px-6 2xl:px-8">
<div className="mx-auto flex w-full max-w-[1760px] gap-3 2xl:gap-4">
<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">
<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-2.5 2xl:gap-3">
<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>
<h1 className="text-xl font-extrabold uppercase tracking-[0.24em] text-text">CODEXIUM</h1>
</div>
<nav className="mt-6 space-y-2">
<nav className="mt-4 space-y-1.5">
{links.map((link) => (
<NavLink
key={link.to}
to={link.to}
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"
}`
}
@@ -222,12 +222,12 @@ export function AppShell() {
</NavLink>
))}
</nav>
<div className="mt-auto space-y-3">
<div className="rounded-[18px] border border-line/70 bg-page/70 p-3">
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted">Theme</p>
<div className="mt-auto space-y-2.5">
<div className="rounded-[16px] border border-line/70 bg-page/70 p-2.5">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">Theme</p>
<ThemeToggle />
</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-xs text-muted">{user?.email}</p>
<button
@@ -235,7 +235,7 @@ export function AppShell() {
onClick={() => {
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
</button>
@@ -243,13 +243,13 @@ export function AppShell() {
</div>
</aside>
<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) => (
<NavLink
key={link.to}
to={link.to}
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"
}`
}
@@ -259,7 +259,7 @@ export function AppShell() {
</NavLink>
))}
</nav>
<div className="mb-4 md:hidden">
<div className="mb-3 md:hidden">
<ThemeToggle />
</div>
<Outlet />

View File

@@ -11,6 +11,12 @@ interface ConfirmActionDialogProps {
intent?: "danger" | "primary";
confirmationLabel?: string;
confirmationValue?: string;
extraFieldLabel?: string;
extraFieldPlaceholder?: string;
extraFieldValue?: string;
extraFieldRequired?: boolean;
extraFieldMultiline?: boolean;
onExtraFieldChange?: (value: string) => void;
isConfirming?: boolean;
onConfirm: () => void | Promise<void>;
onClose: () => void;
@@ -27,6 +33,12 @@ export function ConfirmActionDialog({
intent = "danger",
confirmationLabel,
confirmationValue,
extraFieldLabel,
extraFieldPlaceholder,
extraFieldValue = "",
extraFieldRequired = false,
extraFieldMultiline = false,
onExtraFieldChange,
isConfirming = false,
onConfirm,
onClose,
@@ -44,7 +56,11 @@ export function ConfirmActionDialog({
}
const requiresTypedConfirmation = Boolean(confirmationLabel && confirmationValue);
const isConfirmDisabled = isConfirming || (requiresTypedConfirmation && typedValue.trim() !== confirmationValue);
const requiresExtraField = Boolean(extraFieldLabel);
const isConfirmDisabled =
isConfirming ||
(requiresTypedConfirmation && typedValue.trim() !== confirmationValue) ||
(requiresExtraField && extraFieldRequired && extraFieldValue.trim().length === 0);
const confirmButtonClass =
intent === "danger"
? "bg-red-600 text-white hover:bg-red-700"
@@ -81,6 +97,27 @@ export function ConfirmActionDialog({
/>
</label>
) : null}
{requiresExtraField ? (
<label className="mt-4 block">
<span className="mb-2 block text-sm font-semibold text-text">{extraFieldLabel}</span>
{extraFieldMultiline ? (
<textarea
value={extraFieldValue}
onChange={(event) => onExtraFieldChange?.(event.target.value)}
placeholder={extraFieldPlaceholder}
rows={4}
className="w-full rounded-[18px] border border-line/70 bg-page px-3 py-2 text-text outline-none"
/>
) : (
<input
value={extraFieldValue}
onChange={(event) => onExtraFieldChange?.(event.target.value)}
placeholder={extraFieldPlaceholder}
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
/>
)}
</label>
) : null}
<div className="mt-5 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
<button
type="button"

View File

@@ -100,7 +100,7 @@ function buildFieldChanges(left: ComparisonField[], right: ComparisonField[]): A
function ComparisonCard({ label, document }: { label: string; document: ComparisonDocument }) {
return (
<article className="rounded-[18px] border border-line/70 bg-page/60 p-4">
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
@@ -111,7 +111,7 @@ function ComparisonCard({ label, document }: { label: string; document: Comparis
{document.status}
</span>
</div>
<dl className="mt-4 grid gap-3 sm:grid-cols-2">
<dl className="mt-3 grid gap-2 sm:grid-cols-2">
{document.metaFields.map((field) => (
<div key={`${label}-${field.label}`}>
<dt className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{field.label}</dt>
@@ -119,15 +119,15 @@ function ComparisonCard({ label, document }: { label: string; document: Comparis
</div>
))}
</dl>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{document.totalFields.map((field) => (
<div key={`${label}-total-${field.label}`} className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div key={`${label}-total-${field.label}`} className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{field.label}</div>
<div className="mt-1 text-sm font-semibold text-text">{field.value}</div>
</div>
))}
</div>
<div className="mt-4">
<div className="mt-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</div>
<p className="mt-2 whitespace-pre-line text-sm leading-6 text-text">{document.notes || "No notes recorded."}</p>
</div>
@@ -164,11 +164,11 @@ export function DocumentRevisionComparison({
const totalChanges = buildFieldChanges(leftDocument.totalFields, rightDocument.totalFields);
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 xl:flex-row xl:items-start xl:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{title}</p>
<p className="mt-2 text-sm text-muted">{description}</p>
<p className="section-kicker">{title}</p>
<p className="mt-1 text-sm text-muted">{description}</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<label className="block min-w-[220px]">
@@ -202,19 +202,19 @@ export function DocumentRevisionComparison({
</label>
</div>
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-2">
<div className="mt-3 grid gap-3 xl:grid-cols-2">
<ComparisonCard label="Baseline" document={leftDocument} />
<ComparisonCard label="Compare To" document={rightDocument} />
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-2">
<article className="rounded-[18px] border border-line/70 bg-page/60 p-4">
<div className="mt-3 grid gap-3 xl:grid-cols-2">
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Field Changes</p>
{metaChanges.length === 0 && totalChanges.length === 0 ? (
<div className="mt-4 text-sm text-muted">No header or total changes between the selected revisions.</div>
<div className="mt-3 text-sm text-muted">No header or total changes.</div>
) : (
<div className="mt-4 space-y-3">
<div className="mt-3 space-y-2">
{[...metaChanges, ...totalChanges].map((change) => (
<div key={change.label} className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div key={change.label} className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{change.label}</div>
<div className="mt-2 text-sm text-text">
{change.leftValue} {"->"} {change.rightValue}
@@ -224,28 +224,28 @@ export function DocumentRevisionComparison({
</div>
)}
</article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-4">
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Line Changes</p>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="mt-3 grid gap-2 sm:grid-cols-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Added</div>
<div className="mt-1 text-base font-bold text-text">{diffRows.filter((row) => row.status === "ADDED").length}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Removed</div>
<div className="mt-1 text-base font-bold text-text">{diffRows.filter((row) => row.status === "REMOVED").length}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Changed</div>
<div className="mt-1 text-base font-bold text-text">{diffRows.filter((row) => row.status === "CHANGED").length}</div>
</div>
</div>
{diffRows.length === 0 ? (
<div className="mt-4 text-sm text-muted">No line-level changes between the selected revisions.</div>
<div className="mt-3 text-sm text-muted">No line-level changes.</div>
) : (
<div className="mt-4 space-y-3">
<div className="mt-3 space-y-2">
{diffRows.map((row) => (
<div key={row.key} className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div key={row.key} className="rounded-[16px] border border-line/70 bg-surface/80 px-2 py-2">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-sm font-semibold text-text">{row.right?.title ?? row.left?.title}</div>
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{row.status}</span>

View File

@@ -133,12 +133,12 @@ export function FileAttachmentsPanel({
}
return (
<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">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{eyebrow}</p>
<h4 className="mt-2 text-lg font-bold text-text">{title}</h4>
<p className="mt-2 text-sm text-muted">{description}</p>
<p className="section-kicker">{eyebrow}</p>
<h4 className="text-lg font-bold text-text">{title}</h4>
<p className="mt-1 text-sm text-muted">{description}</p>
</div>
{canWriteFiles ? (
<label className="inline-flex cursor-pointer items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
@@ -147,17 +147,17 @@ export function FileAttachmentsPanel({
</label>
) : null}
</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-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">{status}</div>
{!canReadFiles ? (
<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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
You do not have permission to view file attachments.
</div>
) : attachments.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
{emptyMessage}
</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{attachments.map((attachment) => (
<div
key={attachment.id}
@@ -166,7 +166,7 @@ export function FileAttachmentsPanel({
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-text">{attachment.originalName}</p>
<p className="mt-1 text-xs text-muted">
{attachment.mimeType} · {formatFileSize(attachment.sizeBytes)} · {new Date(attachment.createdAt).toLocaleString()}
{attachment.mimeType} - {formatFileSize(attachment.sizeBytes)} - {new Date(attachment.createdAt).toLocaleString()}
</p>
</div>
<div className="flex shrink-0 gap-3">

View File

@@ -4,6 +4,36 @@
@tailwind components;
@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;
}
.planner-sticky-bar {
@apply sticky top-3 z-20 rounded-[18px] border border-line/70 bg-surface/90 p-3 shadow-panel backdrop-blur;
}
}
:root {
color-scheme: light;
--font-family: "Manrope";

View File

@@ -75,6 +75,7 @@ import type {
WorkOrderOperationTimerInput,
WorkOrderMaterialIssueInput,
WorkOrderStatus,
WorkOrderStatusUpdateInput,
WorkOrderSummaryDto,
ManufacturingUserOptionDto,
} from "@mrp/shared";
@@ -83,6 +84,7 @@ import type {
ProjectDetailDto,
ProjectDocumentOptionDto,
ProjectInput,
ProjectMilestoneStatusUpdateInput,
ProjectOwnerOptionDto,
ProjectPriority,
ProjectShipmentOptionDto,
@@ -601,6 +603,13 @@ export const api = {
updateProject(token: string, projectId: string, payload: ProjectInput) {
return request<ProjectDetailDto>(`/api/v1/projects/${projectId}`, { method: "PUT", body: JSON.stringify(payload) }, token);
},
updateProjectMilestoneStatus(token: string, projectId: string, milestoneId: string, payload: ProjectMilestoneStatusUpdateInput) {
return request<ProjectDetailDto>(
`/api/v1/projects/${projectId}/milestones/${milestoneId}/status`,
{ method: "PATCH", body: JSON.stringify(payload) },
token
);
},
getProjectCustomerOptions(token: string) {
return request<ProjectCustomerOptionDto[]>("/api/v1/projects/customers/options", undefined, token);
},
@@ -667,10 +676,10 @@ export const api = {
updateWorkOrder(token: string, workOrderId: string, payload: WorkOrderInput) {
return request<WorkOrderDetailDto>(`/api/v1/manufacturing/work-orders/${workOrderId}`, { method: "PUT", body: JSON.stringify(payload) }, token);
},
updateWorkOrderStatus(token: string, workOrderId: string, status: WorkOrderStatus) {
updateWorkOrderStatus(token: string, workOrderId: string, payload: WorkOrderStatusUpdateInput) {
return request<WorkOrderDetailDto>(
`/api/v1/manufacturing/work-orders/${workOrderId}/status`,
{ method: "PATCH", body: JSON.stringify({ status }) },
{ method: "PATCH", body: JSON.stringify(payload) },
token
);
},

View File

@@ -58,12 +58,11 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
}
return (
<article className="min-w-0 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">Contacts</p>
<h4 className="mt-2 text-lg font-bold text-text">People on this account</h4>
<div className="mt-5 space-y-3">
<article className="surface-panel min-w-0">
<p className="section-kicker">CONTACTS</p>
<div className="mt-3 space-y-2">
{contacts.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">
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No contacts have been added yet.
</div>
) : (
@@ -72,7 +71,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
<div>
<div className="text-sm font-semibold text-text">
{contact.fullName} {contact.isPrimary ? <span className="text-brand"> Primary</span> : null}
{contact.fullName} {contact.isPrimary ? <span className="text-brand">- PRIMARY</span> : null}
</div>
<div className="mt-1 text-sm text-muted">{crmContactRoleOptions.find((option) => option.value === contact.role)?.label ?? contact.role}</div>
</div>
@@ -86,10 +85,10 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
)}
</div>
{canManage ? (
<form className="mt-5 space-y-4" onSubmit={handleSubmit}>
<form className="mt-3 space-y-3" onSubmit={handleSubmit}>
<div className="grid gap-3 xl:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Full name</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Full name</span>
<input
value={form.fullName}
onChange={(event) => updateField("fullName", event.target.value)}
@@ -97,7 +96,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Role</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Role</span>
<select
value={form.role}
onChange={(event) => updateField("role", event.target.value as CrmContactInput["role"])}
@@ -111,7 +110,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Email</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Email</span>
<input
type="email"
value={form.email}
@@ -120,7 +119,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Phone</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Phone</span>
<input
value={form.phone}
onChange={(event) => updateField("phone", event.target.value)}
@@ -151,4 +150,3 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
</article>
);
}

View File

@@ -111,21 +111,19 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
}
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Detail</p>
<h3 className="mt-2 text-2xl font-bold text-text">{record.name}</h3>
<div className="mt-4">
<div className="flex flex-wrap gap-3">
<p className="section-kicker">CRM DETAIL</p>
<h3 className="module-title">{record.name}</h3>
<div className="mt-2.5">
<div className="flex flex-wrap gap-2">
<CrmStatusBadge status={record.status} />
{record.lifecycleStage ? <CrmLifecycleBadge stage={record.lifecycleStage} /> : null}
</div>
</div>
<p className="mt-2 text-sm text-muted">
{config.singularLabel} record last updated {new Date(record.updatedAt).toLocaleString()}.
</p>
<p className="mt-2 text-sm text-muted">UPDATED {new Date(record.updatedAt).toLocaleString()}</p>
</div>
<div className="flex flex-wrap gap-3">
<Link
@@ -146,8 +144,8 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div>
</div>
<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">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contact</p>
<article className="surface-panel min-w-0">
<p className="section-kicker">CONTACT</p>
<dl className="mt-5 grid gap-3 xl:grid-cols-2">
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt>
@@ -176,8 +174,8 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div>
</dl>
</article>
<article className="min-w-0 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">Internal Notes</p>
<article className="surface-panel min-w-0">
<p className="section-kicker">INTERNAL NOTES</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">
{record.notes || "No internal notes recorded for this account yet."}
</p>
@@ -218,31 +216,30 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
) : null}
</article>
</div>
<section className="grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<section className="grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Last Contact</p>
<div className="mt-2 text-base font-bold text-text">
{record.rollups?.lastContactAt ? new Date(record.rollups.lastContactAt).toLocaleDateString() : "None"}
</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Timeline Entries</p>
<div className="mt-2 text-base font-bold text-text">{record.rollups?.contactHistoryCount ?? record.contactHistory.length}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Account Contacts</p>
<div className="mt-2 text-base font-bold text-text">{record.rollups?.contactCount ?? record.contacts?.length ?? 0}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Attachments</p>
<div className="mt-2 text-base font-bold text-text">{record.rollups?.attachmentCount ?? 0}</div>
</article>
</section>
{entity === "customer" && (record.childCustomers?.length ?? 0) > 0 ? (
<section 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">Hierarchy</p>
<h4 className="mt-2 text-lg font-bold text-text">End customers under this reseller</h4>
<div className="mt-5 grid gap-3 xl:grid-cols-2 2xl:grid-cols-3">
<section className="surface-panel">
<p className="section-kicker">HIERARCHY</p>
<div className="mt-3 grid gap-2 xl:grid-cols-2 2xl:grid-cols-3">
{record.childCustomers?.map((child) => (
<Link
key={child.id}
@@ -259,11 +256,10 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</section>
) : null}
{entity === "vendor" ? (
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchasing Activity</p>
<h4 className="mt-2 text-lg font-bold text-text">Recent purchase orders</h4>
<p className="section-kicker">PURCHASING ACTIVITY</p>
</div>
<div className="flex flex-wrap gap-2">
{canManage ? (
@@ -277,15 +273,15 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div>
</div>
{relatedPurchaseOrders.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 exist for this vendor yet.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No purchase orders yet.</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{relatedPurchaseOrders.slice(0, 8).map((order) => (
<Link key={order.id} to={`/purchasing/orders/${order.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-center justify-between gap-3">
<div>
<div className="font-semibold text-text">{order.documentNumber}</div>
<div className="mt-1 text-xs text-muted">{new Date(order.issueDate).toLocaleDateString()} · {order.lineCount} lines</div>
<div className="mt-1 text-xs text-muted">{new Date(order.issueDate).toLocaleDateString()} - {order.lineCount} lines</div>
</div>
<div className="text-sm font-semibold text-text">${order.total.toFixed(2)}</div>
</div>
@@ -319,13 +315,9 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
/>
<section className="grid gap-3 2xl:grid-cols-[minmax(360px,0.88fr)_minmax(0,1.12fr)]">
{canManage ? (
<article className="min-w-0 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">Contact History</p>
<h4 className="mt-2 text-lg font-bold text-text">Add timeline entry</h4>
<p className="mt-2 text-sm text-muted">
Record calls, emails, meetings, and follow-up notes directly against this account.
</p>
<div className="mt-6">
<article className="surface-panel min-w-0">
<p className="section-kicker">CONTACT HISTORY</p>
<div className="mt-3">
<CrmContactEntryForm
form={contactEntryForm}
isSaving={isSavingContactEntry}
@@ -336,15 +328,14 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div>
</article>
) : null}
<article className="min-w-0 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">Timeline</p>
<h4 className="mt-2 text-lg font-bold text-text">Recent interactions</h4>
<article className="surface-panel min-w-0">
<p className="section-kicker">TIMELINE</p>
{record.contactHistory.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 contact history has been recorded for this account yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No contact history recorded yet.
</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{record.contactHistory.map((entry) => (
<article key={entry.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">

View File

@@ -110,17 +110,14 @@ export function CrmFormPage({ entity, mode }: CrmFormPageProps) {
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">
<p className="section-kicker">CRM EDITOR</p>
<h3 className="module-title">
{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}
</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>
<Link
to={mode === "create" ? config.routeBase : `${config.routeBase}/${recordId}`}
@@ -130,9 +127,9 @@ export function CrmFormPage({ entity, mode }: CrmFormPageProps) {
</Link>
</div>
</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} />
<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>
<button
type="submit"

View File

@@ -55,14 +55,11 @@ export function CrmListPage({ entity }: CrmListPageProps) {
}, [config.collectionLabel, entity, lifecycleFilter, operationalFilter, searchTerm, stateFilter, statusFilter, token]);
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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM</p>
<h3 className="mt-2 text-lg font-bold text-text">{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>
<p className="section-kicker">CRM</p>
<h3 className="module-title">{config.collectionLabel}</h3>
</div>
{canManage ? (
<Link
@@ -73,7 +70,7 @@ export function CrmListPage({ entity }: CrmListPageProps) {
</Link>
) : null}
</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">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input
@@ -137,13 +134,13 @@ export function CrmListPage({ entity }: CrmListPageProps) {
</select>
</label>
</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 ? (
<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}
</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">
<thead className="bg-page/80 text-left text-muted">
<tr>

View File

@@ -82,19 +82,16 @@ function StackedBar({
function DashboardCard({
eyebrow,
title,
children,
className = "",
}: {
eyebrow: string;
title: string;
children: ReactNode;
className?: string;
}) {
return (
<article className={`rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5 ${className}`.trim()}>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{eyebrow}</p>
<h3 className="mt-2 text-lg font-bold text-text">{title}</h3>
<article className={`surface-panel ${className}`.trim()}>
<p className="section-kicker">{eyebrow}</p>
{children}
</article>
);
@@ -290,30 +287,30 @@ export function DashboardPage() {
];
return (
<div className="space-y-4">
{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}
<div className="page-stack">
{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">
{metricCards.map((card) => (
<article key={card.label} 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">{card.label}</p>
<div className="mt-2 text-xl font-extrabold text-text">{isLoading ? "Loading..." : card.value}</div>
<div className="mt-2 flex items-center gap-3">
<article key={card.label} className="surface-panel-tight">
<p className="metric-kicker">{card.label}</p>
<div className="mt-1.5 text-xl font-extrabold text-text">{isLoading ? "Loading..." : card.value}</div>
<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-full rounded-full ${card.tone}`} style={{ width: isLoading ? "35%" : "100%" }} />
</div>
<span className="text-xs text-muted">Live</span>
</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>
))}
</section>
<section className="grid gap-3 xl:grid-cols-[1.2fr_0.8fr]">
<DashboardCard eyebrow="Commercial Surface" title="Revenue and document mix">
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quotes</div>
<div className="mt-2 text-2xl font-bold text-text">{snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access"}</div>
<div className="mt-3 space-y-2">
<DashboardCard eyebrow="COMMERCIAL">
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
<div className="surface-panel-tight">
<div className="metric-kicker">Quotes</div>
<div className="mt-1.5 text-2xl font-bold text-text">{snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access"}</div>
<div className="mt-2.5 space-y-2">
<div className="flex items-center justify-between text-xs text-muted">
<span>Draft</span>
<span>{draftQuoteCount}</span>
@@ -326,10 +323,10 @@ export function DashboardPage() {
<ProgressBar value={approvedQuoteCount} total={Math.max(quoteCount, 1)} tone="bg-emerald-500" />
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Orders</div>
<div className="mt-2 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="surface-panel-tight">
<div className="metric-kicker">Orders</div>
<div className="mt-1.5 text-2xl font-bold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</div>
<div className="mt-2.5 grid gap-2.5 sm:grid-cols-2">
<div>
<div className="text-xs text-muted">Issued / approved</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>
</div>
<div className="mt-3">
<div className="mt-2.5">
<ProgressBar value={issuedOrderCount} total={Math.max(orderCount, 1)} tone="bg-brand" />
</div>
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="CRM Footprint" title="Customer and vendor balance">
<div className="mt-4 space-y-4">
<DashboardCard eyebrow="CRM">
<div className="mt-3 space-y-3">
<div>
<div className="flex items-center justify-between text-sm">
<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" />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Customers</div>
<div className="grid gap-2.5 sm:grid-cols-3">
<div className="surface-panel-tight">
<div className="metric-kicker">Customers</div>
<div className="mt-1 text-lg font-bold text-text">{customerCount}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Resellers</div>
<div className="surface-panel-tight">
<div className="metric-kicker">Resellers</div>
<div className="mt-1 text-lg font-bold text-text">{resellerCount}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Vendors</div>
<div className="surface-panel-tight">
<div className="metric-kicker">Vendors</div>
<div className="mt-1 text-lg font-bold text-text">{vendorCount}</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">
<span className="text-muted">Strategic accounts</span>
<span className="font-semibold text-text">{strategicCustomerCount}</span>
@@ -383,11 +380,11 @@ export function DashboardPage() {
</DashboardCard>
</section>
<section className="grid gap-3 xl:grid-cols-3">
<DashboardCard eyebrow="Inventory and Supply" title="Stock posture">
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Item mix</div>
<div className="mt-3 space-y-3">
<DashboardCard eyebrow="INVENTORY">
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
<div className="surface-panel-tight">
<div className="metric-kicker">Item Mix</div>
<div className="mt-2.5 space-y-2.5">
<div>
<div className="flex items-center justify-between text-xs text-muted">
<span>Active items</span>
@@ -417,14 +414,14 @@ export function DashboardPage() {
</div>
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Storage surface</div>
<div className="mt-3 grid gap-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="surface-panel-tight">
<div className="metric-kicker">Storage</div>
<div className="mt-2.5 grid gap-2.5">
<div className="surface-panel-tight bg-surface/80">
<div className="text-xs text-muted">Warehouses</div>
<div className="mt-1 text-lg font-bold text-text">{warehouseCount}</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="mt-1 text-lg font-bold text-text">{locationCount}</div>
</div>
@@ -432,10 +429,10 @@ export function DashboardPage() {
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="Supply Execution" title="Purchasing and manufacturing flow">
<div className="mt-4 rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Open workload split</div>
<div className="mt-3">
<DashboardCard eyebrow="SUPPLY">
<div className="mt-3 surface-panel-tight">
<div className="metric-kicker">Open Workload</div>
<div className="mt-2.5">
<StackedBar
segments={[
{ value: openPurchaseOrderCount, tone: "bg-teal-500" },
@@ -445,31 +442,31 @@ export function DashboardPage() {
]}
/>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
<div className="surface-panel-tight bg-surface/80">
<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-xs text-muted">{formatCurrency(purchaseOrderValue)} committed</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="mt-1 text-lg font-bold text-text">{activeWorkOrderCount}</div>
<div className="mt-1 text-xs text-muted">{overdueWorkOrderCount} overdue</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="mt-1 text-lg font-bold text-text">{issuedPurchaseOrderCount}</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="mt-1 text-lg font-bold text-text">{releasedWorkOrderCount}</div>
</div>
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="Readiness" title="Planning pressure">
<div className="mt-4 space-y-3">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<DashboardCard eyebrow="READINESS">
<div className="mt-3 space-y-2.5">
<div className="surface-panel-tight">
<div className="flex items-center justify-between text-sm">
<span className="text-muted">Shortage items</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" />
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Build vs buy</div>
<div className="mt-3">
<div className="surface-panel-tight">
<div className="metric-kicker">Build Vs Buy</div>
<div className="mt-2.5">
<StackedBar
segments={[
{ value: buildRecommendationCount, tone: "bg-indigo-500" },
@@ -488,7 +485,7 @@ export function DashboardPage() {
]}
/>
</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 className="text-xs text-muted">Build recommendations</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 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="mt-1 text-lg font-bold text-text">{planningRollup ? totalUncoveredQuantity : "No access"}</div>
</div>
@@ -507,11 +504,11 @@ export function DashboardPage() {
</DashboardCard>
</section>
<section className="grid gap-3 xl:grid-cols-[0.95fr_1.05fr]">
<DashboardCard eyebrow="Programs" title="Project and shipment execution">
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Projects</div>
<div className="mt-3">
<DashboardCard eyebrow="PROGRAMS">
<div className="mt-3 grid gap-2.5 sm:grid-cols-2">
<div className="surface-panel-tight">
<div className="metric-kicker">Projects</div>
<div className="mt-2.5">
<StackedBar
segments={[
{ value: activeProjectCount, tone: "bg-violet-500" },
@@ -520,7 +517,7 @@ export function DashboardPage() {
]}
/>
</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 className="text-xs text-muted">Active</div>
<div className="mt-1 font-semibold text-text">{activeProjectCount}</div>
@@ -535,9 +532,9 @@ export function DashboardPage() {
</div>
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Shipping</div>
<div className="mt-3">
<div className="surface-panel-tight">
<div className="metric-kicker">Shipping</div>
<div className="mt-2.5">
<StackedBar
segments={[
{ value: activeShipmentCount, tone: "bg-brand" },
@@ -546,7 +543,7 @@ export function DashboardPage() {
]}
/>
</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 className="text-xs text-muted">Open</div>
<div className="mt-1 font-semibold text-text">{activeShipmentCount}</div>
@@ -563,8 +560,8 @@ export function DashboardPage() {
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="Operations Mix" title="Cross-module volume">
<div className="mt-4 space-y-3">
<DashboardCard eyebrow="OPERATIONS">
<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: "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: "Projects", value: projectCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-violet-500" },
].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">
<span className="text-muted">{row.label}</span>
<span className="font-semibold text-text">{row.value}</span>
@@ -585,7 +582,7 @@ export function DashboardPage() {
</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>
</section>
</div>

View File

@@ -192,18 +192,15 @@ export function FinancePage() {
const currencyCode = profile.currencyCode || "USD";
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Finance</p>
<h2 className="mt-2 text-2xl font-bold text-text">Cash, spend, and CapEx control</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>
<p className="section-kicker">FINANCE</p>
<h2 className="module-title">CASH SPEND CAPEX</h2>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3 text-sm text-muted">
Live snapshot generated {new Date(dashboard.generatedAt).toLocaleString()}
<div className="surface-panel-tight text-sm text-muted">
SNAPSHOT {new Date(dashboard.generatedAt).toLocaleString()}
</div>
</div>
</div>
@@ -217,22 +214,19 @@ export function FinancePage() {
{ label: "Mfg Cost", value: formatCurrency(summary.manufacturingTotalCost, currencyCode) },
{ label: "CapEx Actual", value: formatCurrency(summary.capexActual, currencyCode) },
].map((card) => (
<article key={card.label} 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">{card.label}</p>
<div className="mt-2 text-xl font-extrabold text-text">{card.value}</div>
<article key={card.label} className="surface-panel-tight bg-surface/90 shadow-panel">
<p className="metric-kicker">{card.label}</p>
<div className="mt-1.5 text-xl font-extrabold text-text">{card.value}</div>
</article>
))}
</section>
<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>
<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>
<p className="section-kicker">SALES ORDER LEDGER</p>
</div>
</div>
<div className="mt-5 overflow-x-auto">
<div className="mt-3 overflow-x-auto">
<table className="min-w-full divide-y divide-line/60 text-sm">
<thead>
<tr className="text-left text-xs uppercase tracking-[0.16em] text-muted">
@@ -290,9 +284,9 @@ export function FinancePage() {
</article>
<div className="space-y-3">
<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">Costing Assumptions</p>
<div className="mt-4 grid gap-3">
<article className="surface-panel">
<p className="section-kicker">COSTING ASSUMPTIONS</p>
<div className="mt-3 grid gap-2.5">
<label className="text-sm text-text">
<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" />
@@ -307,15 +301,15 @@ export function FinancePage() {
</label>
</div>
{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"}
</button>
) : null}
</article>
<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">Post Payment</p>
<div className="mt-4 grid gap-3">
<article className="surface-panel">
<p className="section-kicker">POST PAYMENT</p>
<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">
{salesOrders.map((order) => (
<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" />
</div>
{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"}
</button>
) : null}
@@ -348,19 +342,16 @@ export function FinancePage() {
</div>
<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>
<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><p className="section-kicker">RECENT PAYMENTS</p></div>
</div>
</div>
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2.5">
{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>
) : (
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>
<Link to={`/sales/orders/${payment.salesOrderId}`} className="font-semibold text-brand hover:underline">
@@ -381,12 +372,9 @@ export function FinancePage() {
</div>
</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">CapEx Tracker</p>
<p className="mt-2 text-sm text-muted">Manage equipment, tooling, and consumable capital plans with optional PO linkage.</p>
</div>
<div><p className="section-kicker">CAPEX TRACKER</p></div>
{editingCapexId ? (
<button type="button" onClick={resetCapexForm} className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
Clear edit
@@ -394,7 +382,7 @@ export function FinancePage() {
) : null}
</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" />
<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>)}
@@ -421,12 +409,12 @@ export function FinancePage() {
</div>
{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"}
</button>
) : null}
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2.5">
{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>
) : (
@@ -450,7 +438,7 @@ export function FinancePage() {
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>
@@ -473,7 +461,7 @@ export function FinancePage() {
</article>
</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}
</div>
</section>

View File

@@ -308,18 +308,18 @@ export function InventoryDetailPage() {
}
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Detail</p>
<h3 className="mt-2 text-xl font-bold text-text">{item.sku}</h3>
<p className="section-kicker">INVENTORY DETAIL</p>
<h3 className="module-title">{item.sku}</h3>
<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} />
<InventoryStatusBadge status={item.status} />
</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 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">
@@ -334,40 +334,40 @@ export function InventoryDetailPage() {
</div>
</div>
<section className="grid gap-3 xl:grid-cols-7">
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<section className="grid gap-2 xl:grid-cols-7">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">On Hand</p>
<div className="mt-2 text-base font-bold text-text">{item.onHandQuantity}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Reserved</p>
<div className="mt-2 text-base font-bold text-text">{item.reservedQuantity}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Available</p>
<div className="mt-2 text-base font-bold text-text">{item.availableQuantity}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Stock Locations</p>
<div className="mt-2 text-base font-bold text-text">{item.stockBalances.length}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Transactions</p>
<div className="mt-2 text-base font-bold text-text">{item.recentTransactions.length}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Transfers</p>
<div className="mt-2 text-base font-bold text-text">{item.transfers.length}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Reservations</p>
<div className="mt-2 text-base font-bold text-text">{item.reservations.length}</div>
</article>
</section>
<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">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Item Definition</p>
<article className="surface-panel">
<p className="section-kicker">ITEM DEFINITION</p>
<dl className="mt-5 grid gap-3 xl:grid-cols-2">
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Description</dt>
@@ -397,9 +397,9 @@ export function InventoryDetailPage() {
</div>
</dl>
</article>
<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">Thumbnail</p>
<div className="mt-4 overflow-hidden rounded-[18px] border border-line/70 bg-page/70">
<article className="surface-panel">
<p className="section-kicker">THUMBNAIL</p>
<div className="mt-3 overflow-hidden rounded-[18px] border border-line/70 bg-page/70">
{thumbnailPreviewUrl ? (
<img src={thumbnailPreviewUrl} alt={`${item.sku} thumbnail`} className="aspect-square w-full object-cover" />
) : (
@@ -408,19 +408,19 @@ export function InventoryDetailPage() {
</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."}
</div>
</article>
</div>
<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">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Stock By Location</p>
<article className="surface-panel">
<p className="section-kicker">STOCK BY LOCATION</p>
{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 posted yet.</p>
) : (
<div className="mt-4 space-y-2">
<div className="mt-3 space-y-2">
{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 className="min-w-0">
@@ -444,9 +444,9 @@ export function InventoryDetailPage() {
<section className="grid gap-3 xl:grid-cols-2">
{canManage ? (
<form className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" onSubmit={handleTransactionSubmit}>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Stock Transactions</p>
<div className="mt-5 grid gap-3">
<form className="surface-panel" onSubmit={handleTransactionSubmit}>
<p className="section-kicker">STOCK TRANSACTIONS</p>
<div className="mt-3 grid gap-3">
<div className="grid gap-3 xl:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Transaction type</span>
@@ -496,14 +496,14 @@ export function InventoryDetailPage() {
</div>
</form>
) : null}
<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">Recent Movements</p>
<article className="surface-panel">
<p className="section-kicker">RECENT MOVEMENTS</p>
{item.recentTransactions.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 stock transactions have been recorded for this item yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No stock transactions recorded yet.
</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{item.recentTransactions.map((transaction) => (
<article key={transaction.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
@@ -535,9 +535,9 @@ export function InventoryDetailPage() {
{canManage ? (
<section className="grid gap-3 xl:grid-cols-2">
<form className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" onSubmit={handleTransferSubmit}>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Transfer</p>
<div className="mt-5 grid gap-3">
<form className="surface-panel" onSubmit={handleTransferSubmit}>
<p className="section-kicker">INVENTORY TRANSFER</p>
<div className="mt-3 grid gap-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Quantity</span>
<input type="number" min={1} step={1} value={transferForm.quantity} onChange={(event) => updateTransferField("quantity", Number.parseInt(event.target.value, 10) || 1)} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
@@ -588,9 +588,9 @@ export function InventoryDetailPage() {
</div>
</div>
</form>
<form className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" onSubmit={handleReservationSubmit}>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manual Reservation</p>
<div className="mt-5 grid gap-3">
<form className="surface-panel" onSubmit={handleReservationSubmit}>
<p className="section-kicker">MANUAL RESERVATION</p>
<div className="mt-3 grid gap-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Quantity</span>
<input type="number" min={1} step={1} value={reservationForm.quantity} onChange={(event) => setReservationForm((current) => ({ ...current, quantity: Number.parseInt(event.target.value, 10) || 1 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
@@ -629,14 +629,14 @@ export function InventoryDetailPage() {
) : null}
<section className="grid gap-3 xl:grid-cols-2">
<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">Reservations</p>
<article className="surface-panel">
<p className="section-kicker">RESERVATIONS</p>
{item.reservations.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 reservations have been recorded for this item.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No reservations recorded.
</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{item.reservations.map((reservation) => (
<article key={reservation.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex items-center justify-between gap-3">
@@ -655,14 +655,14 @@ export function InventoryDetailPage() {
</div>
)}
</article>
<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">Transfers</p>
<article className="surface-panel">
<p className="section-kicker">TRANSFERS</p>
{item.transfers.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 transfers have been recorded for this item.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No transfers recorded.
</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{item.transfers.map((transfer) => (
<article key={transfer.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex items-center justify-between gap-3">

View File

@@ -11,7 +11,7 @@ import type {
} from "@mrp/shared/dist/inventory/types.js";
import type { ManufacturingStationDto } from "@mrp/shared";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
@@ -444,41 +444,52 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
}
}
function forceNavigate(path: string) {
window.location.assign(path);
}
function openSkuMaster() {
forceNavigate("/inventory/sku-master");
}
function closeEditor() {
forceNavigate(mode === "create" ? "/inventory/items" : `/inventory/items/${itemId}`);
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Item" : "Edit Item"}</h3>
<p className="mt-2 max-w-2xl text-sm text-muted">
Define item master data and the first revision of the bill of materials for assemblies and manufactured items.
</p>
<p className="section-kicker">INVENTORY EDITOR</p>
<h3 className="module-title">{mode === "create" ? "NEW ITEM" : "EDIT ITEM"}</h3>
</div>
<div className="flex flex-wrap gap-2">
<Link
to="/inventory/sku-master"
<button
type="button"
onClick={openSkuMaster}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
>
SKU master
</Link>
<Link
to={mode === "create" ? "/inventory/items" : `/inventory/items/${itemId}`}
</button>
<button
type="button"
onClick={closeEditor}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
>
Cancel
</Link>
</button>
</div>
</div>
</section>
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="surface-panel space-y-3">
<div className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-4">
<div className="block 2xl:col-span-2">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="block text-sm font-semibold text-text">SKU builder</span>
<Link to="/inventory/sku-master" className="text-xs font-semibold text-brand">
<span className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">SKU BUILDER</span>
<button type="button" onClick={openSkuMaster} className="text-xs font-semibold text-brand">
Manage SKU tree
</Link>
</button>
</div>
<div className="space-y-3 rounded-[18px] border border-line/70 bg-page/70 p-3">
<label className="block">
@@ -593,7 +604,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-sm font-semibold text-text">Thumbnail attachment</div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Thumbnail attachment</div>
<div className="mt-2 text-sm text-muted">
{pendingThumbnailFile
? `${pendingThumbnailFile.name} will upload when you save this item.`
@@ -603,9 +614,6 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
? `${thumbnailAttachment.originalName} is attached as the current item thumbnail.`
: "Attach a product image, render, or reference photo for this item."}
</div>
<div className="mt-3 text-xs text-muted">
Supported by the existing file-attachment system. The thumbnail is stored separately from general item documents so the item editor can treat it as the primary visual.
</div>
</div>
</div>
<div className="grid gap-3 xl:grid-cols-4">
@@ -654,7 +662,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
<div className="grid gap-3 sm:grid-cols-2">
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input type="checkbox" checked={form.isSellable} onChange={(event) => updateField("isSellable", event.target.checked)} />
<span className="text-sm font-semibold text-text">Sellable</span>
<span className="text-sm font-semibold uppercase tracking-[0.08em] text-text">Sellable</span>
</label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input
@@ -662,7 +670,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
checked={form.isPurchasable}
onChange={(event) => updateField("isPurchasable", event.target.checked)}
/>
<span className="text-sm font-semibold text-text">Purchasable</span>
<span className="text-sm font-semibold uppercase tracking-[0.08em] text-text">Purchasable</span>
</label>
</div>
</div>
@@ -733,7 +741,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
) : null}
</div>
<div className="mt-2 text-xs text-muted">
{form.preferredVendorId ? getSelectedVendorName(form.preferredVendorId) : "Demand planning uses this vendor when creating buy recommendations."}
{form.preferredVendorId ? getSelectedVendorName(form.preferredVendorId) : "Used as the default buy source."}
</div>
</label>
<label className="block">
@@ -756,23 +764,22 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
</label>
</section>
{form.type === "ASSEMBLY" || form.type === "MANUFACTURED" ? (
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing Routing</p>
<h4 className="mt-2 text-lg font-bold text-text">Station and time template</h4>
<p className="mt-2 text-sm text-muted">These operations are copied automatically into work orders and feed the planning workbench without manual planner task entry.</p>
<p className="section-kicker">MANUFACTURING ROUTING</p>
<h4 className="text-lg font-bold text-text">STATION AND TIME TEMPLATE</h4>
</div>
<button type="button" onClick={addOperation} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Add operation
</button>
</div>
{form.operations.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
Add at least one station operation for this buildable item.
</div>
) : (
<div className="mt-5 space-y-4">
<div className="mt-3 space-y-3">
{form.operations.map((operation, index) => (
<div key={`${operation.stationId}-${operation.position}-${index}`} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="grid gap-3 xl:grid-cols-[1.2fr_0.55fr_0.7fr_0.55fr_0.55fr_auto]">
@@ -823,12 +830,11 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
)}
</section>
) : null}
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Bill Of Materials</p>
<h4 className="mt-2 text-lg font-bold text-text">Component lines</h4>
<p className="mt-2 text-sm text-muted">Add BOM components for manufactured or assembly items. Purchased and service items can be saved without BOM lines.</p>
<p className="section-kicker">BILL OF MATERIALS</p>
<h4 className="text-lg font-bold text-text">COMPONENT LINES</h4>
</div>
<button
type="button"
@@ -839,11 +845,11 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
</button>
</div>
{form.bomLines.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No BOM lines added yet.
</div>
) : (
<div className="mt-5 space-y-4">
<div className="mt-3 space-y-3">
{form.bomLines.map((line, index) => (
<div key={`${line.componentItemId}-${line.position}-${index}`} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="grid gap-3 xl:grid-cols-[1.4fr_0.7fr_0.7fr_0.7fr_auto]">
@@ -974,7 +980,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
))}
</div>
)}
<div className="mt-6 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="mt-4 flex flex-col gap-2 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span>
<button
type="submit"

View File

@@ -41,14 +41,11 @@ export function InventoryListPage() {
}, [searchTerm, statusFilter, token, typeFilter]);
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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p>
<h3 className="mt-2 text-lg font-bold text-text">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>
<p className="section-kicker">INVENTORY</p>
<h3 className="module-title">ITEM MASTER</h3>
</div>
{canManage ? (
<div className="flex flex-wrap gap-2">
@@ -61,7 +58,7 @@ export function InventoryListPage() {
</div>
) : null}
</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">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input
@@ -100,13 +97,13 @@ export function InventoryListPage() {
</select>
</label>
</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 ? (
<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.
</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">
<thead className="bg-page/80 text-left text-muted">
<tr>

View File

@@ -87,7 +87,7 @@ export function InventorySkuMasterPage() {
return (
<div key={node.id} className="space-y-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" style={{ marginLeft: `${depth * 16}px` }}>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2" style={{ marginLeft: `${depth * 16}px` }}>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
@@ -107,8 +107,10 @@ export function InventorySkuMasterPage() {
</span>
)}
<div className="min-w-0">
<div className="text-sm font-semibold text-text">{node.code} <span className="text-muted">- {node.label}</span></div>
<div className="mt-1 text-xs text-muted">Level {node.level} {node.childCount} child branch(es)</div>
<div className="text-sm font-semibold text-text">
{node.code} <span className="text-muted">- {node.label}</span>
</div>
<div className="mt-1 text-xs text-muted">Level {node.level} - {node.childCount} child branch(es)</div>
</div>
</div>
</div>
@@ -193,13 +195,12 @@ export function InventorySkuMasterPage() {
}
return (
<section className="space-y-6">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Master Data</p>
<h3 className="mt-2 text-xl font-bold text-text">SKU Master Builder</h3>
<p className="mt-2 max-w-3xl text-sm text-muted">Define family roots, branch-specific child codes, and the family-scoped short-code suffix that finishes each generated SKU.</p>
<p className="section-kicker">INVENTORY MASTER DATA</p>
<h3 className="module-title">SKU MASTER BUILDER</h3>
</div>
<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">
Back to items
@@ -207,13 +208,13 @@ export function InventorySkuMasterPage() {
</div>
</div>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.5fr]">
<div className="space-y-6">
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="text-sm font-semibold text-text">Families</div>
<div className="mt-4 space-y-2">
<div className="grid gap-3 xl:grid-cols-[0.9fr_1.5fr]">
<div className="space-y-3">
<section className="surface-panel">
<p className="section-kicker">FAMILIES</p>
<div className="mt-3 space-y-2">
{catalog.families.length === 0 ? (
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-6 text-sm text-muted">No SKU families defined yet.</div>
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-sm text-muted">No SKU families defined yet.</div>
) : (
catalog.families.map((family) => (
<button
@@ -224,13 +225,18 @@ export function InventorySkuMasterPage() {
setExpandedNodeIds([]);
setNodeForm((current) => ({ ...current, familyId: family.id, parentNodeId: null }));
}}
className={`block w-full rounded-[18px] border px-3 py-3 text-left transition ${
className={`block w-full rounded-[18px] border px-2 py-2 text-left transition ${
selectedFamilyId === family.id ? "border-brand bg-brand/8" : "border-line/70 bg-page/60 hover:bg-page/80"
}`}
>
<div className="text-sm font-semibold text-text">{family.code} <span className="text-muted">({family.sequenceCode})</span></div>
<div className="text-sm font-semibold text-text">
{family.code} <span className="text-muted">({family.sequenceCode})</span>
</div>
<div className="mt-1 text-xs text-muted">{family.name}</div>
<div className="mt-2 text-xs text-muted">{family.childNodeCount} branch nodes next {family.sequenceCode}{String(family.nextSequenceNumber).padStart(4, "0")}</div>
<div className="mt-2 text-xs text-muted">
{family.childNodeCount} branch nodes - next {family.sequenceCode}
{String(family.nextSequenceNumber).padStart(4, "0")}
</div>
</button>
))
)}
@@ -238,9 +244,9 @@ export function InventorySkuMasterPage() {
</section>
{canManage ? (
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="text-sm font-semibold text-text">Add family</div>
<form className="mt-4 space-y-3" onSubmit={handleCreateFamily}>
<section className="surface-panel">
<p className="section-kicker">ADD FAMILY</p>
<form className="mt-3 space-y-3" onSubmit={handleCreateFamily}>
<div className="grid gap-3 sm:grid-cols-2">
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Family code</span>
@@ -265,26 +271,26 @@ export function InventorySkuMasterPage() {
) : null}
</div>
<div className="space-y-6">
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="space-y-3">
<section className="surface-panel">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="text-sm font-semibold text-text">Branch tree</div>
<p className="section-kicker">BRANCH TREE</p>
<div className="mt-1 text-xs text-muted">{status}</div>
</div>
{selectedFamilyId ? (
<div className="text-xs text-muted">Up to 6 total SKU levels including family root.</div>
<div className="text-xs text-muted">Up to 6 total SKU levels.</div>
) : null}
</div>
<div className="mt-4 space-y-3">
{selectedFamilyId ? renderNodes(null) : <div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-6 text-sm text-muted">Select a family to inspect or extend its branch tree.</div>}
<div className="mt-3 space-y-2">
{selectedFamilyId ? renderNodes(null) : <div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-sm text-muted">Select a family to inspect or extend its branch tree.</div>}
</div>
</section>
{canManage ? (
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="text-sm font-semibold text-text">Add branch node</div>
<form className="mt-4 space-y-3" onSubmit={handleCreateNode}>
<section className="surface-panel">
<p className="section-kicker">ADD BRANCH NODE</p>
<form className="mt-3 space-y-3" onSubmit={handleCreateNode}>
<div className="grid gap-3 sm:grid-cols-2">
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Family</span>

View File

@@ -32,18 +32,18 @@ export function WarehouseDetailPage() {
}, [token, warehouseId]);
if (!warehouse) {
return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-8 text-sm text-muted shadow-panel">{status}</div>;
return <div className="surface-panel text-sm text-muted">{status}</div>;
}
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Warehouse Detail</p>
<h3 className="mt-2 text-2xl font-bold text-text">{warehouse.code}</h3>
<p className="mt-1 text-sm text-text">{warehouse.name}</p>
<p className="mt-3 text-sm text-muted">Last updated {new Date(warehouse.updatedAt).toLocaleString()}.</p>
<p className="section-kicker">WAREHOUSE DETAIL</p>
<h3 className="module-title">{warehouse.code}</h3>
<p className="text-sm text-text">{warehouse.name}</p>
<p className="mt-2 text-xs text-muted">Updated {new Date(warehouse.updatedAt).toLocaleString()}</p>
</div>
<div className="flex flex-wrap gap-3">
<Link to="/inventory/warehouses" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
@@ -58,27 +58,26 @@ export function WarehouseDetailPage() {
</div>
</div>
<div className="grid gap-3 xl:grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)]">
<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">Notes</p>
<article className="surface-panel">
<p className="section-kicker">NOTES</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{warehouse.notes || "No warehouse notes recorded."}</p>
<div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
<div className="mt-4 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
Created {new Date(warehouse.createdAt).toLocaleDateString()}
</div>
</article>
<section 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">Locations</p>
<h4 className="mt-2 text-lg font-bold text-text">Stock locations</h4>
<section className="surface-panel">
<p className="section-kicker">LOCATIONS</p>
{warehouse.locations.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No stock locations have been defined for this warehouse yet.
</div>
) : (
<div className="mt-6 grid gap-3 xl:grid-cols-2">
<div className="mt-3 grid gap-2 xl:grid-cols-2">
{warehouse.locations.map((location: WarehouseLocationDto) => (
<article key={location.id} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="text-sm font-semibold text-text">{location.code}</div>
<div className="mt-1 text-sm text-text">{location.name}</div>
<div className="mt-2 text-xs leading-6 text-muted">{location.notes || "No notes."}</div>
<div className="mt-2 text-xs text-muted">{location.notes || "No notes."}</div>
</article>
))}
</div>

View File

@@ -92,12 +92,12 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Warehouse Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Warehouse" : "Edit Warehouse"}</h3>
<p className="section-kicker">WAREHOUSE EDITOR</p>
<h3 className="module-title">{mode === "create" ? "NEW WAREHOUSE" : "EDIT WAREHOUSE"}</h3>
</div>
<Link
to={mode === "create" ? "/inventory/warehouses" : `/inventory/warehouses/${warehouseId}`}
@@ -107,40 +107,39 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
</Link>
</div>
</section>
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="surface-panel space-y-3">
<div className="grid gap-3 xl:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Warehouse code</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Warehouse code</span>
<input value={form.code} onChange={(event) => updateField("code", event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Warehouse name</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Warehouse name</span>
<input value={form.name} onChange={(event) => updateField("name", event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Notes</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span>
<textarea value={form.notes} onChange={(event) => updateField("notes", event.target.value)} rows={4} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Locations</p>
<h4 className="mt-2 text-lg font-bold text-text">Internal stock locations</h4>
<p className="section-kicker">LOCATIONS</p>
</div>
<button type="button" onClick={addLocation} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Add location
</button>
</div>
{form.locations.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No locations added yet.
</div>
) : (
<div className="mt-5 space-y-4">
<div className="mt-3 space-y-3">
{form.locations.map((location: WarehouseLocationInput, index: number) => (
<div key={index} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div key={index} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="grid gap-3 xl:grid-cols-[0.7fr_1fr_auto]">
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Code</span>
@@ -156,7 +155,7 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
</button>
</div>
</div>
<label className="mt-4 block">
<label className="mt-3 block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span>
<input value={location.notes} onChange={(event) => updateLocation(index, { ...location, notes: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
@@ -164,7 +163,7 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
))}
</div>
)}
<div className="mt-6 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="mt-3 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">
<span className="min-w-0 text-sm text-muted">{status}</span>
<button type="submit" disabled={isSaving} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isSaving ? "Saving..." : mode === "create" ? "Create warehouse" : "Save changes"}

View File

@@ -31,12 +31,11 @@ export function WarehousesPage() {
}, [token]);
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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p>
<h3 className="mt-2 text-lg font-bold text-text">Warehouses</h3>
<p className="mt-2 max-w-2xl text-sm text-muted">Physical warehouse records and their internal stock locations.</p>
<p className="section-kicker">INVENTORY</p>
<h3 className="module-title">WAREHOUSES</h3>
</div>
{canManage ? (
<Link to="/inventory/warehouses/new" className="inline-flex items-center justify-center rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white">
@@ -44,13 +43,13 @@ export function WarehousesPage() {
</Link>
) : null}
</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-3 rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted">{status}</div>
{warehouses.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No warehouses have been added yet.
</div>
) : (
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
<div className="mt-3 overflow-hidden rounded-2xl border border-line/70">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead className="bg-page/80 text-left text-muted">
<tr>

View File

@@ -79,20 +79,19 @@ export function ManufacturingPage() {
}
return (
<div className="space-y-4">
<div className="page-stack">
<section className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_400px]">
<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">Manufacturing Stations</p>
<h3 className="mt-2 text-xl font-bold text-text">Scheduling anchors</h3>
<p className="mt-2 text-sm text-muted">Stations define where operation time belongs. Buildable items reference them in their routing template, and work orders inherit those steps automatically into planning.</p>
<article className="surface-panel">
<p className="section-kicker">MANUFACTURING STATIONS</p>
<h3 className="module-title">SCHEDULING ANCHORS</h3>
{stations.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No stations defined yet.
</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{stations.map((station) => (
<article key={station.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<article key={station.id} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="flex items-start justify-between gap-3">
<div>
<div className="font-semibold text-text">{station.code} - {station.name}</div>
@@ -116,33 +115,33 @@ export function ManufacturingPage() {
)}
</article>
{canManage ? (
<form onSubmit={handleSubmit} 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">{editingStationId ? "Edit Station" : "New Station"}</p>
<div className="mt-4 grid gap-3">
<form onSubmit={handleSubmit} className="surface-panel">
<p className="section-kicker">{editingStationId ? "EDIT STATION" : "NEW STATION"}</p>
<div className="mt-3 grid gap-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Code</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Code</span>
<input value={form.code} onChange={(event) => setForm((current) => ({ ...current, code: event.target.value }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Name</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Name</span>
<input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Expected Wait (Days)</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Expected wait (days)</span>
<input type="number" min={0} step={1} value={form.queueDays} onChange={(event) => setForm((current) => ({ ...current, queueDays: Number.parseInt(event.target.value, 10) || 0 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<div className="grid gap-3 sm:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Capacity Minutes / Day</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Capacity minutes / day</span>
<input type="number" min={60} step={30} value={form.dailyCapacityMinutes} onChange={(event) => setForm((current) => ({ ...current, dailyCapacityMinutes: Number.parseInt(event.target.value, 10) || 480 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Parallel Capacity</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Parallel capacity</span>
<input type="number" min={1} step={1} value={form.parallelCapacity} onChange={(event) => setForm((current) => ({ ...current, parallelCapacity: Number.parseInt(event.target.value, 10) || 1 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Working Days</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Working days</span>
<div className="flex flex-wrap gap-2">
{[
{ value: 1, label: "Mon" },
@@ -172,7 +171,7 @@ export function ManufacturingPage() {
</div>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Description</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Description</span>
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} rows={3} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">

View File

@@ -30,6 +30,7 @@ export function WorkOrderDetailPage() {
const [operatorOptions, setOperatorOptions] = useState<ManufacturingUserOptionDto[]>([]);
const [issueForm, setIssueForm] = useState<WorkOrderMaterialIssueInput>(emptyMaterialIssueInput);
const [completionForm, setCompletionForm] = useState<WorkOrderCompletionInput>(emptyCompletionInput);
const [holdReasonDraft, setHoldReasonDraft] = useState("");
const [status, setStatus] = useState("Loading work order...");
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [isPostingIssue, setIsPostingIssue] = useState(false);
@@ -121,8 +122,12 @@ export function WorkOrderDetailPage() {
setIsUpdatingStatus(true);
setStatus("Updating work-order status...");
try {
const nextWorkOrder = await api.updateWorkOrderStatus(token, workOrder.id, nextStatus);
const nextWorkOrder = await api.updateWorkOrderStatus(token, workOrder.id, {
status: nextStatus,
reason: nextStatus === "ON_HOLD" ? holdReasonDraft : null,
});
setWorkOrder(nextWorkOrder);
setHoldReasonDraft("");
setStatus("Work-order status updated. Review downstream planning and shipment readiness if this change affects execution timing.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to update work-order status.";
@@ -332,6 +337,8 @@ export function WorkOrderDetailPage() {
impact:
nextStatus === "CANCELLED"
? "Cancelling a work order can invalidate planning assumptions, reservations, and operator expectations."
: nextStatus === "ON_HOLD"
? "Putting a work order on hold pauses expected execution and should capture the exact blocker so planning and shop-floor review stay aligned."
: nextStatus === "COMPLETE"
? "Completing the work order signals execution closure and can change readiness views across the system."
: "This changes the execution state used by planning, dashboards, and downstream operational review.",
@@ -341,6 +348,7 @@ export function WorkOrderDetailPage() {
confirmationValue: nextStatus === "CANCELLED" ? workOrder.workOrderNumber : undefined,
nextStatus,
});
setHoldReasonDraft(nextStatus === "ON_HOLD" ? workOrder.holdReason ?? "" : "");
}
function handleIssueSubmit(event: React.FormEvent<HTMLFormElement>) {
@@ -383,14 +391,20 @@ export function WorkOrderDetailPage() {
}
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Work Order</p>
<h3 className="mt-2 text-xl font-bold text-text">{workOrder.workOrderNumber}</h3>
<p className="section-kicker">WORK ORDER</p>
<h3 className="module-title">{workOrder.workOrderNumber}</h3>
<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 ? (
<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="metric-kicker text-amber-900">Current Hold Reason</div>
<div className="mt-2 whitespace-pre-line">{workOrder.holdReason}</div>
</div>
) : null}
</div>
<div className="flex flex-wrap gap-3">
<Link to="/manufacturing/work-orders" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Back to work orders</Link>
@@ -402,11 +416,10 @@ export function WorkOrderDetailPage() {
</div>
</div>
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
<p className="mt-2 text-sm text-muted">Release, hold, or close administrative status from the work-order record.</p>
<p className="section-kicker">QUICK ACTIONS</p>
</div>
<div className="flex flex-wrap gap-2">
{workOrderStatusOptions.map((option) => (
@@ -418,20 +431,20 @@ export function WorkOrderDetailPage() {
</div>
</section>
) : null}
<section className="grid gap-3 xl:grid-cols-6">
<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">Planned</p><div className="mt-2 text-base font-bold text-text">{workOrder.quantity}</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">Completed</p><div className="mt-2 text-base font-bold text-text">{workOrder.completedQuantity}</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">Remaining</p><div className="mt-2 text-base font-bold text-text">{workOrder.dueQuantity}</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">Project</p><div className="mt-2 text-base font-bold text-text">{workOrder.projectNumber || "Unlinked"}</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">Operations</p><div className="mt-2 text-base font-bold text-text">{workOrder.operations.length}</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">Due Date</p><div className="mt-2 text-base font-bold text-text">{workOrder.dueDate ? new Date(workOrder.dueDate).toLocaleDateString() : "Not set"}</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">Material Shortage</p><div className="mt-2 text-base font-bold text-text">{workOrder.materialRequirements.reduce((sum, requirement) => sum + requirement.shortageQuantity, 0)}</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">Actual Hours</p><div className="mt-2 text-base font-bold text-text">{(workOrder.totalActualMinutes / 60).toFixed(1)}</div></article>
<section className="grid gap-2 xl:grid-cols-6">
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Planned</p><div className="mt-2 text-base font-bold text-text">{workOrder.quantity}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Completed</p><div className="mt-1 text-base font-bold text-text">{workOrder.completedQuantity}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Remaining</p><div className="mt-1 text-base font-bold text-text">{workOrder.dueQuantity}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Project</p><div className="mt-1 text-base font-bold text-text">{workOrder.projectNumber || "Unlinked"}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operations</p><div className="mt-1 text-base font-bold text-text">{workOrder.operations.length}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Due Date</p><div className="mt-1 text-base font-bold text-text">{workOrder.dueDate ? new Date(workOrder.dueDate).toLocaleDateString() : "Not set"}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Material Shortage</p><div className="mt-1 text-base font-bold text-text">{workOrder.materialRequirements.reduce((sum, requirement) => sum + requirement.shortageQuantity, 0)}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Actual Hours</p><div className="mt-1 text-base font-bold text-text">{(workOrder.totalActualMinutes / 60).toFixed(1)}</div></article>
</section>
<div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.9fr)]">
<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">Execution Context</p>
<dl className="mt-5 grid gap-3">
<article className="surface-panel">
<p className="section-kicker">EXECUTION CONTEXT</p>
<dl className="mt-3 grid gap-3">
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build item</dt><dd className="mt-1 text-sm text-text">{workOrder.itemSku} - {workOrder.itemName}</dd></div>
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Item type</dt><dd className="mt-1 text-sm text-text">{workOrder.itemType}</dd></div>
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Output location</dt><dd className="mt-1 text-sm text-text">{workOrder.warehouseCode} / {workOrder.locationCode}</dd></div>
@@ -439,17 +452,17 @@ export function WorkOrderDetailPage() {
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Demand source</dt><dd className="mt-1 text-sm text-text">{workOrder.salesOrderNumber ?? "Not linked"}</dd></div>
</dl>
</article>
<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">Work Instructions</p>
<article className="surface-panel">
<p className="section-kicker">WORK INSTRUCTIONS</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{workOrder.notes || "No work-order notes recorded."}</p>
</article>
</div>
<section 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">Operation Plan</p>
<section className="surface-panel">
<p className="section-kicker">OPERATION PLAN</p>
{workOrder.operations.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">This work order has no inherited station operations. Add routing steps on the item record to automate planning.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">This work order has no inherited station operations. Add routing steps on the item record to automate planning.</div>
) : (
<div className="mt-5 overflow-hidden rounded-[18px] border border-line/70">
<div className="mt-3 overflow-hidden rounded-[18px] border border-line/70">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead className="bg-page/70">
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
@@ -634,11 +647,11 @@ export function WorkOrderDetailPage() {
</section>
{canManage ? (
<section className="grid gap-3 xl:grid-cols-2">
<form onSubmit={handleIssueSubmit} 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">Material Issue</p>
<div className="mt-4 grid gap-3">
<form onSubmit={handleIssueSubmit} className="surface-panel">
<p className="section-kicker">MATERIAL ISSUE</p>
<div className="mt-3 grid gap-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Component</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Component</span>
<select value={issueForm.componentItemId} onChange={(event) => setIssueForm((current) => ({ ...current, componentItemId: event.target.value }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand">
<option value="">Select component</option>
{workOrder.materialRequirements.map((requirement) => (
@@ -648,7 +661,7 @@ export function WorkOrderDetailPage() {
</label>
<div className="grid gap-3 sm:grid-cols-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Warehouse</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Warehouse</span>
<select value={issueForm.warehouseId} onChange={(event) => setIssueForm((current) => ({ ...current, warehouseId: event.target.value, locationId: "" }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand">
{[...new Map(locationOptions.map((option) => [option.warehouseId, option])).values()].map((option) => (
<option key={option.warehouseId} value={option.warehouseId}>{option.warehouseCode} - {option.warehouseName}</option>
@@ -656,7 +669,7 @@ export function WorkOrderDetailPage() {
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Location</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Location</span>
<select value={issueForm.locationId} onChange={(event) => setIssueForm((current) => ({ ...current, locationId: event.target.value }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand">
<option value="">Select location</option>
{filteredLocationOptions.map((option) => (
@@ -665,12 +678,12 @@ export function WorkOrderDetailPage() {
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Quantity</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quantity</span>
<input type="number" min={1} step={1} value={issueForm.quantity} onChange={(event) => setIssueForm((current) => ({ ...current, quantity: Number.parseInt(event.target.value, 10) || 1 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Notes</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span>
<textarea value={issueForm.notes} onChange={(event) => setIssueForm((current) => ({ ...current, notes: event.target.value }))} rows={3} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<button type="submit" disabled={isPostingIssue} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
@@ -678,15 +691,15 @@ export function WorkOrderDetailPage() {
</button>
</div>
</form>
<form onSubmit={handleCompletionSubmit} 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">Production Completion</p>
<div className="mt-4 grid gap-3">
<form onSubmit={handleCompletionSubmit} className="surface-panel">
<p className="section-kicker">PRODUCTION COMPLETION</p>
<div className="mt-3 grid gap-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Quantity</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quantity</span>
<input type="number" min={1} step={1} value={completionForm.quantity} onChange={(event) => setCompletionForm((current) => ({ ...current, quantity: Number.parseInt(event.target.value, 10) || 1 }))} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Notes</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span>
<textarea value={completionForm.notes} onChange={(event) => setCompletionForm((current) => ({ ...current, notes: event.target.value }))} rows={3} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<div className="rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">Finished goods receipt posts back to {workOrder.warehouseCode} / {workOrder.locationCode}.</div>
@@ -697,12 +710,12 @@ export function WorkOrderDetailPage() {
</form>
</section>
) : null}
<section 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">Material Requirements</p>
<section className="surface-panel">
<p className="section-kicker">MATERIAL REQUIREMENTS</p>
{workOrder.materialRequirements.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">This build item does not currently have BOM material requirements.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">This build item does not currently have BOM material requirements.</div>
) : (
<div className="mt-5 overflow-hidden rounded-[18px] border border-line/70">
<div className="mt-3 overflow-hidden rounded-[18px] border border-line/70">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead className="bg-page/70">
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
@@ -733,14 +746,14 @@ export function WorkOrderDetailPage() {
)}
</section>
<section className="grid gap-3 xl:grid-cols-2">
<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">Issue History</p>
<article className="surface-panel">
<p className="section-kicker">ISSUE HISTORY</p>
{workOrder.materialIssues.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 material issues have been posted yet.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No material issues have been posted yet.</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{workOrder.materialIssues.map((issue) => (
<div key={issue.id} className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<div key={issue.id} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="font-semibold text-text">{issue.componentSku} - {issue.componentName}</div>
@@ -755,14 +768,14 @@ export function WorkOrderDetailPage() {
</div>
)}
</article>
<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">Completion History</p>
<article className="surface-panel">
<p className="section-kicker">COMPLETION HISTORY</p>
{workOrder.completions.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 production completions have been posted yet.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No production completions have been posted yet.</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{workOrder.completions.map((completion) => (
<div key={completion.id} className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3">
<div key={completion.id} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="font-semibold text-text">{completion.quantity} completed</div>
<div className="text-xs text-muted">{completion.createdByName}</div>
@@ -793,6 +806,12 @@ export function WorkOrderDetailPage() {
confirmLabel={pendingConfirmation?.confirmLabel ?? "Confirm"}
confirmationLabel={pendingConfirmation?.confirmationLabel}
confirmationValue={pendingConfirmation?.confirmationValue}
extraFieldLabel={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? "Hold reason" : undefined}
extraFieldPlaceholder={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? "Explain the blocker forcing this work order onto hold." : undefined}
extraFieldValue={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? holdReasonDraft : undefined}
extraFieldRequired={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD"}
extraFieldMultiline={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD"}
onExtraFieldChange={pendingConfirmation?.kind === "status" && pendingConfirmation?.nextStatus === "ON_HOLD" ? setHoldReasonDraft : undefined}
isConfirming={
(pendingConfirmation?.kind === "status" && isUpdatingStatus) ||
(pendingConfirmation?.kind === "issue" && isPostingIssue) ||

View File

@@ -5,7 +5,7 @@ import type {
} from "@mrp/shared";
import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js";
import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
@@ -137,21 +137,24 @@ export function WorkOrderFormPage({ mode }: { mode: "create" | "edit" }) {
}
}
function closeEditor() {
navigate(mode === "create" ? "/manufacturing/work-orders" : `/manufacturing/work-orders/${workOrderId}`);
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Work Order" : "Edit Work Order"}</h3>
<p className="mt-2 max-w-2xl text-sm text-muted">Create a build record for a manufactured item, assign it to a project when needed, and define where completed output should post.</p>
<p className="section-kicker">MANUFACTURING EDITOR</p>
<h3 className="module-title">{mode === "create" ? "NEW WORK ORDER" : "EDIT WORK ORDER"}</h3>
</div>
<Link to={mode === "create" ? "/manufacturing/work-orders" : `/manufacturing/work-orders/${workOrderId}`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
<button type="button" onClick={closeEditor} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Cancel
</Link>
</button>
</div>
</section>
<section className="space-y-4 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="surface-panel space-y-3">
<div className="grid gap-3 xl:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Build Item</span>
@@ -195,7 +198,7 @@ export function WorkOrderFormPage({ mode }: { mode: "create" | "edit" }) {
setItemPickerOpen(false);
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70">
<div className="font-semibold text-text">{option.sku}</div>
<div className="mt-1 text-xs text-muted">{option.name} · {option.type} · {option.operationCount} ops</div>
<div className="mt-1 text-xs text-muted">{option.name} - {option.type} - {option.operationCount} ops</div>
</button>
))}
</div>
@@ -252,7 +255,7 @@ export function WorkOrderFormPage({ mode }: { mode: "create" | "edit" }) {
setProjectPickerOpen(false);
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70">
<div className="font-semibold text-text">{option.projectNumber}</div>
<div className="mt-1 text-xs text-muted">{option.name} · {option.customerName}</div>
<div className="mt-1 text-xs text-muted">{option.name} - {option.customerName}</div>
</button>
))}
</div>
@@ -294,7 +297,7 @@ export function WorkOrderFormPage({ mode }: { mode: "create" | "edit" }) {
<span className="mb-2 block text-sm font-semibold text-text">Work instructions / notes</span>
<textarea value={form.notes} onChange={(event) => updateField("notes", event.target.value)} rows={5} className="w-full rounded-[18px] border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<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 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span>
<button type="submit" disabled={isSaving} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isSaving ? "Saving..." : mode === "create" ? "Create work order" : "Save changes"}

View File

@@ -35,13 +35,12 @@ export function WorkOrderListPage() {
}, [query, statusFilter, token]);
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing</p>
<h3 className="mt-2 text-xl font-bold text-text">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>
<p className="section-kicker">MANUFACTURING</p>
<h3 className="module-title">WORK ORDERS</h3>
</div>
{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">
@@ -50,8 +49,8 @@ export function WorkOrderListPage() {
) : null}
</div>
</div>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_240px]">
<section className="surface-panel">
<div className="grid gap-2.5 xl:grid-cols-[minmax(0,1fr)_240px]">
<label className="block">
<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" />
@@ -63,7 +62,7 @@ export function WorkOrderListPage() {
</select>
</label>
</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>
{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>

View File

@@ -1,5 +1,5 @@
import { permissions } from "@mrp/shared";
import type { WorkOrderSummaryDto } from "@mrp/shared";
import type { ProjectMilestoneStatus, WorkOrderSummaryDto } from "@mrp/shared";
import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js";
import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js";
import { useEffect, useState } from "react";
@@ -23,6 +23,7 @@ export function ProjectDetailPage() {
const [workOrders, setWorkOrders] = useState<WorkOrderSummaryDto[]>([]);
const [planning, setPlanning] = useState<SalesOrderPlanningDto | null>(null);
const [status, setStatus] = useState("Loading project...");
const [updatingMilestoneId, setUpdatingMilestoneId] = useState<string | null>(null);
const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false;
@@ -50,7 +51,7 @@ export function ProjectDetailPage() {
}, [projectId, token]);
if (!project) {
return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
return <div className="surface-panel text-sm text-muted">{status}</div>;
}
const sortedMilestones = [...project.milestones].sort((left, right) => {
@@ -107,15 +108,59 @@ export function ProjectDetailPage() {
? "text-amber-600 dark:text-amber-300"
: "text-rose-600 dark:text-rose-300";
async function updateMilestoneStatus(milestoneId: string, nextStatus: ProjectMilestoneStatus) {
if (!token || !project) {
return;
}
setUpdatingMilestoneId(milestoneId);
setStatus("Updating milestone status...");
try {
const nextProject = await api.updateProjectMilestoneStatus(token, project.id, milestoneId, { status: nextStatus });
setProject(nextProject);
setStatus("Milestone status updated.");
} catch (error: unknown) {
const message = error instanceof ApiError ? error.message : "Unable to update milestone status.";
setStatus(message);
} finally {
setUpdatingMilestoneId(null);
}
}
function milestoneQuickActions(currentStatus: ProjectMilestoneStatus) {
if (currentStatus === "PLANNED") {
return [
{ status: "IN_PROGRESS" as const, label: "Start" },
{ status: "BLOCKED" as const, label: "Block" },
{ status: "COMPLETE" as const, label: "Complete" },
];
}
if (currentStatus === "IN_PROGRESS") {
return [
{ status: "BLOCKED" as const, label: "Block" },
{ status: "COMPLETE" as const, label: "Complete" },
{ status: "PLANNED" as const, label: "Reset" },
];
}
if (currentStatus === "BLOCKED") {
return [
{ status: "IN_PROGRESS" as const, label: "Resume" },
{ status: "COMPLETE" as const, label: "Complete" },
{ status: "PLANNED" as const, label: "Reset" },
];
}
return [{ status: "IN_PROGRESS" as const, label: "Reopen" }];
}
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project</p>
<h3 className="mt-2 text-xl font-bold text-text">{project.projectNumber}</h3>
<p className="section-kicker">PROJECT</p>
<h3 className="module-title">{project.projectNumber}</h3>
<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} />
<ProjectPriorityBadge priority={project.priority} />
</div>
@@ -127,47 +172,45 @@ export function ProjectDetailPage() {
</div>
</div>
<section className="grid gap-3 xl:grid-cols-4">
<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">Customer</p><div className="mt-2 text-base font-bold text-text">{project.customerName}</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">Owner</p><div className="mt-2 text-base font-bold text-text">{project.ownerName || "Unassigned"}</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">Due Date</p><div className="mt-2 text-base font-bold text-text">{project.dueDate ? new Date(project.dueDate).toLocaleDateString() : "Not set"}</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">Created</p><div className="mt-2 text-base font-bold text-text">{new Date(project.createdAt).toLocaleDateString()}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Customer</p><div className="mt-1 text-base font-bold text-text">{project.customerName}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Owner</p><div className="mt-1 text-base font-bold text-text">{project.ownerName || "Unassigned"}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Due Date</p><div className="mt-1 text-base font-bold text-text">{project.dueDate ? new Date(project.dueDate).toLocaleDateString() : "Not set"}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Created</p><div className="mt-1 text-base font-bold text-text">{new Date(project.createdAt).toLocaleDateString()}</div></article>
</section>
<section className="grid gap-3 xl:grid-cols-4">
<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">Milestones</p><div className="mt-2 text-base font-bold text-text">{project.rollups.completedMilestoneCount}/{project.rollups.milestoneCount}</div><div className="mt-1 text-xs text-muted">{project.rollups.openMilestoneCount} open</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 Milestones</p><div className="mt-2 text-base font-bold text-text">{project.rollups.overdueMilestoneCount}</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="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Milestones</p><div className="mt-1 text-base font-bold text-text">{project.rollups.completedMilestoneCount}/{project.rollups.milestoneCount}</div><div className="mt-1 text-xs text-muted">{project.rollups.openMilestoneCount} open</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Overdue Milestones</p><div className="mt-1 text-base font-bold text-text">{project.rollups.overdueMilestoneCount}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Linked Work Orders</p><div className="mt-1 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="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Overdue Work Orders</p><div className="mt-1 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 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">PROJECT COCKPIT</p>
</div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-right">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Milestone Progress</div>
<div className="surface-panel-tight text-right">
<div className="metric-kicker">Milestone Progress</div>
<div className="mt-1 text-2xl font-bold text-text">{completionPercent}%</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">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">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 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">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>
</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">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">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 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">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Next Checkpoints</p>
<div className="mt-4 space-y-3">
@@ -182,17 +225,16 @@ export function ProjectDetailPage() {
</div>
</section>
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Actionable Cockpit</p>
<p className="mt-2 text-sm text-muted">Turn current exceptions into purchasing, manufacturing, and planning follow-through.</p>
<p className="section-kicker">ACTIONABLE COCKPIT</p>
</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">
Open workbench
</Link>
</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">
<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>
@@ -224,7 +266,7 @@ export function ProjectDetailPage() {
) : null}
</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">
New project work order
</Link>
@@ -238,92 +280,92 @@ export function ProjectDetailPage() {
</Link>
</div>
</article>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<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>
{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>}
<article className="surface-panel">
<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-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 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>
<div className="mt-5 space-y-3">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Risk posture</div><div className={`mt-2 text-lg font-bold ${riskTone}`}>{project.cockpit.risk.riskLevel}</div><div className="mt-1 text-xs text-muted">{project.cockpit.risk.outstandingPurchaseOrderCount} PO(s) still waiting on receipts.</div></div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm text-text">Blocked milestones: <span className="font-semibold">{project.cockpit.risk.blockedMilestoneCount}</span></div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm text-text">Overdue execution items: <span className="font-semibold">{project.cockpit.risk.overdueMilestoneCount + project.cockpit.risk.overdueWorkOrderCount}</span></div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm text-text">Uncovered material quantity: <span className="font-semibold">{project.cockpit.risk.totalUncoveredQuantity}</span></div>
<article className="surface-panel">
<p className="section-kicker">READINESS DRIVERS</p>
<div className="mt-3 space-y-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Risk posture</div><div className={`mt-2 text-lg font-bold ${riskTone}`}>{project.cockpit.risk.riskLevel}</div><div className="mt-1 text-xs text-muted">{project.cockpit.risk.outstandingPurchaseOrderCount} PO(s) still waiting on receipts.</div></div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm text-text">Blocked milestones: <span className="font-semibold">{project.cockpit.risk.blockedMilestoneCount}</span></div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm text-text">Overdue execution items: <span className="font-semibold">{project.cockpit.risk.overdueMilestoneCount + project.cockpit.risk.overdueWorkOrderCount}</span></div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm text-text">Uncovered material quantity: <span className="font-semibold">{project.cockpit.risk.totalUncoveredQuantity}</span></div>
</div>
</article>
</section>
<section 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">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Vendor Exposure</p>
{project.cockpit.purchasing.vendors.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 supplier exposure exists until purchasing is linked.</div> : <div className="mt-5 space-y-3">{project.cockpit.purchasing.vendors.slice(0, 4).map((vendor) => (<div key={vendor.vendorId} className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="flex flex-wrap items-center justify-between gap-3"><div><div className="font-semibold text-text">{vendor.vendorName}</div><div className="mt-1 text-xs text-muted">{vendor.orderCount} linked order(s)</div></div><div className="text-right text-xs text-muted"><div>${vendor.linkedLineValue.toFixed(2)}</div><div>{vendor.outstandingQuantity} outstanding qty</div></div></div></div>))}</div>}
<article className="surface-panel">
<p className="section-kicker">VENDOR EXPOSURE</p>
{project.cockpit.purchasing.vendors.length === 0 ? <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No supplier exposure exists until purchasing is linked.</div> : <div className="mt-3 space-y-2">{project.cockpit.purchasing.vendors.slice(0, 4).map((vendor) => (<div key={vendor.vendorId} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2"><div className="flex flex-wrap items-center justify-between gap-3"><div><div className="font-semibold text-text">{vendor.vendorName}</div><div className="mt-1 text-xs text-muted">{vendor.orderCount} linked order(s)</div></div><div className="text-right text-xs text-muted"><div>${vendor.linkedLineValue.toFixed(2)}</div><div>{vendor.outstandingQuantity} outstanding qty</div></div></div></div>))}</div>}
</article>
<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">Recent Receipts</p>
{project.cockpit.purchasing.recentReceipts.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 receipts have been posted against linked project supply.</div> : <div className="mt-5 space-y-3">{project.cockpit.purchasing.recentReceipts.map((receipt) => (<div key={receipt.receiptId} className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="flex flex-wrap items-start justify-between gap-3"><div><div className="font-semibold text-text">{receipt.receiptNumber}</div><div className="mt-1 text-xs text-muted">{receipt.vendorName} - {receipt.purchaseOrderNumber}</div></div><div className="text-right text-xs text-muted"><div>{new Date(receipt.receivedAt).toLocaleDateString()}</div><div>{receipt.totalQuantity} units received</div></div></div></div>))}</div>}
<article className="surface-panel">
<p className="section-kicker">RECENT RECEIPTS</p>
{project.cockpit.purchasing.recentReceipts.length === 0 ? <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No purchase receipts have been posted against linked project supply.</div> : <div className="mt-3 space-y-2">{project.cockpit.purchasing.recentReceipts.map((receipt) => (<div key={receipt.receiptId} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2"><div className="flex flex-wrap items-start justify-between gap-3"><div><div className="font-semibold text-text">{receipt.receiptNumber}</div><div className="mt-1 text-xs text-muted">{receipt.vendorName} - {receipt.purchaseOrderNumber}</div></div><div className="text-right text-xs text-muted"><div>{new Date(receipt.receivedAt).toLocaleDateString()}</div><div>{receipt.totalQuantity} units received</div></div></div></div>))}</div>}
</article>
</section>
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]">
<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">Customer Linkage</p>
<dl className="mt-5 grid gap-3">
<article className="surface-panel">
<p className="section-kicker">CUSTOMER LINKAGE</p>
<dl className="mt-3 grid gap-3">
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Account</dt><dd className="mt-1 text-sm text-text"><Link to={`/crm/customers/${project.customerId}`} className="hover:text-brand">{project.customerName}</Link></dd></div>
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt><dd className="mt-1 text-sm text-text">{project.customerEmail}</dd></div>
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Phone</dt><dd className="mt-1 text-sm text-text">{project.customerPhone}</dd></div>
</dl>
</article>
<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">Program Notes</p>
<article className="surface-panel">
<p className="section-kicker">PROGRAM NOTES</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{project.notes || "No project notes recorded."}</p>
</article>
</div>
<section 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">Commercial + Delivery Links</p>
<div className="mt-5 grid gap-3 xl:grid-cols-3">
<section className="surface-panel">
<p className="section-kicker">COMMERCIAL + DELIVERY LINKS</p>
<div className="mt-3 grid gap-3 xl:grid-cols-3">
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quote</div><div className="mt-2 font-semibold text-text">{project.salesQuoteNumber ? <Link to={`/sales/quotes/${project.salesQuoteId}`} className="hover:text-brand">{project.salesQuoteNumber}</Link> : "Not linked"}</div></div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Sales Order</div><div className="mt-2 font-semibold text-text">{project.salesOrderNumber ? <Link to={`/sales/orders/${project.salesOrderId}`} className="hover:text-brand">{project.salesOrderNumber}</Link> : "Not linked"}</div></div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Shipment</div><div className="mt-2 font-semibold text-text">{project.shipmentNumber ? <Link to={`/shipping/shipments/${project.shipmentId}`} className="hover:text-brand">{project.shipmentNumber}</Link> : "Not linked"}</div></div>
</div>
</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><p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Milestones</p><p className="mt-2 text-sm text-muted">Track project checkpoints, blockers, and completion progress.</p></div>
<div><p className="section-kicker">MILESTONES</p></div>
{canManage ? <Link to={`/projects/${project.id}/edit`} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Edit milestones</Link> : null}
</div>
{project.milestones.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 milestones are defined for this project yet.</div> : <div className="mt-6 space-y-3">{project.milestones.map((milestone) => (<div key={milestone.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="flex flex-wrap items-start justify-between gap-3"><div className="min-w-0"><div className="font-semibold text-text">{milestone.title}</div><div className="mt-2 flex flex-wrap items-center gap-2"><span className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${projectMilestoneStatusPalette[milestone.status]}`}>{milestone.status.replace("_", " ")}</span><span className="text-xs text-muted">Due {milestone.dueDate ? new Date(milestone.dueDate).toLocaleDateString() : "not scheduled"}</span>{milestone.completedAt ? <span className="text-xs text-muted">Completed {new Date(milestone.completedAt).toLocaleDateString()}</span> : null}</div>{milestone.notes ? <div className="mt-3 whitespace-pre-line text-sm text-text">{milestone.notes}</div> : null}</div></div></div>))}</div>}
{project.milestones.length === 0 ? <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No milestones are defined for this project yet.</div> : <div className="mt-3 space-y-2">{project.milestones.map((milestone) => (<div key={milestone.id} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2"><div className="flex flex-wrap items-start justify-between gap-3"><div className="min-w-0"><div className="font-semibold text-text">{milestone.title}</div><div className="mt-2 flex flex-wrap items-center gap-2"><span className={`inline-flex rounded-full px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${projectMilestoneStatusPalette[milestone.status]}`}>{milestone.status.replace("_", " ")}</span><span className="text-xs text-muted">Due {milestone.dueDate ? new Date(milestone.dueDate).toLocaleDateString() : "not scheduled"}</span>{milestone.completedAt ? <span className="text-xs text-muted">Completed {new Date(milestone.completedAt).toLocaleDateString()}</span> : null}</div>{milestone.notes ? <div className="mt-3 whitespace-pre-line text-sm text-text">{milestone.notes}</div> : null}</div>{canManage ? <div className="flex flex-wrap gap-2">{milestoneQuickActions(milestone.status).map((action) => (<button key={action.status} type="button" onClick={() => void updateMilestoneStatus(milestone.id, action.status)} disabled={updatingMilestoneId === milestone.id} className="rounded-2xl border border-line/70 px-2 py-2 text-xs font-semibold text-text disabled:cursor-not-allowed disabled:opacity-60">{updatingMilestoneId === milestone.id ? "Saving..." : action.label}</button>))}</div> : null}</div></div>))}</div>}
</section>
{planning ? (
<section 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">Material Readiness</p>
<div className="mt-5 grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Qty</p><div className="mt-2 text-base font-bold text-text">{planning.summary.totalBuildQuantity}</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Buy Qty</p><div className="mt-2 text-base font-bold text-text">{planning.summary.totalPurchaseQuantity}</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Uncovered Qty</p><div className="mt-2 text-base font-bold text-text">{planning.summary.totalUncoveredQuantity}</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Shortage Items</p><div className="mt-2 text-base font-bold text-text">{planning.summary.uncoveredItemCount}</div></article>
<section className="surface-panel">
<p className="section-kicker">MATERIAL READINESS</p>
<div className="mt-3 grid gap-3 xl:grid-cols-4">
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Qty</p><div className="mt-1 text-base font-bold text-text">{planning.summary.totalBuildQuantity}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Buy Qty</p><div className="mt-1 text-base font-bold text-text">{planning.summary.totalPurchaseQuantity}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Uncovered Qty</p><div className="mt-1 text-base font-bold text-text">{planning.summary.totalUncoveredQuantity}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Shortage Items</p><div className="mt-1 text-base font-bold text-text">{planning.summary.uncoveredItemCount}</div></article>
</div>
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{planning.items.filter((item) => item.recommendedBuildQuantity > 0 || item.recommendedPurchaseQuantity > 0 || item.uncoveredQuantity > 0).slice(0, 8).map((item) => (
<div key={item.itemId} className="rounded-[18px] border border-line/70 bg-page/60 p-3"><div className="flex flex-wrap items-center justify-between gap-3"><div><div className="font-semibold text-text">{item.itemSku}</div><div className="mt-1 text-xs text-muted">{item.itemName}</div></div><div className="text-sm text-muted">Build {item.recommendedBuildQuantity} - Buy {item.recommendedPurchaseQuantity} - Uncovered {item.uncoveredQuantity}</div></div></div>
<div key={item.itemId} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2"><div className="flex flex-wrap items-center justify-between gap-3"><div><div className="font-semibold text-text">{item.itemSku}</div><div className="mt-1 text-xs text-muted">{item.itemName}</div></div><div className="text-sm text-muted">Build {item.recommendedBuildQuantity} - Buy {item.recommendedPurchaseQuantity} - Uncovered {item.uncoveredQuantity}</div></div></div>
))}
</div>
</section>
) : null}
<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><p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing Links</p><p className="mt-2 text-sm text-muted">Work orders already linked to this project.</p></div>
<div><p className="section-kicker">MANUFACTURING LINKS</p></div>
{canManage ? <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 work order</Link> : null}
</div>
{workOrders.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 work orders are linked to this project yet.</div> : <div className="mt-6 space-y-3">{workOrders.map((workOrder) => (<Link key={workOrder.id} to={`/manufacturing/work-orders/${workOrder.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-center justify-between gap-3"><div><div className="font-semibold text-text">{workOrder.workOrderNumber}</div><div className="mt-1 text-xs text-muted">{workOrder.itemSku} - {workOrder.completedQuantity}/{workOrder.quantity} complete</div></div><div className="text-sm font-semibold text-text">{workOrder.status.replace("_", " ")}</div></div></Link>))}</div>}
{workOrders.length === 0 ? <div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No work orders are linked to this project yet.</div> : <div className="mt-3 space-y-2">{workOrders.map((workOrder) => (<Link key={workOrder.id} to={`/manufacturing/work-orders/${workOrder.id}`} className="block rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 transition hover:bg-page/80"><div className="flex flex-wrap items-center justify-between gap-3"><div><div className="font-semibold text-text">{workOrder.workOrderNumber}</div><div className="mt-1 text-xs text-muted">{workOrder.itemSku} - {workOrder.completedQuantity}/{workOrder.quantity} complete</div></div><div className="text-sm font-semibold text-text">{workOrder.status.replace("_", " ")}</div></div></Link>))}</div>}
</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><p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Activity Timeline</p><p className="mt-2 text-sm text-muted">Chronological project, milestone, purchasing, manufacturing, sales, and shipping history.</p></div>
<div><p className="section-kicker">ACTIVITY TIMELINE</p></div>
</div>
{project.timeline.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 timeline activity is available for this project yet.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No timeline activity is available for this project yet.</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{project.timeline.map((entry) => (
<div key={entry.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div key={entry.id} className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{entry.sourceType}</div>

View File

@@ -235,20 +235,19 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Projects Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{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>
<p className="section-kicker">PROJECTS EDITOR</p>
<h3 className="module-title">{mode === "create" ? "New Project" : "Edit Project"}</h3>
</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">
Cancel
</Link>
</div>
</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">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Project name</span>

View File

@@ -42,13 +42,12 @@ export function ProjectListPage() {
}, [priorityFilter, query, statusFilter, token]);
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Projects</p>
<h3 className="mt-2 text-xl font-bold text-text">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>
<p className="section-kicker">PROJECTS</p>
<h3 className="module-title">PROGRAM RECORDS</h3>
</div>
{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">
@@ -57,8 +56,8 @@ export function ProjectListPage() {
) : null}
</div>
</div>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.2fr)_0.45fr_0.45fr]">
<section className="surface-panel">
<div className="grid gap-2.5 xl:grid-cols-[minmax(0,1.2fr)_0.45fr_0.45fr]">
<label className="block">
<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" />
@@ -76,11 +75,11 @@ export function ProjectListPage() {
</select>
</label>
</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 ? (
<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">
<thead className="bg-page/80 text-left text-muted">
<tr>

View File

@@ -292,11 +292,11 @@ export function PurchaseDetailPage() {
return (
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchase Order</p>
<h3 className="mt-2 text-xl font-bold text-text">{activeDocument.documentNumber}</h3>
<p className="section-kicker">PURCHASE ORDER</p>
<h3 className="module-title">{activeDocument.documentNumber}</h3>
<p className="mt-1 text-sm text-text">{activeDocument.vendorName}</p>
<div className="mt-3 flex flex-wrap gap-2">
<PurchaseStatusBadge status={activeDocument.status} />
@@ -326,11 +326,10 @@ export function PurchaseDetailPage() {
</div>
</div>
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
<p className="mt-2 text-sm text-muted">Update purchase-order status without opening the full editor.</p>
<p className="section-kicker">QUICK ACTIONS</p>
</div>
<div className="flex flex-wrap gap-2">
{purchaseStatusOptions.map((option) => (
@@ -342,33 +341,32 @@ export function PurchaseDetailPage() {
</div>
</section>
) : null}
<section className="grid gap-3 xl:grid-cols-4">
<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">Issue Date</p><div className="mt-2 text-base font-bold text-text">{new Date(activeDocument.issueDate).toLocaleDateString()}</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">Lines</p><div className="mt-2 text-base font-bold text-text">{activeDocument.lineCount}</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">Receipts</p><div className="mt-2 text-base font-bold text-text">{activeDocument.receipts.length}</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">Qty Remaining</p><div className="mt-2 text-base font-bold text-text">{activeDocument.lines.reduce((sum, line) => sum + line.remainingQuantity, 0)}</div></article>
<section className="grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Issue Date</p><div className="mt-2 text-base font-bold text-text">{new Date(activeDocument.issueDate).toLocaleDateString()}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Lines</p><div className="mt-2 text-base font-bold text-text">{activeDocument.lineCount}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Receipts</p><div className="mt-2 text-base font-bold text-text">{activeDocument.receipts.length}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Qty Remaining</p><div className="mt-2 text-base font-bold text-text">{activeDocument.lines.reduce((sum, line) => sum + line.remainingQuantity, 0)}</div></article>
</section>
<section className="grid gap-3 xl:grid-cols-4">
<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">Subtotal</p><div className="mt-2 text-base font-bold text-text">${activeDocument.subtotal.toFixed(2)}</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">Total</p><div className="mt-2 text-base font-bold text-text">${activeDocument.total.toFixed(2)}</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">Tax</p><div className="mt-2 text-base font-bold text-text">${activeDocument.taxAmount.toFixed(2)}</div><div className="mt-1 text-xs text-muted">{activeDocument.taxPercent.toFixed(2)}%</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">Freight</p><div className="mt-2 text-base font-bold text-text">${activeDocument.freightAmount.toFixed(2)}</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>
<section className="grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Subtotal</p><div className="mt-2 text-base font-bold text-text">${activeDocument.subtotal.toFixed(2)}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Total</p><div className="mt-2 text-base font-bold text-text">${activeDocument.total.toFixed(2)}</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Tax</p><div className="mt-2 text-base font-bold text-text">${activeDocument.taxAmount.toFixed(2)}</div><div className="mt-1 text-xs text-muted">{activeDocument.taxPercent.toFixed(2)}%</div></article>
<article className="surface-panel-tight"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Freight</p><div className="mt-2 text-base font-bold text-text">${activeDocument.freightAmount.toFixed(2)}</div></article>
<article className="surface-panel-tight"><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="surface-panel-tight"><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 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">REVISION HISTORY</p>
</div>
</div>
{activeDocument.revisions.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 revisions have been recorded yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No revisions recorded yet.
</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{activeDocument.revisions.map((revision) => (
<article key={revision.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-wrap items-start justify-between gap-3">
@@ -419,37 +417,37 @@ export function PurchaseDetailPage() {
/>
) : null}
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]">
<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">Vendor</p>
<article className="surface-panel">
<p className="section-kicker">VENDOR</p>
<dl className="mt-5 grid gap-3">
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Account</dt><dd className="mt-1 text-sm text-text"><Link to={`/crm/vendors/${activeDocument.vendorId}`} className="hover:text-brand">{activeDocument.vendorName}</Link></dd></div>
<div><dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt><dd className="mt-1 text-sm text-text">{activeDocument.vendorEmail}</dd></div>
</dl>
</article>
<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">Project Link</p>
<article className="surface-panel">
<p className="section-kicker">PROJECT LINK</p>
{activeDocument.projectId ? (
<div className="mt-3 space-y-2">
<Link to={`/projects/${activeDocument.projectId}`} className="inline-flex items-center rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text hover:bg-page/70">
{activeDocument.projectNumber} / {activeDocument.projectName}
</Link>
<p className="text-sm text-muted">This purchase order is linked to the project context used by project cockpit and downstream rollups.</p>
<p className="text-sm text-muted">Project cockpit and rollups use this linkage.</p>
</div>
) : (
<p className="mt-3 text-sm text-muted">No linked project is currently attached to this purchase order.</p>
<p className="mt-3 text-sm text-muted">No linked project.</p>
)}
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-4 text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{activeDocument.notes || "No notes recorded for this document."}</p>
</article>
</div>
<section 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">Demand Context</p>
<section className="surface-panel">
<p className="section-kicker">DEMAND CONTEXT</p>
{demandContextItems.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 active shared shortage or buy-signal records currently point at items on this purchase order.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No active shortage or buy-signal context for these items.
</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{demandContextItems.map((item) => (
<div key={item.itemId} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -466,12 +464,12 @@ export function PurchaseDetailPage() {
</div>
)}
</section>
<section 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">Line Items</p>
<section className="surface-panel">
<p className="section-kicker">LINE ITEMS</p>
{activeDocument.lines.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 line items have been added yet.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No line items added yet.</div>
) : (
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
<div className="mt-3 overflow-hidden rounded-2xl border border-line/70">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead className="bg-page/80 text-left text-muted">
<tr><th className="px-2 py-2">Item</th><th className="px-2 py-2">Description</th><th className="px-2 py-2">Demand Source</th><th className="px-2 py-2">Ordered</th><th className="px-2 py-2">Received</th><th className="px-2 py-2">Remaining</th><th className="px-2 py-2">UOM</th><th className="px-2 py-2">Unit Cost</th><th className="px-2 py-2">Total</th></tr>
@@ -499,16 +497,14 @@ export function PurchaseDetailPage() {
</section>
<section className="grid gap-3 2xl:grid-cols-[minmax(360px,0.82fr)_minmax(0,1.18fr)]">
{canReceive ? (
<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">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>
<article className="surface-panel">
<p className="section-kicker">PURCHASE RECEIVING</p>
{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">
All ordered quantities have been received for this purchase order.
</div>
) : (
<form className="mt-5 space-y-4" onSubmit={handleReceiptSubmit}>
<form className="mt-3 space-y-3" onSubmit={handleReceiptSubmit}>
<div className="grid gap-3 xl:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Receipt date</span>
@@ -580,7 +576,7 @@ export function PurchaseDetailPage() {
</div>
))}
</div>
<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 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{receiptStatus}</span>
<button
type="submit"
@@ -594,15 +590,14 @@ export function PurchaseDetailPage() {
)}
</article>
) : null}
<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">Receipt History</p>
<h4 className="mt-2 text-lg font-bold text-text">Received material log</h4>
<article className="surface-panel">
<p className="section-kicker">RECEIPT HISTORY</p>
{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">
No purchase receipts have been recorded for this order yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No purchase receipts recorded yet.
</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{activeDocument.receipts.map((receipt) => (
<article key={receipt.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">

View File

@@ -1,6 +1,6 @@
import type { InventoryItemOptionDto, PurchaseLineInput, PurchaseOrderInput, PurchaseVendorOptionDto, SalesOrderPlanningDto, SalesOrderPlanningNodeDto } from "@mrp/shared";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
@@ -263,20 +263,24 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
return vendor.name.toLowerCase().includes(query) || vendor.email.toLowerCase().includes(query);
}).length;
function closeEditor() {
navigate(mode === "create" ? "/purchasing/orders" : `/purchasing/orders/${orderId}`);
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchasing Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Purchase Order" : "Edit Purchase Order"}</h3>
<p className="section-kicker">PURCHASING EDITOR</p>
<h3 className="module-title">{mode === "create" ? "New Purchase Order" : "Edit Purchase Order"}</h3>
</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">
<button type="button" onClick={closeEditor} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Cancel
</Link>
</button>
</div>
</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">
<label className="block xl:col-span-2">
<span className="mb-2 block text-sm font-semibold text-text">Vendor</span>
@@ -352,18 +356,13 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
<input type="date" value={form.issueDate.slice(0, 10)} onChange={(event) => updateField("issueDate", new Date(event.target.value).toISOString())} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 px-3 py-3 text-sm">
<div className="rounded-[18px] border border-line/70 bg-page/60 px-2 py-2 text-sm">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Linked Project</div>
<div className="mt-2 font-semibold text-text">
{mode === "edit"
? (form.projectId ? "Project context saved on this purchase order." : "No project linked.")
: (seededProjectId ? `${seededProjectNumber || "Project"}${seededProjectName ? ` - ${seededProjectName}` : ""}` : "Will auto-link from sales-order demand when possible.")}
</div>
<div className="mt-1 text-xs text-muted">
{mode === "edit"
? "This header link is used for downstream project cockpit and finance rollups."
: "Generated purchasing from a project-linked sales order will carry project context automatically."}
</div>
</div>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Notes</span>
@@ -391,18 +390,18 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
</label>
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Line Items</p>
<h4 className="mt-2 text-lg font-bold text-text">Procurement lines</h4>
<p className="section-kicker">LINE ITEMS</p>
<h4 className="text-lg font-bold text-text">PROCUREMENT LINES</h4>
</div>
<button type="button" onClick={addLine} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">Add line</button>
</div>
{form.lines.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 line items added yet.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No line items added yet.</div>
) : (
<div className="mt-5 space-y-4">
<div className="mt-3 space-y-3">
{form.lines.map((line: PurchaseLineInput, index: number) => (
<div key={index} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="grid gap-3 xl:grid-cols-[1.15fr_1.25fr_0.5fr_0.55fr_0.7fr_0.75fr_auto]">
@@ -478,13 +477,13 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
))}
</div>
)}
<div className="mt-5 grid gap-3 md:grid-cols-3 xl:grid-cols-4">
<div className="mt-4 grid gap-2 md:grid-cols-3 xl:grid-cols-4">
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Subtotal</div><div className="mt-1 font-semibold text-text">${subtotal.toFixed(2)}</div></div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Tax</div><div className="mt-1 font-semibold text-text">${taxAmount.toFixed(2)}</div></div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Freight</div><div className="mt-1 font-semibold text-text">${form.freightAmount.toFixed(2)}</div></div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Total</div><div className="mt-1 font-semibold text-text">${total.toFixed(2)}</div></div>
</div>
<div className="mt-6 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="mt-4 flex flex-col gap-2 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span>
<button type="submit" disabled={isSaving} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isSaving ? "Saving..." : mode === "create" ? "Create purchase order" : "Save changes"}

View File

@@ -34,12 +34,11 @@ export function PurchaseListPage() {
}, [searchTerm, statusFilter, token]);
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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Purchasing</p>
<h3 className="mt-2 text-lg font-bold text-text">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>
<p className="section-kicker">PURCHASING</p>
<h3 className="module-title">PURCHASE ORDERS</h3>
</div>
{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">
@@ -47,7 +46,7 @@ export function PurchaseListPage() {
</Link>
) : null}
</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">
<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" />
@@ -63,11 +62,11 @@ export function PurchaseListPage() {
</select>
</label>
</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 ? (
<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">
<thead className="bg-page/80 text-left text-muted">
<tr>

View File

@@ -334,12 +334,12 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
}
return (
<section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<section className="page-stack">
<div className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{config.detailEyebrow}</p>
<h3 className="mt-2 text-xl font-bold text-text">{activeDocument.documentNumber}</h3>
<p className="section-kicker">{config.detailEyebrow.toUpperCase()}</p>
<h3 className="module-title">{activeDocument.documentNumber}</h3>
<p className="mt-1 text-sm text-text">{activeDocument.customerName}</p>
<div className="mt-3 flex flex-wrap gap-2">
<SalesStatusBadge status={activeDocument.status} />
@@ -396,11 +396,10 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
</div>
</div>
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
<p className="mt-2 text-sm text-muted">Update document status without opening the full editor.</p>
<p className="section-kicker">QUICK ACTIONS</p>
</div>
<div className="flex flex-wrap gap-2">
{salesStatusOptions.map((option) => (
@@ -418,58 +417,57 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
</div>
</section>
) : null}
<section className="grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<section className="grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Issue Date</p>
<div className="mt-2 text-base font-bold text-text">{new Date(activeDocument.issueDate).toLocaleDateString()}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Expires</p>
<div className="mt-2 text-base font-bold text-text">{activeDocument.expiresAt ? new Date(activeDocument.expiresAt).toLocaleDateString() : "N/A"}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Lines</p>
<div className="mt-2 text-base font-bold text-text">{activeDocument.lineCount}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Approval</p>
<div className="mt-2 text-base font-bold text-text">{activeDocument.approvedAt ? new Date(activeDocument.approvedAt).toLocaleDateString() : "Pending"}</div>
<div className="mt-1 text-xs text-muted">{activeDocument.approvedByName ?? "No approver recorded"}</div>
</article>
</section>
<section className="grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<section className="grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Discount</p>
<div className="mt-2 text-base font-bold text-text">-${activeDocument.discountAmount.toFixed(2)}</div>
<div className="mt-1 text-xs text-muted">{activeDocument.discountPercent.toFixed(2)}%</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Tax</p>
<div className="mt-2 text-base font-bold text-text">${activeDocument.taxAmount.toFixed(2)}</div>
<div className="mt-1 text-xs text-muted">{activeDocument.taxPercent.toFixed(2)}%</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Freight</p>
<div className="mt-2 text-base font-bold text-text">${activeDocument.freightAmount.toFixed(2)}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Total</p>
<div className="mt-2 text-base font-bold text-text">${activeDocument.total.toFixed(2)}</div>
</article>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">REVISION HISTORY</p>
</div>
</div>
{activeDocument.revisions.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 revisions have been recorded yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No revisions recorded yet.
</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{activeDocument.revisions.map((revision) => (
<article key={revision.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-wrap items-start justify-between gap-3">
@@ -519,8 +517,8 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
/>
) : null}
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]">
<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">Customer</p>
<article className="surface-panel">
<p className="section-kicker">CUSTOMER</p>
<dl className="mt-5 grid gap-3">
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Account</dt>
@@ -532,30 +530,30 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
</div>
</dl>
</article>
<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">Project Link</p>
<article className="surface-panel">
<p className="section-kicker">PROJECT LINK</p>
{activeDocument.linkedProjectId ? (
<div className="mt-3 space-y-2">
<Link to={`/projects/${activeDocument.linkedProjectId}`} className="inline-flex items-center rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text hover:bg-page/70">
{activeDocument.linkedProjectNumber} / {activeDocument.linkedProjectName}
</Link>
<p className="text-sm text-muted">This {entity === "quote" ? "quote" : "sales order"} is already linked to a project, and downstream WO/PO launches will carry that project context.</p>
<p className="text-sm text-muted">Downstream WO and PO launches carry this project context.</p>
</div>
) : (
<p className="mt-3 text-sm text-muted">No linked project is currently attached to this {entity === "quote" ? "quote" : "sales order"}.</p>
<p className="mt-3 text-sm text-muted">No linked project.</p>
)}
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-4 text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{activeDocument.notes || "No notes recorded for this document."}</p>
</article>
</div>
<section 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">Line Items</p>
<section className="surface-panel">
<p className="section-kicker">LINE ITEMS</p>
{activeDocument.lines.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 line items have been added yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No line items added yet.
</div>
) : (
<div className="mt-6 overflow-hidden rounded-2xl border border-line/70">
<div className="mt-3 overflow-hidden rounded-2xl border border-line/70">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead className="bg-page/80 text-left text-muted">
<tr>
@@ -587,37 +585,33 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
)}
</section>
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">DEMAND PLANNING</p>
</div>
<div className="text-right text-xs text-muted">
<div>Generated {new Date(planning.generatedAt).toLocaleString()}</div>
<div>Status {planning.status}</div>
</div>
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-page/70 px-3 py-3">
<div className="mt-4 grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight bg-page/70 shadow-none">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Recommendations</p>
<div className="mt-2 text-base font-bold text-text">{planning.summary.totalBuildQuantity}</div>
<div className="mt-1 text-xs text-muted">{planning.summary.buildRecommendationCount} items</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/70 px-3 py-3">
<article className="surface-panel-tight bg-page/70 shadow-none">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Purchase Recommendations</p>
<div className="mt-2 text-base font-bold text-text">{planning.summary.totalPurchaseQuantity}</div>
<div className="mt-1 text-xs text-muted">{planning.summary.purchaseRecommendationCount} items</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/70 px-3 py-3">
<article className="surface-panel-tight bg-page/70 shadow-none">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Uncovered</p>
<div className="mt-2 text-base font-bold text-text">{planning.summary.totalUncoveredQuantity}</div>
<div className="mt-1 text-xs text-muted">{planning.summary.uncoveredItemCount} items</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/70 px-3 py-3">
<article className="surface-panel-tight bg-page/70 shadow-none">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Planned Items</p>
<div className="mt-2 text-base font-bold text-text">{planning.summary.itemCount}</div>
<div className="mt-1 text-xs text-muted">{planning.summary.lineCount} sales lines</div>
@@ -688,7 +682,7 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
</Link>
</div>
) : null}
<div className="mt-5 space-y-3">
<div className="mt-4 space-y-2">
{planning.lines.map((line) => (
<div key={line.lineId} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="mb-3">
@@ -706,11 +700,10 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
</section>
) : null}
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping</p>
<p className="mt-2 text-sm text-muted">Shipment records currently tied to this sales order.</p>
<p className="section-kicker">SHIPPING</p>
</div>
{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">
@@ -719,11 +712,11 @@ export function SalesDetailPage({ entity }: { entity: SalesDocumentEntity }) {
) : null}
</div>
{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 created for this sales order yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No shipments created yet.
</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{shipments.map((shipment) => (
<Link key={shipment.id} to={`/shipping/shipments/${shipment.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-center justify-between gap-3">

View File

@@ -1,7 +1,7 @@
import type { InventoryItemOptionDto } from "@mrp/shared/dist/inventory/types.js";
import type { SalesCustomerOptionDto, SalesDocumentDetailDto, SalesDocumentInput, SalesLineInput } from "@mrp/shared/dist/sales/types.js";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
@@ -167,20 +167,24 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
}
}
function closeEditor() {
navigate(mode === "create" ? config.routeBase : `${config.routeBase}/${documentId}`);
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{config.detailEyebrow} Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}</h3>
<p className="section-kicker">{`${config.detailEyebrow} EDITOR`.toUpperCase()}</p>
<h3 className="module-title">{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}</h3>
</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">
<button type="button" onClick={closeEditor} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Cancel
</Link>
</button>
</div>
</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">
<label className="block xl:col-span-2">
<span className="mb-2 block text-sm font-semibold text-text">Customer</span>
@@ -351,22 +355,22 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
</label>
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Line Items</p>
<h4 className="mt-2 text-lg font-bold text-text">Commercial lines</h4>
<p className="section-kicker">LINE ITEMS</p>
<h4 className="text-lg font-bold text-text">COMMERCIAL LINES</h4>
</div>
<button type="button" onClick={addLine} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Add line
</button>
</div>
{form.lines.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-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No line items added yet.
</div>
) : (
<div className="mt-5 space-y-4">
<div className="mt-3 space-y-3">
{form.lines.map((line: SalesLineInput, index: number) => (
<div key={index} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="grid gap-3 xl:grid-cols-[1.15fr_1.25fr_0.5fr_0.55fr_0.7fr_0.75fr_auto]">
@@ -451,7 +455,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
))}
</div>
)}
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="mt-4 grid gap-2 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Subtotal</div>
<div className="mt-1 font-semibold text-text">${subtotal.toFixed(2)}</div>
@@ -469,7 +473,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
<div className="mt-1 font-semibold text-text">${total.toFixed(2)}</div>
</div>
</div>
<div className="mt-6 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="mt-4 flex flex-col gap-2 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span>
<button type="submit" disabled={isSaving} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isSaving ? "Saving..." : mode === "create" ? `Create ${config.singularLabel.toLowerCase()}` : "Save changes"}

View File

@@ -40,14 +40,11 @@ export function SalesListPage({ entity }: { entity: SalesDocumentEntity }) {
}, [config.collectionLabel, entity, searchTerm, statusFilter, token]);
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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{config.listEyebrow}</p>
<h3 className="mt-2 text-lg font-bold text-text">{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>
<p className="section-kicker">{config.listEyebrow.toUpperCase()}</p>
<h3 className="module-title">{config.collectionLabel}</h3>
</div>
{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">
@@ -55,7 +52,7 @@ export function SalesListPage({ entity }: { entity: SalesDocumentEntity }) {
</Link>
) : null}
</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">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input
@@ -80,13 +77,13 @@ export function SalesListPage({ entity }: { entity: SalesDocumentEntity }) {
</select>
</label>
</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 ? (
<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.
</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">
<thead className="bg-page/80 text-left text-muted">
<tr>

View File

@@ -74,7 +74,7 @@ export function AdminDiagnosticsPage() {
}, [token, supportLogLevel, supportLogSource, supportLogQuery, supportLogWindowDays]);
if (!diagnostics || !backupGuidance) {
return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
return <div className="surface-panel text-sm text-muted">{status}</div>;
}
async function handleExportSupportSnapshot() {
@@ -156,15 +156,12 @@ export function AdminDiagnosticsPage() {
];
return (
<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">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="page-stack">
<section className="surface-panel backdrop-blur">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Admin Diagnostics</p>
<h3 className="mt-2 text-lg font-bold text-text">Operational runtime and audit visibility</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>
<p className="section-kicker">ADMIN DIAGNOSTICS</p>
<h3 className="module-title">RUNTIME AUDIT SUPPORT</h3>
</div>
<div className="flex flex-wrap gap-3">
<button
@@ -189,35 +186,31 @@ export function AdminDiagnosticsPage() {
</Link>
</div>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
{summaryCards.map(([label, value]) => (
<div key={label} className="rounded-[18px] border border-line/70 bg-page/70 p-4">
<div key={label} className="rounded-[18px] border border-line/70 bg-page/70 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted">{label}</p>
<p className="mt-3 text-lg font-bold text-text">{value}</p>
<p className="mt-2 text-lg font-bold text-text">{value}</p>
</div>
))}
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">BACKUP AND RESTORE</p>
</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-2 py-2 text-sm text-muted">
<div>Data: {backupGuidance.dataPath}</div>
<div>DB: {backupGuidance.databasePath}</div>
<div>Uploads: {backupGuidance.uploadsPath}</div>
</div>
</div>
<div className="mt-5 grid gap-4 xl:grid-cols-2">
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
<div className="mt-3 grid gap-3 xl:grid-cols-2">
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-sm font-semibold text-text">Backup checklist</p>
<div className="mt-3 space-y-3">
<div className="mt-3 space-y-2">
{backupGuidance.backupSteps.map((step) => (
<div key={step.id}>
<p className="text-sm font-semibold text-text">{step.label}</p>
@@ -226,9 +219,9 @@ export function AdminDiagnosticsPage() {
))}
</div>
</div>
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-sm font-semibold text-text">Restore checklist</p>
<div className="mt-3 space-y-3">
<div className="mt-3 space-y-2">
{backupGuidance.restoreSteps.map((step) => (
<div key={step.id}>
<p className="text-sm font-semibold text-text">{step.label}</p>
@@ -238,10 +231,10 @@ export function AdminDiagnosticsPage() {
</div>
</div>
</div>
<div className="mt-5 grid gap-4 xl:grid-cols-2">
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
<div className="mt-3 grid gap-3 xl:grid-cols-2">
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-sm font-semibold text-text">Backup verification checklist</p>
<div className="mt-3 space-y-3">
<div className="mt-3 space-y-2">
{backupGuidance.verificationChecklist.map((item) => (
<div key={item.id}>
<p className="text-sm font-semibold text-text">{item.label}</p>
@@ -251,9 +244,9 @@ export function AdminDiagnosticsPage() {
))}
</div>
</div>
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-sm font-semibold text-text">Restore drill runbook</p>
<div className="mt-3 space-y-3">
<div className="mt-3 space-y-2">
{backupGuidance.restoreDrillSteps.map((step) => (
<div key={step.id}>
<p className="text-sm font-semibold text-text">{step.label}</p>
@@ -266,17 +259,16 @@ export function AdminDiagnosticsPage() {
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Startup Validation</p>
<h3 className="mt-2 text-lg font-bold text-text">Boot-time readiness checks</h3>
<p className="section-kicker">STARTUP VALIDATION</p>
</div>
<span className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${startupStatusTone}`}>
{diagnostics.startup.status}
</span>
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-2">
<div className="mt-3 grid gap-3 xl:grid-cols-2">
{diagnostics.startup.checks.map((check) => (
<div key={check.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<div className="flex items-center justify-between gap-3">
@@ -287,7 +279,7 @@ export function AdminDiagnosticsPage() {
</div>
))}
</div>
<div className="mt-5 grid gap-3 lg:grid-cols-3">
<div className="mt-3 grid gap-3 lg:grid-cols-3">
{startupSummaryCards.map(([label, value]) => (
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
@@ -297,9 +289,9 @@ export function AdminDiagnosticsPage() {
</div>
</section>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">System Footprint</p>
<div className="mt-5 grid gap-3 xl:grid-cols-2">
<section className="surface-panel backdrop-blur">
<p className="section-kicker">SYSTEM FOOTPRINT</p>
<div className="mt-3 grid gap-3 xl:grid-cols-2">
{footprintCards.map(([label, value]) => (
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
@@ -309,19 +301,18 @@ export function AdminDiagnosticsPage() {
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Support Logs</p>
<h3 className="mt-2 text-lg font-bold text-text">Recent runtime warnings and failures</h3>
<p className="section-kicker">SUPPORT LOGS</p>
</div>
<p className="text-sm text-muted">
{supportLogSummary ? `${supportLogSummary.filteredCount} of ${supportLogSummary.totalCount} entries` : "No entries loaded"}
</p>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
<div className="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input
value={supportLogQuery}
onChange={(event) => setSupportLogQuery(event.target.value)}
@@ -330,7 +321,7 @@ export function AdminDiagnosticsPage() {
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Level</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Level</span>
<select value={supportLogLevel} onChange={(event) => setSupportLogLevel(event.target.value as "ALL" | SupportLogEntryDto["level"])} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
<option value="ALL">All levels</option>
<option value="ERROR">Error</option>
@@ -339,7 +330,7 @@ export function AdminDiagnosticsPage() {
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Source</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Source</span>
<select value={supportLogSource} onChange={(event) => setSupportLogSource(event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
<option value="ALL">All sources</option>
{supportLogSources.map((source) => (
@@ -348,7 +339,7 @@ export function AdminDiagnosticsPage() {
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Window</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Window</span>
<select value={supportLogWindowDays} onChange={(event) => setSupportLogWindowDays(event.target.value as "ALL" | "1" | "7" | "14")} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
<option value="ALL">All retained</option>
<option value="1">Last 24 hours</option>
@@ -356,13 +347,13 @@ export function AdminDiagnosticsPage() {
<option value="14">Last 14 days</option>
</select>
</label>
<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-2 py-2 text-sm text-muted">
<div>Errors: {supportLogSummary?.levelCounts.ERROR ?? 0}</div>
<div>Warnings: {supportLogSummary?.levelCounts.WARN ?? 0}</div>
<div>Info: {supportLogSummary?.levelCounts.INFO ?? 0}</div>
</div>
</div>
<div className="mt-5 overflow-x-auto">
<div className="mt-3 overflow-x-auto">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead>
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
@@ -404,15 +395,14 @@ export function AdminDiagnosticsPage() {
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Recent Audit Trail</p>
<h3 className="mt-2 text-lg font-bold text-text">Latest cross-module write activity</h3>
<p className="section-kicker">RECENT AUDIT TRAIL</p>
</div>
<p className="text-sm text-muted">{status}</p>
</div>
<div className="mt-5 overflow-x-auto">
<div className="mt-3 overflow-x-auto">
<table className="min-w-full divide-y divide-line/70 text-sm">
<thead>
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">

View File

@@ -93,7 +93,7 @@ export function CompanySettingsPage() {
}, [logoUrl]);
if (!form || !token) {
return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
return <div className="surface-panel text-sm text-muted">{status}</div>;
}
async function handleSave(event: React.FormEvent<HTMLFormElement>) {
@@ -145,14 +145,13 @@ export function CompanySettingsPage() {
}
return (
<form className="space-y-6" onSubmit={handleSave}>
<form className="page-stack" onSubmit={handleSave}>
{user?.permissions.includes("admin.manage") ? (
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Admin</p>
<h3 className="mt-2 text-lg font-bold text-text">Admin access and diagnostics</h3>
<p className="mt-2 text-sm text-muted">Manage users, roles, and system diagnostics from the linked admin surfaces.</p>
<p className="section-kicker">ADMIN</p>
<h3 className="module-title">ADMIN SURFACES</h3>
</div>
<div className="flex flex-wrap gap-3">
<Link to="/settings/users" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
@@ -165,14 +164,13 @@ export function CompanySettingsPage() {
</div>
</section>
) : null}
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Company Profile</p>
<h3 className="mt-2 text-lg font-bold text-text">Branding and legal identity</h3>
<p className="mt-2 max-w-2xl text-sm text-muted">Every internal document and PDF template will inherit its company identity from this profile.</p>
<p className="section-kicker">COMPANY PROFILE</p>
<h3 className="module-title">BRANDING AND LEGAL IDENTITY</h3>
</div>
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/80 p-4">
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/80 px-3 py-3">
{logoUrl ? <img alt="Company logo" className="h-20 w-20 rounded-2xl object-cover" src={logoUrl} /> : <div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-brand text-sm font-bold text-white">LOGO</div>}
<label className="mt-3 block cursor-pointer text-sm font-semibold text-brand">
Upload logo
@@ -180,7 +178,7 @@ export function CompanySettingsPage() {
</label>
</div>
</div>
<div className="mt-6 grid gap-4 xl:grid-cols-2 2xl:grid-cols-3">
<div className="mt-3 grid gap-3 xl:grid-cols-2 2xl:grid-cols-3">
{[
["companyName", "Company name"],
["legalName", "Legal name"],
@@ -196,7 +194,7 @@ export function CompanySettingsPage() {
["country", "Country"],
].map(([key, label]) => (
<label key={key} className="block">
<span className="mb-2 block text-sm font-semibold text-text">{label}</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">{label}</span>
<input
value={String(form[key as keyof CompanyProfileInput])}
onChange={(event) => updateField(key as keyof CompanyProfileInput, event.target.value as never)}
@@ -206,27 +204,27 @@ export function CompanySettingsPage() {
))}
</div>
</section>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Theme</p>
<div className="mt-5 grid gap-4 md:grid-cols-2 2xl:grid-cols-4">
<section className="surface-panel">
<p className="section-kicker">THEME</p>
<div className="mt-3 grid gap-3 md:grid-cols-2 2xl:grid-cols-4">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Primary color</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Primary color</span>
<input type="color" value={form.theme.primaryColor} onChange={(event) => updateField("theme", { ...form.theme, primaryColor: event.target.value })} className="h-10 w-full rounded-2xl border border-line/70 bg-page p-2" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Accent color</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Accent color</span>
<input type="color" value={form.theme.accentColor} onChange={(event) => updateField("theme", { ...form.theme, accentColor: event.target.value })} className="h-10 w-full rounded-2xl border border-line/70 bg-page p-2" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Surface color</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Surface color</span>
<input type="color" value={form.theme.surfaceColor} onChange={(event) => updateField("theme", { ...form.theme, surfaceColor: event.target.value })} className="h-10 w-full rounded-2xl border border-line/70 bg-page p-2" />
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Font family</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Font family</span>
<input value={form.theme.fontFamily} onChange={(event) => updateField("theme", { ...form.theme, fontFamily: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
<div className="mt-5 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 lg:flex-row lg:items-center lg:justify-between">
<div className="mt-3 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 lg:flex-row lg:items-center lg:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span>
<div className="flex flex-wrap gap-3">
<button

View File

@@ -272,15 +272,12 @@ export function UserManagementPage() {
const reviewSessionCount = sessions.filter((session) => session.reviewState === "REVIEW").length;
return (
<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">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="page-stack">
<section className="surface-panel backdrop-blur">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">User Management</p>
<h3 className="mt-2 text-lg font-bold text-text">Accounts, roles, and permission assignment</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>
<p className="section-kicker">ADMIN</p>
<h3 className="module-title">USERS ROLES SESSIONS</h3>
</div>
<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">
@@ -293,12 +290,11 @@ export function UserManagementPage() {
</div>
</section>
<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}>
<section className="grid gap-3 xl:grid-cols-2">
<form className="surface-panel backdrop-blur" onSubmit={handleUserSave}>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Users</p>
<h3 className="mt-2 text-lg font-bold text-text">Account generation and role assignment</h3>
<p className="section-kicker">USERS</p>
</div>
<select
value={selectedUserId}
@@ -314,9 +310,9 @@ export function UserManagementPage() {
</select>
</div>
<div className="mt-5 grid gap-4 md:grid-cols-2">
<div className="mt-3 grid gap-3 md:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Email</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Email</span>
<input
value={userForm.email}
onChange={(event) => setUserForm((current) => ({ ...current, email: event.target.value }))}
@@ -324,7 +320,7 @@ export function UserManagementPage() {
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Password</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Password</span>
<input
type="password"
value={userForm.password ?? ""}
@@ -334,7 +330,7 @@ export function UserManagementPage() {
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">First name</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">First name</span>
<input
value={userForm.firstName}
onChange={(event) => setUserForm((current) => ({ ...current, firstName: event.target.value }))}
@@ -342,7 +338,7 @@ export function UserManagementPage() {
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Last name</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Last name</span>
<input
value={userForm.lastName}
onChange={(event) => setUserForm((current) => ({ ...current, lastName: event.target.value }))}
@@ -351,7 +347,7 @@ export function UserManagementPage() {
</label>
</div>
<label className="mt-4 flex items-center gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
<label className="mt-3 flex items-center gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
<input
type="checkbox"
checked={userForm.isActive}
@@ -360,11 +356,11 @@ export function UserManagementPage() {
User can sign in
</label>
<div className="mt-5">
<p className="text-sm font-semibold text-text">Assigned roles</p>
<div className="mt-3 grid gap-3">
<div className="mt-3">
<p className="section-kicker">ASSIGNED ROLES</p>
<div className="mt-3 grid gap-2">
{roles.map((role) => (
<label key={role.id} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
<label key={role.id} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
<input
type="checkbox"
checked={userForm.roleIds.includes(role.id)}
@@ -379,7 +375,7 @@ export function UserManagementPage() {
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<div className="mt-3 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
<span className="text-sm text-muted">{status}</span>
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
{selectedUserId === "new" ? "Create user" : "Save user"}
@@ -387,11 +383,10 @@ export function UserManagementPage() {
</div>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Roles</p>
<h3 className="mt-2 text-lg font-bold text-text">Permission assignment administration</h3>
<p className="section-kicker">ROLES</p>
</div>
<select
value={selectedRoleId}
@@ -407,9 +402,9 @@ export function UserManagementPage() {
</select>
</div>
<div className="mt-5 grid gap-4">
<div className="mt-3 grid gap-3">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Role name</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Role name</span>
<input
value={roleForm.name}
onChange={(event) => setRoleForm((current) => ({ ...current, name: event.target.value }))}
@@ -417,7 +412,7 @@ export function UserManagementPage() {
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Description</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Description</span>
<textarea
value={roleForm.description}
onChange={(event) => setRoleForm((current) => ({ ...current, description: event.target.value }))}
@@ -427,11 +422,11 @@ export function UserManagementPage() {
</label>
</div>
<div className="mt-5">
<p className="text-sm font-semibold text-text">Role permissions</p>
<div className="mt-3 grid gap-3 md:grid-cols-2">
<div className="mt-3">
<p className="section-kicker">ROLE PERMISSIONS</p>
<div className="mt-3 grid gap-2 md:grid-cols-2">
{permissions.map((permission) => (
<label key={permission.key} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
<label key={permission.key} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
<input
type="checkbox"
checked={roleForm.permissionKeys.includes(permission.key)}
@@ -446,9 +441,9 @@ export function UserManagementPage() {
</div>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-3">
<div className="mt-3 grid gap-2 md:grid-cols-3">
{roles.map((role) => (
<div key={role.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<div key={role.id} className="rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
<p className="text-sm font-semibold text-text">{role.name}</p>
<p className="mt-1 text-xs text-muted">{role.userCount} assigned users</p>
<p className="mt-2 text-xs text-muted">{role.permissionKeys.length} permissions</p>
@@ -456,7 +451,7 @@ export function UserManagementPage() {
))}
</div>
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<div className="mt-3 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
<span className="text-sm text-muted">{status}</span>
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
{selectedRoleId === "new" ? "Create role" : "Save role"}
@@ -465,18 +460,14 @@ export function UserManagementPage() {
</form>
</section>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<section className="surface-panel backdrop-blur">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">SESSIONS</p>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input
value={sessionQuery}
onChange={(event) => setSessionQuery(event.target.value)}
@@ -485,7 +476,7 @@ export function UserManagementPage() {
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">User</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">User</span>
<select
value={sessionUserFilter}
onChange={(event) => setSessionUserFilter(event.target.value)}
@@ -500,7 +491,7 @@ export function UserManagementPage() {
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Status</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Status</span>
<select
value={sessionStatusFilter}
onChange={(event) => setSessionStatusFilter(event.target.value as "ALL" | AdminAuthSessionDto["status"])}
@@ -513,7 +504,7 @@ export function UserManagementPage() {
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Review</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Review</span>
<select
value={sessionReviewFilter}
onChange={(event) => setSessionReviewFilter(event.target.value as "ALL" | AdminAuthSessionDto["reviewState"])}

View File

@@ -218,11 +218,11 @@ export function ShipmentDetailPage() {
return (
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipment</p>
<h3 className="mt-2 text-xl font-bold text-text">{shipment.shipmentNumber}</h3>
<p className="section-kicker">SHIPMENT</p>
<h3 className="module-title">{shipment.shipmentNumber}</h3>
<p className="mt-1 text-sm text-text">{shipment.salesOrderNumber} / {shipment.customerName}</p>
<div className="mt-3 flex flex-wrap items-center gap-3">
<ShipmentStatusBadge status={shipment.status} />
@@ -249,11 +249,10 @@ export function ShipmentDetailPage() {
</div>
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Quick Actions</p>
<p className="mt-2 text-sm text-muted">Use inventory-backed picking before marking the shipment packed or shipped.</p>
<p className="section-kicker">QUICK ACTIONS</p>
</div>
<div className="flex flex-wrap gap-2">
{shipmentStatusOptions.map((option) => (
@@ -266,31 +265,30 @@ export function ShipmentDetailPage() {
</section>
) : null}
<section className="grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<section className="grid gap-2 xl:grid-cols-4">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Carrier</p>
<div className="mt-2 text-base font-bold text-text">{shipment.carrier || "Not set"}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Ordered Units</p>
<div className="mt-2 text-base font-bold text-text">{totalOrderedQuantity}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Picked Units</p>
<div className="mt-2 text-base font-bold text-text">{totalPickedQuantity}</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<article className="surface-panel-tight">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Packages</p>
<div className="mt-2 text-base font-bold text-text">{shipment.packageCount}</div>
</article>
</section>
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipment Lines</p>
<p className="mt-2 text-sm text-muted">Track ordered, picked, and remaining quantity before shipment closeout.</p>
<p className="section-kicker">SHIPMENT LINES</p>
</div>
</div>
<div className="mt-5 overflow-x-auto">
@@ -326,8 +324,8 @@ export function ShipmentDetailPage() {
</div>
</article>
<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">Timing</p>
<article className="surface-panel">
<p className="section-kicker">TIMING</p>
<dl className="mt-5 grid gap-3">
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Ship Date</dt>
@@ -350,16 +348,13 @@ export function ShipmentDetailPage() {
</div>
{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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">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>
<p className="section-kicker">PICK AND ISSUE FROM STOCK</p>
</div>
<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.
<div className="rounded-[16px] border border-line/70 bg-page/60 px-2 py-2 text-xs text-muted">
Select line, location, and quantity.
</div>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
@@ -468,19 +463,18 @@ export function ShipmentDetailPage() {
) : null}
<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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Pick History</p>
<p className="mt-2 text-sm text-muted">Every pick here already issued stock from a specific inventory location.</p>
<p className="section-kicker">PICK HISTORY</p>
</div>
</div>
{shipment.picks.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 shipment picks have been posted yet.
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
No shipment picks posted yet.
</div>
) : (
<div className="mt-5 space-y-3">
<div className="mt-3 space-y-2">
{shipment.picks.map((pick) => (
<div key={pick.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex flex-wrap items-start justify-between gap-3">
@@ -502,26 +496,25 @@ export function ShipmentDetailPage() {
)}
</article>
<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">Shipment Notes</p>
<article className="surface-panel">
<p className="section-kicker">SHIPMENT NOTES</p>
<p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{shipment.notes || "No notes recorded for this shipment."}</p>
</article>
</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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Related Shipments</p>
<p className="mt-2 text-sm text-muted">Other shipments already tied to this sales order.</p>
<p className="section-kicker">RELATED SHIPMENTS</p>
</div>
{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>
) : null}
</div>
{relatedShipments.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 additional shipments exist for this sales order.</div>
<div className="mt-3 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">No additional shipments.</div>
) : (
<div className="mt-6 space-y-3">
<div className="mt-3 space-y-2">
{relatedShipments.map((related) => (
<Link key={related.id} to={`/shipping/shipments/${related.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-center justify-between gap-3">

View File

@@ -1,6 +1,6 @@
import type { ShipmentInput, ShipmentOrderOptionDto } from "@mrp/shared/dist/shipping/types.js";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
@@ -85,20 +85,24 @@ export function ShipmentFormPage({ mode }: { mode: "create" | "edit" }) {
}
}
function closeEditor() {
navigate(mode === "create" ? "/shipping/shipments" : `/shipping/shipments/${shipmentId}`);
}
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<form className="page-stack" onSubmit={handleSubmit}>
<section className="surface-panel">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping Editor</p>
<h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Shipment" : "Edit Shipment"}</h3>
<p className="section-kicker">SHIPPING EDITOR</p>
<h3 className="module-title">{mode === "create" ? "New Shipment" : "Edit Shipment"}</h3>
</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">
<button type="button" onClick={closeEditor} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Cancel
</Link>
</button>
</div>
</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">
<span className="mb-2 block text-sm font-semibold text-text">Sales Order</span>
<div className="relative">

View File

@@ -38,12 +38,11 @@ export function ShipmentListPage() {
}, [searchTerm, statusFilter, token]);
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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping</p>
<h3 className="mt-2 text-lg font-bold text-text">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>
<p className="section-kicker">SHIPPING</p>
<h3 className="module-title">SHIPMENTS</h3>
</div>
{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">
@@ -51,7 +50,7 @@ export function ShipmentListPage() {
</Link>
) : null}
</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">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input
@@ -76,11 +75,11 @@ export function ShipmentListPage() {
</select>
</label>
</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 ? (
<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">
<thead className="bg-page/80 text-left text-muted">
<tr>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
ALTER TABLE "WorkOrder" ADD COLUMN "holdReason" TEXT;

View File

@@ -653,6 +653,7 @@ model WorkOrder {
warehouseId String
locationId String
status String
holdReason String?
quantity Int
completedQuantity Int @default(0)
dueDate DateTime?

View File

@@ -1,8 +1,15 @@
import puppeteer from "puppeteer";
import puppeteer, { PaperFormat } from "puppeteer";
import { env } from "../config/env.js";
export async function renderPdf(html: string) {
interface PdfOptions {
format?: PaperFormat;
width?: string;
height?: string;
margin?: { top?: string; right?: string; bottom?: string; left?: string };
}
export async function renderPdf(html: string, options?: PdfOptions) {
const browser = await puppeteer.launch({
executablePath: env.PUPPETEER_EXECUTABLE_PATH,
headless: true,
@@ -14,7 +21,10 @@ export async function renderPdf(html: string) {
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({
format: "A4",
format: options?.width || options?.height ? undefined : (options?.format || "A4"),
width: options?.width,
height: options?.height,
margin: options?.margin,
printBackground: true,
preferCSSPageSize: true,
});

View File

@@ -152,29 +152,40 @@ function buildShippingLabelPdf(options: {
<html>
<head>
<style>
@page { size: 4in 6in; margin: 8mm; }
body { font-family: ${company.theme.fontFamily}, Arial, sans-serif; color: #111827; font-size: 11px; }
.label { border: 2px solid #111827; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 12px; min-height: calc(6in - 16mm); box-sizing: border-box; }
.row { display: flex; justify-content: space-between; gap: 12px; }
@page { size: 4in 6in; margin: 0; }
*, *::before, *::after { box-sizing: border-box; }
html, body { width: 4in; min-width: 4in; max-width: 4in; height: 6in; min-height: 6in; max-height: 6in; margin: 0; padding: 0; overflow: hidden; background: white; }
body { font-family: ${company.theme.fontFamily}, Arial, sans-serif; color: #111827; font-size: 10px; line-height: 1.2; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.page { width: 4in; height: 6in; padding: 0.14in; overflow: hidden; page-break-after: avoid; break-after: avoid-page; }
.label { width: 100%; height: 100%; border: 2px solid #111827; border-radius: 10px; padding: 0.11in; display: flex; flex-direction: column; gap: 0.09in; overflow: hidden; }
.row { display: flex; justify-content: space-between; gap: 0.09in; }
.muted { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: #4b5563; }
.brand { border-bottom: 2px solid ${company.theme.primaryColor}; padding-bottom: 10px; }
.brand h1 { margin: 0; font-size: 18px; color: ${company.theme.primaryColor}; }
.block { border: 1px solid #d1d5db; border-radius: 10px; padding: 10px; }
.stack { display: flex; flex-direction: column; gap: 4px; }
.barcode { border: 2px solid #111827; border-radius: 10px; padding: 8px; text-align: center; font-family: monospace; font-size: 18px; letter-spacing: 0.18em; }
.brand { border-bottom: 2px solid ${company.theme.primaryColor}; padding-bottom: 0.09in; }
.brand-row { align-items: flex-start; }
.brand-company { flex: 1; min-width: 0; padding-right: 0.06in; }
.brand h1 { margin: 0; font-size: 16px; line-height: 1.05; color: ${company.theme.primaryColor}; overflow-wrap: anywhere; }
.shipment-number { width: 1.25in; flex: 0 0 1.25in; text-align: right; }
.block { border: 1px solid #d1d5db; border-radius: 10px; padding: 0.08in; min-width: 0; }
.stack { display: flex; flex-direction: column; gap: 3px; }
.barcode { border: 2px solid #111827; border-radius: 8px; padding: 0.08in; text-align: center; font-family: monospace; font-size: 16px; line-height: 1; letter-spacing: 0.15em; }
.strong { font-weight: 700; }
.big { font-size: 16px; font-weight: 700; }
.big { font-size: 15px; line-height: 1.05; font-weight: 700; }
.footer { text-align: center; font-size: 9px; color: #4b5563; overflow-wrap: anywhere; }
.reference-text { margin-top: 6px; overflow-wrap: anywhere; word-break: break-word; }
.block > div[style="margin-top:6px;"] { overflow-wrap: anywhere; word-break: break-word; }
div[style="text-align:center; font-size:10px; color:#4b5563;"] { text-align: center; font-size: 9px; color: #4b5563; overflow-wrap: anywhere; }
</style>
</head>
<body>
<div class="page">
<div class="label">
<div class="brand">
<div class="row">
<div>
<div class="row brand-row">
<div class="brand-company">
<div class="muted">From</div>
<h1>${escapeHtml(company.companyName)}</h1>
</div>
<div style="text-align:right;">
<div class="shipment-number">
<div class="muted">Shipment</div>
<div class="big">${escapeHtml(shipment.shipmentNumber)}</div>
</div>
@@ -217,7 +228,7 @@ function buildShippingLabelPdf(options: {
</div>
</body>
</html>
`);
`, { width: "4in", height: "6in", margin: { top: "0", right: "0", bottom: "0", left: "0" } });
}
function buildBillOfLadingPdf(options: {

View File

@@ -2,6 +2,7 @@ import type {
GanttLinkDto,
GanttTaskDto,
PlanningReadinessState,
PlanningStationDayLoadDto,
PlanningStationLoadDto,
PlanningTaskActionDto,
PlanningTimelineDto,
@@ -94,6 +95,15 @@ type StationAccumulator = {
workingDays: number[];
};
type StationDayAccumulator = {
stationId: string;
dateKey: string;
plannedMinutes: number;
actualMinutes: number;
operationCount: number;
capacityMinutes: number;
};
function clampProgress(value: number) {
return Math.max(0, Math.min(100, Math.round(value)));
}
@@ -199,6 +209,23 @@ function createStationLoad(record: StationAccumulator): PlanningStationLoadDto {
};
}
function createStationDayLoad(record: StationDayAccumulator): PlanningStationDayLoadDto {
const capacityMinutes = Math.max(record.capacityMinutes, 1);
const utilizationPercent = Math.round((record.plannedMinutes / capacityMinutes) * 100);
const actualUtilizationPercent = Math.round((record.actualMinutes / capacityMinutes) * 100);
return {
stationId: record.stationId,
dateKey: record.dateKey,
plannedMinutes: record.plannedMinutes,
actualMinutes: record.actualMinutes,
capacityMinutes,
utilizationPercent,
actualUtilizationPercent,
operationCount: record.operationCount,
overloaded: utilizationPercent > 100,
};
}
function buildProjectTask(
project: PlanningProjectRecord,
projectWorkOrders: PlanningWorkOrderRecord[],
@@ -492,6 +519,7 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
}
const stationAccumulators = new Map<string, StationAccumulator>();
const stationDayAccumulators = new Map<string, StationDayAccumulator>();
for (const workOrder of openWorkOrders) {
const insight = workOrderInsights.get(workOrder.id);
for (const operation of workOrder.operations) {
@@ -525,7 +553,22 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
current.lateCount += 1;
}
for (let cursor = startOfDay(operation.plannedStart).getTime(); cursor <= startOfDay(operation.plannedEnd).getTime(); cursor += DAY_MS) {
current.dayKeys.add(dateKey(new Date(cursor)));
const currentDate = new Date(cursor);
const currentDateKey = dateKey(currentDate);
current.dayKeys.add(currentDateKey);
const dayAccumulatorKey = `${operation.station.id}:${currentDateKey}`;
const dayAccumulator = stationDayAccumulators.get(dayAccumulatorKey) ?? {
stationId: operation.station.id,
dateKey: currentDateKey,
plannedMinutes: 0,
actualMinutes: 0,
operationCount: 0,
capacityMinutes: Math.max(operation.station.dailyCapacityMinutes, 60) * Math.max(operation.station.parallelCapacity, 1),
};
dayAccumulator.plannedMinutes += operation.plannedMinutes;
dayAccumulator.actualMinutes += operation.actualMinutes;
dayAccumulator.operationCount += 1;
stationDayAccumulators.set(dayAccumulatorKey, dayAccumulator);
}
stationAccumulators.set(operation.station.id, current);
}
@@ -537,6 +580,14 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
}
return left.stationCode.localeCompare(right.stationCode);
});
const stationDayLoads = [...stationDayAccumulators.values()]
.map(createStationDayLoad)
.sort((left, right) => {
if (left.dateKey !== right.dateKey) {
return left.dateKey.localeCompare(right.dateKey);
}
return left.stationId.localeCompare(right.stationId);
});
const stationLoadById = new Map(stationLoads.map((load) => [load.stationId, load]));
const tasks: GanttTaskDto[] = [];
@@ -863,5 +914,6 @@ export async function getPlanningTimeline(): Promise<PlanningTimelineDto> {
})
.slice(0, 12),
stationLoads,
stationDayLoads,
};
}

View File

@@ -59,6 +59,7 @@ const workOrderFiltersSchema = z.object({
const statusUpdateSchema = z.object({
status: z.enum(workOrderStatuses),
reason: z.string().nullable().optional(),
});
const materialIssueSchema = z.object({
@@ -215,7 +216,7 @@ manufacturingRouter.patch("/work-orders/:workOrderId/status", requirePermissions
return fail(response, 400, "INVALID_INPUT", "Work-order status payload is invalid.");
}
const result = await updateWorkOrderStatus(workOrderId, parsed.data.status, request.authUser?.id);
const result = await updateWorkOrderStatus(workOrderId, parsed.data, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}

View File

@@ -15,6 +15,7 @@ import type {
WorkOrderOperationTimerInput,
WorkOrderMaterialIssueInput,
WorkOrderStatus,
WorkOrderStatusUpdateInput,
WorkOrderSummaryDto,
} from "@mrp/shared";
@@ -41,6 +42,7 @@ type WorkOrderRecord = {
id: string;
workOrderNumber: string;
status: string;
holdReason: string | null;
quantity: number;
completedQuantity: number;
dueDate: Date | null;
@@ -390,6 +392,7 @@ function mapDetail(
return {
...mapSummary(record),
notes: record.notes,
holdReason: record.holdReason,
createdAt: record.createdAt.toISOString(),
itemType: record.item.type,
itemUnitOfMeasure: record.item.unitOfMeasure,
@@ -1294,12 +1297,18 @@ export async function updateWorkOrder(workOrderId: string, payload: WorkOrderInp
return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." };
}
export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrderStatus, actorId?: string | null) {
export async function updateWorkOrderStatus(
workOrderId: string,
payload: WorkOrderStatusUpdateInput,
actorId?: string | null
) {
const existing = await workOrderModel.findUnique({
where: { id: workOrderId },
select: {
id: true,
workOrderNumber: true,
status: true,
holdReason: true,
quantity: true,
completedQuantity: true,
},
@@ -1309,18 +1318,24 @@ export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrd
return { ok: false as const, reason: "Work order was not found." };
}
if (existing.status === "COMPLETE" && status !== "COMPLETE") {
if (existing.status === "COMPLETE" && payload.status !== "COMPLETE") {
return { ok: false as const, reason: "Completed work orders cannot be reopened from quick actions." };
}
if (status === "COMPLETE" && existing.completedQuantity < existing.quantity) {
if (payload.status === "COMPLETE" && existing.completedQuantity < existing.quantity) {
return { ok: false as const, reason: "Use the completion action to finish a work order." };
}
const nextHoldReason = payload.reason?.trim() ?? "";
if (payload.status === "ON_HOLD" && nextHoldReason.length === 0) {
return { ok: false as const, reason: "An On Hold reason is required before the work order can be paused." };
}
await workOrderModel.update({
where: { id: workOrderId },
data: {
status,
status: payload.status,
holdReason: payload.status === "ON_HOLD" ? nextHoldReason : null,
},
});
@@ -1333,10 +1348,12 @@ export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrd
entityType: "work-order",
entityId: workOrderId,
action: "status.updated",
summary: `Updated work order ${workOrder.workOrderNumber} to ${status}.`,
summary: `Updated work order ${workOrder.workOrderNumber} to ${payload.status}.`,
metadata: {
workOrderNumber: workOrder.workOrderNumber,
status,
previousStatus: existing.status,
status: payload.status,
holdReason: payload.status === "ON_HOLD" ? nextHoldReason : null,
},
});
}

View File

@@ -14,6 +14,7 @@ import {
listProjectQuoteOptions,
listProjectShipmentOptions,
updateProject,
updateProjectMilestoneStatus,
} from "./service.js";
const projectSchema = z.object({
@@ -51,6 +52,10 @@ const projectOptionQuerySchema = z.object({
customerId: z.string().optional(),
});
const milestoneStatusSchema = z.object({
status: z.enum(projectMilestoneStatuses),
});
function getRouteParam(value: unknown) {
return typeof value === "string" ? value : null;
}
@@ -147,3 +152,23 @@ projectsRouter.put("/:projectId", requirePermissions([permissions.projectsWrite]
return ok(response, result.project);
});
projectsRouter.patch("/:projectId/milestones/:milestoneId/status", requirePermissions([permissions.projectsWrite]), async (request, response) => {
const projectId = getRouteParam(request.params.projectId);
const milestoneId = getRouteParam(request.params.milestoneId);
if (!projectId || !milestoneId) {
return fail(response, 400, "INVALID_INPUT", "Project or milestone id is invalid.");
}
const parsed = milestoneStatusSchema.safeParse(request.body);
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "Project milestone status payload is invalid.");
}
const result = await updateProjectMilestoneStatus(projectId, milestoneId, parsed.data, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.project);
});

View File

@@ -11,6 +11,8 @@ import type {
ProjectInput,
ProjectMilestoneDto,
ProjectMilestoneInput,
ProjectMilestoneStatus,
ProjectMilestoneStatusUpdateInput,
ProjectOwnerOptionDto,
ProjectPriority,
ProjectRollupDto,
@@ -1266,3 +1268,60 @@ export async function updateProject(projectId: string, payload: ProjectInput, ac
}
return project ? { ok: true as const, project } : { ok: false as const, reason: "Unable to load saved project." };
}
export async function updateProjectMilestoneStatus(
projectId: string,
milestoneId: string,
payload: ProjectMilestoneStatusUpdateInput,
actorId?: string | null
) {
const existing = await prisma.projectMilestone.findUnique({
where: { id: milestoneId },
select: {
id: true,
projectId: true,
title: true,
status: true,
completedAt: true,
project: {
select: {
id: true,
projectNumber: true,
},
},
},
});
if (!existing || existing.projectId !== projectId) {
return { ok: false as const, reason: "Project milestone was not found." };
}
const nextStatus = payload.status as ProjectMilestoneStatus;
await prisma.projectMilestone.update({
where: { id: milestoneId },
data: {
status: nextStatus,
completedAt: nextStatus === "COMPLETE" ? existing.completedAt ?? new Date() : null,
},
});
const project = await getProjectById(projectId);
if (project) {
await logAuditEvent({
actorId,
entityType: "project",
entityId: projectId,
action: "milestone.status.updated",
summary: `Updated milestone ${existing.title} on ${existing.project.projectNumber} to ${nextStatus}.`,
metadata: {
projectNumber: existing.project.projectNumber,
milestoneId,
milestoneTitle: existing.title,
previousStatus: existing.status,
status: nextStatus,
},
});
}
return project ? { ok: true as const, project } : { ok: false as const, reason: "Unable to load saved project." };
}

View File

@@ -95,10 +95,23 @@ export interface PlanningStationLoadDto {
lateCount: number;
}
export interface PlanningStationDayLoadDto {
stationId: string;
dateKey: string;
plannedMinutes: number;
actualMinutes: number;
capacityMinutes: number;
utilizationPercent: number;
actualUtilizationPercent: number;
operationCount: number;
overloaded: boolean;
}
export interface PlanningTimelineDto {
tasks: GanttTaskDto[];
links: GanttLinkDto[];
summary: PlanningSummaryDto;
exceptions: PlanningExceptionDto[];
stationLoads: PlanningStationLoadDto[];
stationDayLoads: PlanningStationDayLoadDto[];
}

View File

@@ -157,6 +157,7 @@ export interface WorkOrderCompletionDto {
export interface WorkOrderDetailDto extends WorkOrderSummaryDto {
notes: string;
holdReason: string | null;
createdAt: string;
itemType: string;
itemUnitOfMeasure: string;
@@ -218,3 +219,8 @@ export interface WorkOrderOperationTimerInput {
action: "START" | "STOP";
notes: string;
}
export interface WorkOrderStatusUpdateInput {
status: WorkOrderStatus;
reason?: string | null;
}

View File

@@ -189,6 +189,10 @@ export interface ProjectMilestoneInput {
sortOrder: number;
}
export interface ProjectMilestoneStatusUpdateInput {
status: ProjectMilestoneStatus;
}
export interface ProjectDetailDto extends ProjectSummaryDto {
notes: string;
createdAt: string;

96
test-puppeteer.js Normal file
View File

@@ -0,0 +1,96 @@
import puppeteer from 'puppeteer';
import fs from 'fs';
const html = `
<html>
<head>
<style>
@page { size: 4in 6in; margin: 0; }
*, *::before, *::after { box-sizing: border-box; }
html, body { width: 4in; height: 6in; margin: 0; padding: 0.5in; overflow: hidden; }
body { font-family: Arial, sans-serif; color: #111827; font-size: 11px; }
.label { border: 2px solid #111827; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 12px; height: 100%; overflow: hidden; }
.row { display: flex; justify-content: space-between; gap: 12px; }
.muted { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: #4b5563; }
.brand { border-bottom: 2px solid #ed8936; padding-bottom: 10px; }
.brand h1 { margin: 0; font-size: 18px; color: #ed8936; }
.block { border: 1px solid #d1d5db; border-radius: 10px; padding: 10px; }
.stack { display: flex; flex-direction: column; gap: 4px; }
.barcode { border: 2px solid #111827; border-radius: 10px; padding: 8px; text-align: center; font-family: monospace; font-size: 18px; letter-spacing: 0.18em; }
.strong { font-weight: 700; }
.big { font-size: 16px; font-weight: 700; }
</style>
</head>
<body>
<div class="label">
<div class="brand">
<div class="row">
<div>
<div class="muted">From</div>
<h1>Message Point Media</h1>
</div>
<div style="text-align:right;">
<div class="muted">Shipment</div>
<div class="big">SHP-00003</div>
</div>
</div>
</div>
<div class="block">
<div class="muted">Ship To</div>
<div class="stack" style="margin-top:8px;">
<div class="strong">Northwind Fabrication</div>
<div>42 Assembly Ave</div>
<div>Milwaukee, WI 53202</div>
<div>USA</div>
</div>
</div>
<div class="row">
<div class="block" style="flex:1;">
<div class="muted">Service</div>
<div class="big" style="margin-top:6px;">GROUND</div>
</div>
<div class="block" style="width:90px;">
<div class="muted">Pkgs</div>
<div class="big" style="margin-top:6px;">1</div>
</div>
</div>
<div class="row">
<div class="block" style="flex:1;">
<div class="muted">Sales Order</div>
<div class="strong" style="margin-top:6px;">SO-00002</div>
</div>
<div class="block" style="width:110px;">
<div class="muted">Ship Date</div>
<div class="strong" style="margin-top:6px;">N/A</div>
</div>
</div>
<div class="block">
<div class="muted">Reference</div>
<div style="margin-top:6px;">FG-CTRL-BASE · Control Base Assembly</div>
</div>
<div class="barcode">
*SHP-00003*
</div>
<div style="text-align:center; font-size:10px; color:#4b5563;">Carrier pending · Tracking pending</div>
</div>
</body>
</html>
`;
async function run() {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({
format: "A4",
printBackground: true,
preferCSSPageSize: true,
});
fs.writeFileSync('/tmp/test-label.pdf', pdf);
console.log("PDF generated at /tmp/test-label.pdf");
} finally {
await browser.close();
}
}
run();

71
usage_guide.md Normal file
View File

@@ -0,0 +1,71 @@
# CODEXIUM: End-to-End Workflow Guide
This guide walks through the core operational workflow in CODEXIUM, starting from capturing a customer request in a Quote, all the way to shipping the final product.
## 1. Create a Quote
The process begins when a customer requests pricing.
1. Navigate to the **Quotes** module.
2. Click to create a **New Quote**.
3. Select the Customer using the searchable lookup.
4. Add **Quote Lines** by searching for the requested Inventory SKUs. The default price from the item master will automatically populate.
5. Add any necessary discounts, freight, and notes.
6. The Quote can go through an approval process. Once the customer accepts the terms, proceed to the next step.
## 2. Convert to Sales Order
Once the Quote is accepted, it becomes firm demand.
1. Open the approved **Quote**.
2. Use the action menu to **Convert to Sales Order**.
3. This creates a new Sales Order record, carrying over all lines, pricing, and customer information.
4. The Sales Order is now the authoritative commercial record for this demand.
## 3. Create a Project
For complex or long-running deliveries, create a Project to track the execution.
1. Navigate to the **Projects** module.
2. Click **New Project** and select the same Customer.
3. Define the project scope, priority, due dates, and owner.
4. Set up high-level **Milestones** to track progress on deliverables.
## 4. Assign Quote and Sales Order to Project
Link the commercial documents to the execution tracker.
1. Open the newly created **Project**.
2. In the project details, link the original **Quote** and the active **Sales Order**.
3. *Note: You can also link the Project from within the Quote and Sales Order detail pages. This reverse-link ensures that quote conversion automatically carries the project context into the Sales Order.*
4. The Project dashboard now provides visibility into the commercial value and linked deliverables.
## 5. Determine Manufacturing & Supply Requirements (Demand Planning)
With the Sales Order firm and linked to a Project, determine what needs to be made or bought.
1. Navigate to the **Workbench** module, where the **Demand Planning** view is available for the Sales Order.
2. The system runs a **Multi-level BOM explosion** against the Sales Order lines, netting against current stock and open supply (existing POs/MOs).
3. The system will generate **Build/Buy recommendations** for any shortages.
## 6. Issue Purchase Orders (POs)
Fulfill the "Buy" recommendations.
1. From the Demand Planning recommendations, use the planner-assisted conversion to draft **Purchase Orders** for the required buyout items and raw materials.
2. The POs will automatically peg back to the source Sales Order and carry the Project context.
3. Preferred vendors from the inventory item master are selected by default.
4. Send the PO PDFs to vendors and manage receiving in the **Purchase Orders** and **Warehouse** modules.
## 7. Issue Manufacturing Orders (MOs/Work Orders)
Fulfill the "Build" recommendations.
1. Again, from the Demand Planning recommendations, convert the "Build" shortages into **Work Orders**.
2. The Work Orders define the execution plan (Operations/Stations) based on the item's routing templates in the **Manufacturing** module.
3. Provide the Work Order to the shop floor.
4. As operators log labor and issue materials against the Work Order, the costs roll up, and final completion posts the finished goods to inventory, making them available for shipment.
## 8. Assign Shipping
Once production is complete, deliver the goods.
1. Navigate to the **Shipments** module and create a **New Shipment**.
2. Link the Shipment directly to the **Sales Order**.
3. The system shows the ordered vs. picked vs. remaining quantities.
4. Execute **Shipment Picking**, which pulls stock from specific warehouse locations and posts the inventory issue transactions.
5. Update logistics details: Carrier, Service Level, Tracking Number, and Package Count.
6. Generate branded logistics PDFs directly from the Shipment: **Packing Slips, Shipping Labels, and Bills of Lading**.
7. The Shipment can also be seen from the linked Project, closing the loop on the delivery lifecycle.