feat: implement frontend-only mock for production lines and runs

- Add ProductionTemplatesPage component for managing production templates with a visual representation using React Flow.
- Create LineHeaderNode and LineStageNode components for rendering production line nodes.
- Introduce RunStageNode and RunHeaderNode components for displaying run stages and headers.
- Implement StageProgressStrip for visualizing stage progress in runs.
- Create mock data for production runs and templates to simulate backend functionality.
- Define types for production templates and runs to structure mock data.
- Document frontend Phase 2 specifications for manufacturing processes, including screens, dialogs, and validation posture.
This commit is contained in:
2026-07-28 22:55:56 +05:30
parent b12adebaa0
commit 4561ef7ba8
19 changed files with 2457 additions and 1 deletions
@@ -11,16 +11,19 @@ import {
CalendarClock,
ChevronRight,
ClipboardList,
Factory,
FileBarChart,
FileText,
HelpCircle,
IdCard,
LayoutGrid,
LayoutTemplate,
ListTree,
Menu,
Package,
PackageCheck,
PackageX,
PlayCircle,
Ruler,
Settings,
ShieldCheck,
@@ -89,6 +92,18 @@ const navItems: {
{ title: "Stock", code: "stock", href: "/dashboard/stock", icon: Warehouse, chevron: true },
{ title: "Warehouses", code: "warehouses", href: "/dashboard/warehouse", icon: Building2, chevron: true },
{ title: "Orders", code: "orders", href: "/dashboard/orders", icon: ShoppingCart, chevron: true },
{
title: "Production",
code: "production",
href: "/dashboard/production",
landingHref: "/dashboard/production/runs",
icon: Factory,
chevron: true,
children: [
{ title: "Templates", code: "production.templates", href: "/dashboard/production/templates", icon: LayoutTemplate },
{ title: "Runs", code: "production.runs", href: "/dashboard/production/runs", icon: PlayCircle },
],
},
{
title: "HRM",
code: "hrm",
@@ -333,7 +348,7 @@ export function AppSidebar() {
// grants it. This is also flagged in 02-SECURITY.md as the AR-09 sidebar-visibility
// stopgap for HRM's salary/PII data — it hides HRM from the UI but doesn't enforce
// anything server-side.
const bypassCodes = new Set(["procurement", "hrm"])
const bypassCodes = new Set(["procurement", "hrm", "production"])
const visibleItems = loading
? []
: navItems
@@ -35,6 +35,9 @@ const SEGMENT_LABELS: Record<string, string> = {
rfqs: "RFQs",
"purchase-orders": "Purchase Orders",
"purchase-returns": "Purchase Returns",
production: "Production",
templates: "Templates",
runs: "Runs",
}
function labelFor(segment: string): string {
@@ -0,0 +1,57 @@
import { memo } from "react"
import { Handle, Position, type NodeProps } from "@xyflow/react"
import { cn } from "@/lib/utils"
export interface LineHeaderData extends Record<string, unknown> {
templateId: number
docNo: string
name: string
status: "Active" | "Inactive"
activeRunCount: number
}
/** Row label docked at the left of each production line — the template itself. */
function LineHeaderNode({ data }: NodeProps & { data: LineHeaderData }) {
return (
<div className="flex w-56 flex-col gap-1.5 rounded-2xl bg-card p-3 shadow-sm ring-1 ring-foreground/10">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-xs font-medium text-muted-foreground">{data.docNo}</span>
<span
className={cn(
"shrink-0 rounded-full px-2 py-0.5 text-xs font-medium",
data.status === "Active" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
)}
>
{data.status}
</span>
</div>
<p className="truncate text-sm font-bold text-foreground">{data.name}</p>
{data.activeRunCount > 0 && (
<span className="w-fit rounded-full bg-warning/10 px-2 py-0.5 text-xs font-medium text-warning">
{data.activeRunCount} in progress
</span>
)}
<Handle type="source" position={Position.Right} className="!bg-primary !size-2.5" />
</div>
)
}
export interface LineStageData extends Record<string, unknown> {
templateId: number
name: string
}
/** One stage on a production line — read-only, purely a visual chip on the overview canvas. */
function LineStageNode({ data }: NodeProps & { data: LineStageData }) {
return (
<div className="w-36 rounded-xl bg-primary/10 px-3 py-2.5 text-center shadow-sm ring-1 ring-primary/20">
<Handle type="target" position={Position.Left} className="!bg-primary !size-2.5" />
<p className="truncate text-sm font-semibold text-primary">{data.name}</p>
<Handle type="source" position={Position.Right} className="!bg-primary !size-2.5" />
</div>
)
}
export const LineHeaderNodeComponent = memo(LineHeaderNode)
export const LineStageNodeComponent = memo(LineStageNode)
@@ -0,0 +1,75 @@
import { memo } from "react"
import { Handle, Position, type NodeProps } from "@xyflow/react"
import { ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { RunStatus } from "@/types/production"
import { STAGE_STATUS_COLOR, STAGE_STATUS_LABEL, type StageStatus } from "@/lib/production-status-colors"
export interface RunHeaderData extends Record<string, unknown> {
docNo: string
templateName: string
status: RunStatus
}
/** Left-most box on a run's production line — the run itself, not a stage. */
function RunHeaderNode({ data }: NodeProps & { data: RunHeaderData }) {
return (
<div className="flex w-48 flex-col gap-1.5 rounded-2xl bg-card p-3 shadow-sm ring-1 ring-foreground/10">
<span className="truncate text-xs font-medium text-muted-foreground">{data.docNo}</span>
<p className="truncate text-sm font-bold text-foreground">{data.templateName}</p>
<Handle type="source" position={Position.Right} className="!bg-primary !size-2.5" />
</div>
)
}
export interface RunStageData extends Record<string, unknown> {
name: string
state: StageStatus
isActive: boolean
/** Present only on the current (leftmost incomplete) stage of an InProgress run. */
onAdvance?: () => void
}
/** One stage on a run's production line, colored by its live status — the box the "give
* progress" action lives on: the active stage grows an Advance button to push it forward. */
function RunStageNode({ data }: NodeProps & { data: RunStageData }) {
const color = STAGE_STATUS_COLOR[data.state]
return (
<div
className={cn(
"w-44 rounded-2xl bg-card p-3 shadow-sm ring-2 transition-all",
data.isActive ? "ring-offset-2 ring-offset-background" : "ring-foreground/10"
)}
style={data.isActive ? { boxShadow: `0 0 0 2px ${color}` } : undefined}
>
<Handle type="target" position={Position.Left} className="!bg-primary !size-2.5" />
<div className="flex items-center gap-1.5">
<span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: color }} />
<p className="min-w-0 truncate text-sm font-bold text-foreground">{data.name}</p>
</div>
<p className="mt-1 text-xs font-medium" style={{ color }}>{STAGE_STATUS_LABEL[data.state]}</p>
{data.onAdvance && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
data.onAdvance?.()
}}
className="nodrag mt-2 flex w-full items-center justify-center gap-1 rounded-lg bg-primary px-2 py-1.5 text-xs font-semibold text-primary-foreground hover:bg-primary/90"
>
Advance
<ChevronRight className="size-3.5" />
</button>
)}
<Handle type="source" position={Position.Right} className="!bg-primary !size-2.5" />
</div>
)
}
export const RunHeaderNodeComponent = memo(RunHeaderNode)
export const RunStageNodeComponent = memo(RunStageNode)
@@ -0,0 +1,85 @@
import { CheckCircle2 } from "lucide-react"
import { cn } from "@/lib/utils"
import {
RUN_CANCELLED_COLOR,
RUN_COMPLETED_COLOR,
STAGE_STATUS_COLOR,
STAGE_STATUS_LABEL,
STAGE_STATUS_ORDER,
} from "@/lib/production-status-colors"
import { RunStatus, StageSummary } from "@/types/production"
/**
* One segment per stage-status count (docs/21-FRONTEND-PHASE2.md §3). Completed runs render
* a full teal strip + check; cancelled runs get a red accent instead of per-stage segments.
*/
export function StageProgressStrip({
status,
summary,
className,
}: {
status: RunStatus
summary: StageSummary
className?: string
}) {
if (status === "Completed") {
return (
<div className={cn("flex h-2.5 items-center gap-1.5 rounded-full", className)}>
<div className="h-2.5 flex-1 rounded-full" style={{ backgroundColor: RUN_COMPLETED_COLOR }} />
<CheckCircle2 className="size-4 shrink-0" style={{ color: RUN_COMPLETED_COLOR }} />
</div>
)
}
const counts: Record<string, number> = {
Waiting: summary.waiting,
Ready: summary.ready,
InProgress: summary.inProgress,
Done: summary.done,
Approved: summary.approved,
}
const total = STAGE_STATUS_ORDER.reduce((sum, key) => sum + counts[key], 0)
return (
<div
className={cn("flex h-2.5 w-full overflow-hidden rounded-full bg-muted", className)}
style={status === "Cancelled" ? { boxShadow: `0 0 0 2px ${RUN_CANCELLED_COLOR}` } : undefined}
>
{total === 0
? null
: STAGE_STATUS_ORDER.map((key) => {
const count = counts[key]
if (count === 0) return null
return (
<div
key={key}
title={`${STAGE_STATUS_LABEL[key]}: ${count}`}
style={{ backgroundColor: STAGE_STATUS_COLOR[key], width: `${(count / total) * 100}%` }}
/>
)
})}
</div>
)
}
export function StageStatusLegend({ className }: { className?: string }) {
return (
<div className={cn("flex flex-wrap items-center gap-x-4 gap-y-1.5", className)}>
{STAGE_STATUS_ORDER.map((key) => (
<div key={key} className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: STAGE_STATUS_COLOR[key] }} />
{STAGE_STATUS_LABEL[key]}
</div>
))}
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: RUN_CANCELLED_COLOR }} />
Cancelled
</div>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: RUN_COMPLETED_COLOR }} />
Completed
</div>
</div>
)
}