189 lines
10 KiB
TypeScript
189 lines
10 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Gantt } from "@svar-ui/react-gantt";
|
|
import "@svar-ui/react-gantt/style.css";
|
|
import { Link } from "react-router-dom";
|
|
|
|
import type { DemandPlanningRollupDto, GanttTaskDto, PlanningExceptionDto, PlanningTimelineDto } from "@mrp/shared";
|
|
|
|
import { useAuth } from "../../auth/AuthProvider";
|
|
import { ApiError, api } from "../../lib/api";
|
|
import { useTheme } from "../../theme/ThemeProvider";
|
|
|
|
function formatDate(value: string | null) {
|
|
if (!value) {
|
|
return "Unscheduled";
|
|
}
|
|
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
}).format(new Date(value));
|
|
}
|
|
|
|
export function GanttPage() {
|
|
const { token } = useAuth();
|
|
const { mode } = useTheme();
|
|
const [timeline, setTimeline] = useState<PlanningTimelineDto | null>(null);
|
|
const [planningRollup, setPlanningRollup] = useState<DemandPlanningRollupDto | null>(null);
|
|
const [status, setStatus] = useState("Loading live planning timeline...");
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
return;
|
|
}
|
|
|
|
Promise.all([api.getPlanningTimeline(token), api.getDemandPlanningRollup(token)])
|
|
.then(([data, rollup]) => {
|
|
setTimeline(data);
|
|
setPlanningRollup(rollup);
|
|
setStatus("Planning timeline loaded.");
|
|
})
|
|
.catch((error: unknown) => {
|
|
const message = error instanceof ApiError ? error.message : "Unable to load planning timeline.";
|
|
setStatus(message);
|
|
});
|
|
}, [token]);
|
|
|
|
const tasks = timeline?.tasks ?? [];
|
|
const links = timeline?.links ?? [];
|
|
const summary = timeline?.summary;
|
|
const exceptions = timeline?.exceptions ?? [];
|
|
|
|
return (
|
|
<section className="space-y-4">
|
|
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
|
<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">Planning</p>
|
|
<h3 className="mt-2 text-2xl font-bold text-text">Live Project + Manufacturing Gantt</h3>
|
|
<p className="mt-2 max-w-3xl text-sm text-muted">
|
|
The planning surface now reads directly from active projects and open work orders so schedule pressure, due-date risk, and standalone manufacturing load are visible in one place.
|
|
</p>
|
|
</div>
|
|
<div className="rounded-3xl border border-line/70 bg-page/60 px-3 py-3 text-sm">
|
|
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Timeline Status</div>
|
|
<div className="mt-2 font-semibold text-text">{status}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section className="grid gap-3 xl:grid-cols-6">
|
|
<article className="rounded-[24px] 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">Active Projects</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{summary?.activeProjects ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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">At Risk</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{summary?.atRiskProjects ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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 Projects</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{summary?.overdueProjects ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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">Active Work Orders</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{summary?.activeWorkOrders ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{summary?.overdueWorkOrders ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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">Unscheduled Work</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{summary?.unscheduledWorkOrders ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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">Shortage Items</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">{planningRollup?.summary.uncoveredItemCount ?? 0}</div>
|
|
</article>
|
|
<article className="rounded-[24px] 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">Build / Buy</p>
|
|
<div className="mt-2 text-xl font-extrabold text-text">
|
|
{planningRollup ? `${planningRollup.summary.totalBuildQuantity} / ${planningRollup.summary.totalPurchaseQuantity}` : "0 / 0"}
|
|
</div>
|
|
</article>
|
|
</section>
|
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.2fr)_360px]">
|
|
<div
|
|
className={`gantt-theme overflow-hidden rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5 ${
|
|
mode === "dark" ? "wx-willow-dark-theme" : "wx-willow-theme"
|
|
}`}
|
|
>
|
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Schedule Window</p>
|
|
<p className="mt-2 text-sm text-muted">
|
|
{summary ? `${formatDate(summary.horizonStart)} through ${formatDate(summary.horizonEnd)}` : "Waiting for live schedule data."}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
|
{tasks.length} schedule rows
|
|
</div>
|
|
</div>
|
|
<Gantt
|
|
tasks={tasks.map((task: GanttTaskDto) => ({
|
|
...task,
|
|
start: new Date(task.start),
|
|
end: new Date(task.end),
|
|
parent: task.parentId ?? undefined,
|
|
}))}
|
|
links={links}
|
|
/>
|
|
</div>
|
|
<aside className="space-y-3">
|
|
<section className="rounded-[28px] 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">Planning Exceptions</p>
|
|
<p className="mt-2 text-sm text-muted">Priority schedule issues from live project due dates and manufacturing commitments.</p>
|
|
{exceptions.length === 0 ? (
|
|
<div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
|
No planning exceptions are active.
|
|
</div>
|
|
) : (
|
|
<div className="mt-5 space-y-3">
|
|
{exceptions.map((exception: PlanningExceptionDto) => (
|
|
<Link key={exception.id} to={exception.detailHref} className="block rounded-3xl border border-line/70 bg-page/60 p-3 transition hover:bg-page/80">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">{exception.kind === "PROJECT" ? "Project" : "Work Order"}</div>
|
|
<div className="mt-1 font-semibold text-text">{exception.title}</div>
|
|
<div className="mt-2 text-xs text-muted">{exception.ownerLabel ?? "No owner context"}</div>
|
|
</div>
|
|
<span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">
|
|
{exception.status.replaceAll("_", " ")}
|
|
</span>
|
|
</div>
|
|
<div className="mt-3 text-xs text-muted">Due: {formatDate(exception.dueDate)}</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
<section className="rounded-[28px] 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">Planner Actions</p>
|
|
<div className="mt-4 space-y-2 rounded-3xl border border-line/70 bg-page/60 p-3 text-sm">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-muted">Uncovered quantity</span>
|
|
<span className="font-semibold text-text">{planningRollup?.summary.totalUncoveredQuantity ?? 0}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-muted">Projects with linked demand</span>
|
|
<span className="font-semibold text-text">{planningRollup?.summary.projectCount ?? 0}</span>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
<Link to="/projects" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
|
Open projects
|
|
</Link>
|
|
<Link to="/manufacturing/work-orders" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
|
Open work orders
|
|
</Link>
|
|
<Link to="/" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
|
|
Back to dashboard
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
</aside>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|