@burdenoff/microfe-assethandler 2026.810.1 → 2026.813.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assethandler/AssetHandlerRoutes.d.ts.map +1 -1
- package/dist/assethandler/AssetHandlerRoutes.js +29 -25
- package/dist/assethandler/AssetHandlerRoutes.js.map +1 -1
- package/dist/assethandler/components/AttachmentsPanel.d.ts.map +1 -1
- package/dist/assethandler/components/AttachmentsPanel.js.map +1 -1
- package/dist/assethandler/hooks/useMaintenancePlans.d.ts +93 -0
- package/dist/assethandler/hooks/useMaintenancePlans.d.ts.map +1 -0
- package/dist/assethandler/hooks/useMaintenancePlans.js +135 -0
- package/dist/assethandler/hooks/useMaintenancePlans.js.map +1 -0
- package/dist/assethandler/ops/MaintenancePlanDialog.d.ts +18 -0
- package/dist/assethandler/ops/MaintenancePlanDialog.d.ts.map +1 -0
- package/dist/assethandler/ops/MaintenancePlanDialog.js +267 -0
- package/dist/assethandler/ops/MaintenancePlanDialog.js.map +1 -0
- package/dist/assethandler/pages/AssetsAssetCategoriesPage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetCategoriesPage.js.map +1 -1
- package/dist/assethandler/pages/AssetsAssetCreatePage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetCreatePage.js +169 -159
- package/dist/assethandler/pages/AssetsAssetCreatePage.js.map +1 -1
- package/dist/assethandler/pages/AssetsAssetDetailPage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetDetailPage.js +209 -209
- package/dist/assethandler/pages/AssetsAssetDetailPage.js.map +1 -1
- package/dist/assethandler/pages/AssetsAssetListPage.d.ts.map +1 -1
- package/dist/assethandler/pages/AssetsAssetListPage.js.map +1 -1
- package/dist/assethandler/pages/MaintenanceMaintenancePlansPage.d.ts +2 -0
- package/dist/assethandler/pages/MaintenanceMaintenancePlansPage.d.ts.map +1 -0
- package/dist/assethandler/pages/MaintenanceMaintenancePlansPage.js +370 -0
- package/dist/assethandler/pages/MaintenanceMaintenancePlansPage.js.map +1 -0
- package/dist/assethandler/pages/MaintenanceMaintenanceSchedulerPage.d.ts.map +1 -1
- package/dist/assethandler/pages/MaintenanceMaintenanceSchedulerPage.js +151 -143
- package/dist/assethandler/pages/MaintenanceMaintenanceSchedulerPage.js.map +1 -1
- package/dist/assethandler/store/assethandlerStore.d.ts.map +1 -1
- package/dist/assethandler/store/assethandlerStore.js.map +1 -1
- package/dist/types/assistant.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMaintenancePlans.js","names":[],"sources":["../../../src/assethandler/hooks/useMaintenancePlans.ts"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { notify } from '../utils/toast';\n\n/**\n * Recurring preventive-maintenance plans (BOFF-6031).\n *\n * These operations post-date the MFE's bootstrap query, so they are hand-written\n * `gql` documents rather than additions to it: the bootstrap is one large query\n * every page pays for, and plans are only needed on the plans surface.\n *\n * ── What the backend guarantees, and what this must therefore not do ─────────\n *\n * `nextDueAt` / `nextDueMeter` are DERIVED server-side on every read — they are\n * not stored. So this layer never caches a due date or recomputes one locally;\n * it re-reads after anything that could move it.\n *\n * `currentMeter`, `nextDueMeter`, `daysUntilDue` and `meterUntilDue` are all\n * genuinely nullable and null means \"not applicable\", never zero. An asset that\n * nobody meters has no meter reading; rendering that as 0 would claim a machine\n * sitting at zero hours.\n */\n\nexport const MAINTENANCE_PLAN_FIELDS = gql`\n fragment MaintenancePlanFields on MaintenancePlan {\n id\n assetId\n title\n description\n type\n priority\n isActive\n intervalDays\n intervalMeter\n lastServiceAt\n lastServiceMeter\n startAt\n startMeter\n leadTimeDays\n estimatedCost\n checklistTemplate\n createdAt\n updatedAt\n }\n`;\n\nconst MAINTENANCE_PLANS_QUERY = gql`\n ${MAINTENANCE_PLAN_FIELDS}\n query AssetHandlerMaintenancePlans($assetId: ID, $includeInactive: Boolean) {\n maintenancePlans(assetId: $assetId, includeInactive: $includeInactive) {\n ...MaintenancePlanFields\n }\n }\n`;\n\nconst DUE_MAINTENANCE_PLANS_QUERY = gql`\n ${MAINTENANCE_PLAN_FIELDS}\n query AssetHandlerDueMaintenancePlans($assetId: ID, $withinDays: Int) {\n dueMaintenancePlans(assetId: $assetId, withinDays: $withinDays) {\n plan {\n ...MaintenancePlanFields\n }\n nextDueAt\n nextDueMeter\n currentMeter\n isDue\n isOverdue\n daysUntilDue\n meterUntilDue\n dueReason\n }\n }\n`;\n\nconst CREATE_MAINTENANCE_PLAN = gql`\n ${MAINTENANCE_PLAN_FIELDS}\n mutation AssetHandlerCreateMaintenancePlan($input: CreateMaintenancePlanInput!) {\n createMaintenancePlan(input: $input) {\n ...MaintenancePlanFields\n }\n }\n`;\n\nconst UPDATE_MAINTENANCE_PLAN = gql`\n ${MAINTENANCE_PLAN_FIELDS}\n mutation AssetHandlerUpdateMaintenancePlan($id: ID!, $input: UpdateMaintenancePlanInput!) {\n updateMaintenancePlan(id: $id, input: $input) {\n ...MaintenancePlanFields\n }\n }\n`;\n\nconst DELETE_MAINTENANCE_PLAN = gql`\n mutation AssetHandlerDeleteMaintenancePlan($id: ID!) {\n deleteMaintenancePlan(id: $id)\n }\n`;\n\nconst GENERATE_DUE_RECORDS = gql`\n mutation AssetHandlerGenerateDueMaintenanceRecords($input: GenerateMaintenanceRecordsInput) {\n generateDueMaintenanceRecords(input: $input) {\n dryRun\n generated\n skipped\n notes\n records {\n id\n title\n scheduledAt\n dueMeter\n }\n }\n }\n`;\n\nexport interface MaintenancePlan {\n id: string;\n assetId: string;\n title: string;\n description: string | null;\n type: string;\n priority: string;\n isActive: boolean;\n intervalDays: number | null;\n intervalMeter: number | null;\n lastServiceAt: string | null;\n lastServiceMeter: number | null;\n startAt: string | null;\n startMeter: number | null;\n leadTimeDays: number | null;\n estimatedCost: number | null;\n checklistTemplate: Record<string, unknown>[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface DueMaintenancePlan {\n plan: MaintenancePlan;\n nextDueAt: string | null;\n nextDueMeter: number | null;\n currentMeter: number | null;\n isDue: boolean;\n isOverdue: boolean;\n daysUntilDue: number | null;\n meterUntilDue: number | null;\n dueReason: string | null;\n}\n\nexport interface GenerateResult {\n dryRun: boolean;\n generated: number;\n skipped: number;\n notes: string[];\n records: { id: string; title: string; scheduledAt: string | null; dueMeter: number | null }[];\n}\n\n/** The four values the backend accepts for a PLAN. */\nexport const PLAN_TYPES = ['PREVENTIVE', 'CORRECTIVE', 'PREDICTIVE', 'INSPECTION'] as const;\nexport const PLAN_PRIORITIES = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const;\n\nconst REFETCH = ['AssetHandlerMaintenancePlans', 'AssetHandlerDueMaintenancePlans'];\n\nexport function useMaintenancePlans(\n options: { includeInactive?: boolean; withinDays?: number } = {}\n) {\n const { includeInactive = false, withinDays = 0 } = options;\n\n const plansQuery = useQuery<{ maintenancePlans: MaintenancePlan[] }>(MAINTENANCE_PLANS_QUERY, {\n variables: { includeInactive },\n fetchPolicy: 'cache-and-network',\n });\n\n const dueQuery = useQuery<{ dueMaintenancePlans: DueMaintenancePlan[] }>(\n DUE_MAINTENANCE_PLANS_QUERY,\n { variables: { withinDays }, fetchPolicy: 'cache-and-network' }\n );\n\n const [createPlan, createState] = useMutation(CREATE_MAINTENANCE_PLAN, {\n refetchQueries: REFETCH,\n onCompleted: () => notify.success('Plan created', 'It will raise work when it falls due.'),\n onError: (error) => notify.error('Could not create the plan', error.message),\n });\n\n const [updatePlan, updateState] = useMutation(UPDATE_MAINTENANCE_PLAN, {\n refetchQueries: REFETCH,\n onCompleted: () => notify.success('Plan updated'),\n onError: (error) => notify.error('Could not update the plan', error.message),\n });\n\n const [deletePlan, deleteState] = useMutation(DELETE_MAINTENANCE_PLAN, {\n refetchQueries: REFETCH,\n // Said plainly, because it is the reassuring half: deleting a schedule does\n // not delete the service history it produced.\n onCompleted: () =>\n notify.success('Plan deleted', 'The service records it raised have been kept.'),\n onError: (error) => notify.error('Could not delete the plan', error.message),\n });\n\n const [generateRecords, generateState] = useMutation<{\n generateDueMaintenanceRecords: GenerateResult;\n }>(GENERATE_DUE_RECORDS, {\n // Generated records are MaintenanceRecords, which the bootstrap owns — so\n // the scheduler and calendar pages must be refreshed too, or the new work\n // is invisible everywhere except here.\n refetchQueries: [...REFETCH, 'AssetHandlerBootstrap'],\n onError: (error) => notify.error('Could not raise the work', error.message),\n });\n\n return {\n plans: plansQuery.data?.maintenancePlans ?? [],\n duePlans: dueQuery.data?.dueMaintenancePlans ?? [],\n loading: plansQuery.loading || dueQuery.loading,\n error: plansQuery.error ?? dueQuery.error ?? null,\n refetch: () => {\n void plansQuery.refetch();\n void dueQuery.refetch();\n },\n createPlan,\n updatePlan,\n deletePlan,\n generateRecords,\n busy:\n createState.loading || updateState.loading || deleteState.loading || generateState.loading,\n };\n}\n"],"mappings":";;;;AAuBA,IAAa,IAA0B,CAAG;;;;;;;;;;;;;;;;;;;;;GAuBpC,IAA0B,CAAG;IAC/B,EAAwB;;;;;;GAQtB,IAA8B,CAAG;IACnC,EAAwB;;;;;;;;;;;;;;;;GAkBtB,IAA0B,CAAG;IAC/B,EAAwB;;;;;;GAQtB,IAA0B,CAAG;IAC/B,EAAwB;;;;;;GAQtB,IAA0B,CAAG;;;;GAM7B,IAAuB,CAAG;;;;;;;;;;;;;;;GA2DnB,IAAa;CAAC;CAAc;CAAc;CAAc;CAAa,EACrE,IAAkB;CAAC;CAAY;CAAQ;CAAU;CAAM,EAE9D,IAAU,CAAC,gCAAgC,kCAAkC;AAEnF,SAAgB,EACd,IAA8D,EAAE,EAChE;CACA,IAAM,EAAE,qBAAkB,IAAO,gBAAa,MAAM,GAE9C,IAAa,EAAkD,GAAyB;EAC5F,WAAW,EAAE,oBAAiB;EAC9B,aAAa;EACd,CAAC,EAEI,IAAW,EACf,GACA;EAAE,WAAW,EAAE,eAAY;EAAE,aAAa;EAAqB,CAChE,EAEK,CAAC,GAAY,KAAe,EAAY,GAAyB;EACrE,gBAAgB;EAChB,mBAAmB,EAAO,QAAQ,gBAAgB,wCAAwC;EAC1F,UAAU,MAAU,EAAO,MAAM,6BAA6B,EAAM,QAAQ;EAC7E,CAAC,EAEI,CAAC,GAAY,KAAe,EAAY,GAAyB;EACrE,gBAAgB;EAChB,mBAAmB,EAAO,QAAQ,eAAe;EACjD,UAAU,MAAU,EAAO,MAAM,6BAA6B,EAAM,QAAQ;EAC7E,CAAC,EAEI,CAAC,GAAY,KAAe,EAAY,GAAyB;EACrE,gBAAgB;EAGhB,mBACE,EAAO,QAAQ,gBAAgB,gDAAgD;EACjF,UAAU,MAAU,EAAO,MAAM,6BAA6B,EAAM,QAAQ;EAC7E,CAAC,EAEI,CAAC,GAAiB,KAAiB,EAEtC,GAAsB;EAIvB,gBAAgB,CAAC,GAAG,GAAS,wBAAwB;EACrD,UAAU,MAAU,EAAO,MAAM,4BAA4B,EAAM,QAAQ;EAC5E,CAAC;AAEF,QAAO;EACL,OAAO,EAAW,MAAM,oBAAoB,EAAE;EAC9C,UAAU,EAAS,MAAM,uBAAuB,EAAE;EAClD,SAAS,EAAW,WAAW,EAAS;EACxC,OAAO,EAAW,SAAS,EAAS,SAAS;EAC7C,eAAe;AAER,GADA,EAAW,SAAS,EACpB,EAAS,SAAS;;EAEzB;EACA;EACA;EACA;EACA,MACE,EAAY,WAAW,EAAY,WAAW,EAAY,WAAW,EAAc;EACtF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MaintenancePlan } from '../hooks/useMaintenancePlans';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The dialog is a thin shell; the form below is REMOUNTED per plan via `key`.
|
|
5
|
+
*
|
|
6
|
+
* That is what keeps the fields correct when you edit one plan, close, then edit
|
|
7
|
+
* another. Re-seeding the same mounted form from an effect would do the same job
|
|
8
|
+
* but through a cascade of setState calls during render — which React (and this
|
|
9
|
+
* repo's lint) rightly rejects. A fresh mount reads its initial state straight
|
|
10
|
+
* from props, with no synchronisation to get wrong.
|
|
11
|
+
*/
|
|
12
|
+
export declare function MaintenancePlanDialog({ open, onOpenChange, plan, }: {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
/** Present when editing; absent when creating. */
|
|
16
|
+
plan?: MaintenancePlan | null;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=MaintenancePlanDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MaintenancePlanDialog.d.ts","sourceRoot":"","sources":["../../../src/assethandler/ops/MaintenancePlanDialog.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,8BAA8B,CAAC;AAsCtC;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,IAAI,GACL,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,kDAAkD;IAClD,IAAI,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/B,+BAcA"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useAssetHandlerStore as e } from "../store/assethandlerStore.js";
|
|
2
|
+
import { notify as t } from "../utils/toast.js";
|
|
3
|
+
import { PLAN_PRIORITIES as n, PLAN_TYPES as r, useMaintenancePlans as i } from "../hooks/useMaintenancePlans.js";
|
|
4
|
+
import { useState as a } from "react";
|
|
5
|
+
import { Loader2 as ee, Plus as te, X as ne } from "lucide-react";
|
|
6
|
+
import { Fragment as o, jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
7
|
+
import { Button as l, Input as u, Label as d, ResponsiveDialog as f, ResponsiveDialogContent as p, ResponsiveDialogDescription as m, ResponsiveDialogFooter as h, ResponsiveDialogHeader as g, ResponsiveDialogTitle as _, Select as v, SelectContent as y, SelectItem as b, SelectTrigger as x, SelectValue as S, Textarea as C } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
//#region src/assethandler/ops/MaintenancePlanDialog.tsx
|
|
9
|
+
var w = (e) => e.toLowerCase().replace(/_/g, " ").replace(/^\w/, (e) => e.toUpperCase()), T = (e) => e ? e.slice(0, 10) : "", E = (e) => {
|
|
10
|
+
let t = e.trim();
|
|
11
|
+
if (t === "") return null;
|
|
12
|
+
let n = Number(t);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}, D = (e) => {
|
|
15
|
+
let t = (e?.checklistTemplate ?? []).map((e) => typeof e?.task == "string" ? e.task : "").filter((e) => e !== "");
|
|
16
|
+
return t.length > 0 ? t : [""];
|
|
17
|
+
};
|
|
18
|
+
function O({ open: e, onOpenChange: t, plan: n }) {
|
|
19
|
+
return /* @__PURE__ */ s(f, {
|
|
20
|
+
open: e,
|
|
21
|
+
onOpenChange: t,
|
|
22
|
+
children: /* @__PURE__ */ s(p, {
|
|
23
|
+
className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl",
|
|
24
|
+
children: e && /* @__PURE__ */ s(k, {
|
|
25
|
+
plan: n ?? null,
|
|
26
|
+
onOpenChange: t
|
|
27
|
+
}, n?.id ?? "new")
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function k({ plan: f, onOpenChange: p }) {
|
|
32
|
+
let { assets: O } = e(), { createPlan: k, updatePlan: A, busy: j } = i(), M = !!f, [N, re] = a(f?.assetId ?? ""), [P, F] = a(f?.title ?? ""), [I, L] = a(f?.description ?? ""), [R, z] = a(f?.type ?? "PREVENTIVE"), [B, V] = a(f?.priority ?? "MEDIUM"), [H, U] = a(f?.intervalDays == null ? "" : String(f.intervalDays)), [W, G] = a(f?.intervalMeter == null ? "" : String(f.intervalMeter)), [K, q] = a(T(f?.startAt ?? null)), [J, Y] = a(f?.startMeter == null ? "" : String(f.startMeter)), [X, ie] = a(f?.leadTimeDays == null ? "" : String(f.leadTimeDays)), [Z, ae] = a(f?.estimatedCost == null ? "" : String(f.estimatedCost)), [Q, $] = a(D(f)), oe = (e, t) => $((n) => n.map((n, r) => r === e ? t : n)), se = () => $((e) => [...e, ""]), ce = (e) => $((t) => t.length === 1 ? t : t.filter((t, n) => n !== e));
|
|
33
|
+
return /* @__PURE__ */ c(o, { children: [
|
|
34
|
+
/* @__PURE__ */ c(g, { children: [/* @__PURE__ */ s(_, { children: M ? "Edit maintenance plan" : "New maintenance plan" }), /* @__PURE__ */ s(m, { children: "A recurring schedule. When it falls due it raises a maintenance job, and completing that job starts the next cycle." })] }),
|
|
35
|
+
/* @__PURE__ */ c("div", {
|
|
36
|
+
className: "space-y-4",
|
|
37
|
+
children: [
|
|
38
|
+
!M && /* @__PURE__ */ c("div", {
|
|
39
|
+
className: "space-y-2",
|
|
40
|
+
children: [/* @__PURE__ */ s(d, { children: "Asset *" }), /* @__PURE__ */ c(v, {
|
|
41
|
+
value: N,
|
|
42
|
+
onValueChange: re,
|
|
43
|
+
children: [/* @__PURE__ */ s(x, { children: /* @__PURE__ */ s(S, { placeholder: "Select an asset" }) }), /* @__PURE__ */ s(y, { children: O.map((e) => /* @__PURE__ */ c(b, {
|
|
44
|
+
value: e.id,
|
|
45
|
+
children: [e.name, e.assetTag ? ` · ${e.assetTag}` : ""]
|
|
46
|
+
}, e.id)) })]
|
|
47
|
+
})]
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ c("div", {
|
|
50
|
+
className: "space-y-2",
|
|
51
|
+
children: [/* @__PURE__ */ s(d, { children: "Title *" }), /* @__PURE__ */ s(u, {
|
|
52
|
+
placeholder: "e.g., 250-hour tractor service",
|
|
53
|
+
value: P,
|
|
54
|
+
onChange: (e) => F(e.target.value)
|
|
55
|
+
})]
|
|
56
|
+
}),
|
|
57
|
+
/* @__PURE__ */ c("div", {
|
|
58
|
+
className: "space-y-2",
|
|
59
|
+
children: [/* @__PURE__ */ s(d, { children: "Description" }), /* @__PURE__ */ s(C, {
|
|
60
|
+
placeholder: "What does this service involve?",
|
|
61
|
+
value: I,
|
|
62
|
+
onChange: (e) => L(e.target.value),
|
|
63
|
+
rows: 2
|
|
64
|
+
})]
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ c("div", {
|
|
67
|
+
className: "grid gap-4 md:grid-cols-2",
|
|
68
|
+
children: [/* @__PURE__ */ c("div", {
|
|
69
|
+
className: "space-y-2",
|
|
70
|
+
children: [/* @__PURE__ */ s(d, { children: "Type" }), /* @__PURE__ */ c(v, {
|
|
71
|
+
value: R,
|
|
72
|
+
onValueChange: z,
|
|
73
|
+
children: [/* @__PURE__ */ s(x, { children: /* @__PURE__ */ s(S, {}) }), /* @__PURE__ */ s(y, { children: r.map((e) => /* @__PURE__ */ s(b, {
|
|
74
|
+
value: e,
|
|
75
|
+
children: w(e)
|
|
76
|
+
}, e)) })]
|
|
77
|
+
})]
|
|
78
|
+
}), /* @__PURE__ */ c("div", {
|
|
79
|
+
className: "space-y-2",
|
|
80
|
+
children: [/* @__PURE__ */ s(d, { children: "Priority" }), /* @__PURE__ */ c(v, {
|
|
81
|
+
value: B,
|
|
82
|
+
onValueChange: V,
|
|
83
|
+
children: [/* @__PURE__ */ s(x, { children: /* @__PURE__ */ s(S, {}) }), /* @__PURE__ */ s(y, { children: n.map((e) => /* @__PURE__ */ s(b, {
|
|
84
|
+
value: e,
|
|
85
|
+
children: w(e)
|
|
86
|
+
}, e)) })]
|
|
87
|
+
})]
|
|
88
|
+
})]
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ c("div", {
|
|
91
|
+
className: "rounded-lg border border-border-subtle bg-bg-sunken p-4",
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ s("p", {
|
|
94
|
+
className: "text-sm font-medium text-text-primary",
|
|
95
|
+
children: "How often? *"
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ s("p", {
|
|
98
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
99
|
+
children: "Set at least one. With both, the plan comes due at whichever arrives first — the way a service book reads “every 90 days or 250 hours”."
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ c("div", {
|
|
102
|
+
className: "mt-3 grid gap-4 md:grid-cols-2",
|
|
103
|
+
children: [/* @__PURE__ */ c("div", {
|
|
104
|
+
className: "space-y-2",
|
|
105
|
+
children: [/* @__PURE__ */ s(d, { children: "Every N days" }), /* @__PURE__ */ s(u, {
|
|
106
|
+
type: "number",
|
|
107
|
+
min: 1,
|
|
108
|
+
placeholder: "90",
|
|
109
|
+
value: H,
|
|
110
|
+
onChange: (e) => U(e.target.value)
|
|
111
|
+
})]
|
|
112
|
+
}), /* @__PURE__ */ c("div", {
|
|
113
|
+
className: "space-y-2",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ s(d, { children: "Every N usage units" }),
|
|
116
|
+
/* @__PURE__ */ s(u, {
|
|
117
|
+
type: "number",
|
|
118
|
+
min: 1,
|
|
119
|
+
placeholder: "250",
|
|
120
|
+
value: W,
|
|
121
|
+
onChange: (e) => G(e.target.value)
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ s("p", {
|
|
124
|
+
className: "text-xs text-text-tertiary",
|
|
125
|
+
children: "Counts against the asset’s usage meter. Assets without one never come due this way."
|
|
126
|
+
})
|
|
127
|
+
]
|
|
128
|
+
})]
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
}),
|
|
132
|
+
/* @__PURE__ */ c("div", {
|
|
133
|
+
className: "grid gap-4 md:grid-cols-3",
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ c("div", {
|
|
136
|
+
className: "space-y-2",
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ s(d, { children: "Start counting from" }),
|
|
139
|
+
/* @__PURE__ */ s(u, {
|
|
140
|
+
type: "date",
|
|
141
|
+
value: K,
|
|
142
|
+
onChange: (e) => q(e.target.value)
|
|
143
|
+
}),
|
|
144
|
+
/* @__PURE__ */ s("p", {
|
|
145
|
+
className: "text-xs text-text-tertiary",
|
|
146
|
+
children: "Defaults to today."
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
}),
|
|
150
|
+
/* @__PURE__ */ c("div", {
|
|
151
|
+
className: "space-y-2",
|
|
152
|
+
children: [/* @__PURE__ */ s(d, { children: "Meter at start" }), /* @__PURE__ */ s(u, {
|
|
153
|
+
type: "number",
|
|
154
|
+
min: 0,
|
|
155
|
+
placeholder: "Current reading",
|
|
156
|
+
value: J,
|
|
157
|
+
onChange: (e) => Y(e.target.value)
|
|
158
|
+
})]
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ c("div", {
|
|
161
|
+
className: "space-y-2",
|
|
162
|
+
children: [/* @__PURE__ */ s(d, { children: "Raise work N days early" }), /* @__PURE__ */ s(u, {
|
|
163
|
+
type: "number",
|
|
164
|
+
min: 0,
|
|
165
|
+
placeholder: "0",
|
|
166
|
+
value: X,
|
|
167
|
+
onChange: (e) => ie(e.target.value)
|
|
168
|
+
})]
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
}),
|
|
172
|
+
/* @__PURE__ */ c("div", {
|
|
173
|
+
className: "space-y-2",
|
|
174
|
+
children: [/* @__PURE__ */ s(d, { children: "Estimated cost" }), /* @__PURE__ */ s(u, {
|
|
175
|
+
type: "number",
|
|
176
|
+
min: 0,
|
|
177
|
+
placeholder: "0.00",
|
|
178
|
+
value: Z,
|
|
179
|
+
onChange: (e) => ae(e.target.value)
|
|
180
|
+
})]
|
|
181
|
+
}),
|
|
182
|
+
/* @__PURE__ */ c("div", {
|
|
183
|
+
className: "space-y-2",
|
|
184
|
+
children: [
|
|
185
|
+
/* @__PURE__ */ s(d, { children: "Checklist template" }),
|
|
186
|
+
/* @__PURE__ */ s("p", {
|
|
187
|
+
className: "text-xs text-text-tertiary",
|
|
188
|
+
children: "Copied onto every job this plan raises, so the procedure travels with the schedule."
|
|
189
|
+
}),
|
|
190
|
+
Q.map((e, t) => /* @__PURE__ */ c("div", {
|
|
191
|
+
className: "flex items-center gap-2",
|
|
192
|
+
children: [/* @__PURE__ */ s(u, {
|
|
193
|
+
placeholder: `Step ${t + 1}`,
|
|
194
|
+
value: e,
|
|
195
|
+
onChange: (e) => oe(t, e.target.value)
|
|
196
|
+
}), /* @__PURE__ */ s(l, {
|
|
197
|
+
type: "button",
|
|
198
|
+
variant: "ghost",
|
|
199
|
+
size: "icon",
|
|
200
|
+
onClick: () => ce(t),
|
|
201
|
+
"aria-label": `Remove step ${t + 1}`,
|
|
202
|
+
children: /* @__PURE__ */ s(ne, { className: "size-4" })
|
|
203
|
+
})]
|
|
204
|
+
}, t)),
|
|
205
|
+
/* @__PURE__ */ c(l, {
|
|
206
|
+
type: "button",
|
|
207
|
+
variant: "outline",
|
|
208
|
+
size: "sm",
|
|
209
|
+
onClick: se,
|
|
210
|
+
children: [/* @__PURE__ */ s(te, { className: "mr-1 size-4" }), " Add step"]
|
|
211
|
+
})
|
|
212
|
+
]
|
|
213
|
+
})
|
|
214
|
+
]
|
|
215
|
+
}),
|
|
216
|
+
/* @__PURE__ */ c(h, { children: [/* @__PURE__ */ s(l, {
|
|
217
|
+
variant: "outline",
|
|
218
|
+
onClick: () => p(!1),
|
|
219
|
+
children: "Cancel"
|
|
220
|
+
}), /* @__PURE__ */ c(l, {
|
|
221
|
+
onClick: () => {
|
|
222
|
+
if (!M && !N) {
|
|
223
|
+
t.warning("Asset required", "Pick the machine this plan is for.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!P.trim()) {
|
|
227
|
+
t.warning("Title required", "Give the plan a name, e.g. \"250-hour tractor service\".");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let e = E(H), n = E(W);
|
|
231
|
+
if (e === null && n === null) {
|
|
232
|
+
t.warning("This plan needs an interval", "Set a day interval, a usage interval, or both — otherwise it can never come due.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let r = Q.map((e) => e.trim()).filter((e) => e !== "").map((e) => ({
|
|
236
|
+
task: e,
|
|
237
|
+
completed: !1
|
|
238
|
+
})), i = {
|
|
239
|
+
title: P.trim(),
|
|
240
|
+
description: I.trim() || null,
|
|
241
|
+
type: R,
|
|
242
|
+
priority: B,
|
|
243
|
+
intervalDays: e,
|
|
244
|
+
intervalMeter: n,
|
|
245
|
+
startAt: K ? new Date(K).toISOString() : null,
|
|
246
|
+
startMeter: E(J),
|
|
247
|
+
leadTimeDays: E(X),
|
|
248
|
+
estimatedCost: E(Z),
|
|
249
|
+
checklistTemplate: r
|
|
250
|
+
}, a = () => p(!1);
|
|
251
|
+
M && f ? A({ variables: {
|
|
252
|
+
id: f.id,
|
|
253
|
+
input: i
|
|
254
|
+
} }).then(a) : k({ variables: { input: {
|
|
255
|
+
...i,
|
|
256
|
+
assetId: N
|
|
257
|
+
} } }).then(a);
|
|
258
|
+
},
|
|
259
|
+
disabled: j,
|
|
260
|
+
children: [j && /* @__PURE__ */ s(ee, { className: "mr-2 size-4 animate-spin" }), M ? "Save plan" : "Create plan"]
|
|
261
|
+
})] })
|
|
262
|
+
] });
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { O as MaintenancePlanDialog };
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=MaintenancePlanDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MaintenancePlanDialog.js","names":[],"sources":["../../../src/assethandler/ops/MaintenancePlanDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { Loader2, Plus, X } from 'lucide-react';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { notify } from '../utils/toast';\nimport {\n PLAN_PRIORITIES,\n PLAN_TYPES,\n type MaintenancePlan,\n useMaintenancePlans,\n} from '../hooks/useMaintenancePlans';\n\n/**\n * Create or edit a recurring maintenance plan (BOFF-6031).\n *\n * The one rule this form has to get right: a plan needs AT LEAST ONE interval.\n * The backend refuses a plan without one — such a plan can never come due, which\n * is the exact gap the feature closes — so the form says so up front rather than\n * letting someone fill in eight fields and be rejected on submit.\n *\n * Only the four backend plan types are offered. The one-off \"Schedule\n * Maintenance\" dialog also lists CALIBRATION and EMERGENCY, which a free-form\n * MaintenanceRecord.type accepts but a plan does not.\n */\n\nconst humanize = (value: string) =>\n value\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/^\\w/, (c) => c.toUpperCase());\n\n/** `<input type=\"date\">` wants YYYY-MM-DD; the API speaks ISO-8601. */\nconst toDateInput = (iso: string | null): string => (iso ? iso.slice(0, 10) : '');\n\nconst numberOrNull = (raw: string): number | null => {\n const trimmed = raw.trim();\n if (trimmed === '') return null;\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : null;\n};\n\nconst checklistTasks = (plan?: MaintenancePlan | null): string[] => {\n const tasks = (plan?.checklistTemplate ?? [])\n .map((item) => (typeof item?.task === 'string' ? item.task : ''))\n .filter((task) => task !== '');\n return tasks.length > 0 ? tasks : [''];\n};\n\n/**\n * The dialog is a thin shell; the form below is REMOUNTED per plan via `key`.\n *\n * That is what keeps the fields correct when you edit one plan, close, then edit\n * another. Re-seeding the same mounted form from an effect would do the same job\n * but through a cascade of setState calls during render — which React (and this\n * repo's lint) rightly rejects. A fresh mount reads its initial state straight\n * from props, with no synchronisation to get wrong.\n */\nexport function MaintenancePlanDialog({\n open,\n onOpenChange,\n plan,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Present when editing; absent when creating. */\n plan?: MaintenancePlan | null;\n}) {\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n {open && (\n <MaintenancePlanForm\n key={plan?.id ?? 'new'}\n plan={plan ?? null}\n onOpenChange={onOpenChange}\n />\n )}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction MaintenancePlanForm({\n plan,\n onOpenChange,\n}: {\n plan: MaintenancePlan | null;\n onOpenChange: (open: boolean) => void;\n}) {\n const { assets } = useAssetHandlerStore();\n const { createPlan, updatePlan, busy } = useMaintenancePlans();\n const isEdit = Boolean(plan);\n\n const [assetId, setAssetId] = useState(plan?.assetId ?? '');\n const [title, setTitle] = useState(plan?.title ?? '');\n const [description, setDescription] = useState(plan?.description ?? '');\n const [type, setType] = useState<string>(plan?.type ?? 'PREVENTIVE');\n const [priority, setPriority] = useState<string>(plan?.priority ?? 'MEDIUM');\n const [intervalDays, setIntervalDays] = useState(\n plan?.intervalDays != null ? String(plan.intervalDays) : ''\n );\n const [intervalMeter, setIntervalMeter] = useState(\n plan?.intervalMeter != null ? String(plan.intervalMeter) : ''\n );\n const [startAt, setStartAt] = useState(toDateInput(plan?.startAt ?? null));\n const [startMeter, setStartMeter] = useState(\n plan?.startMeter != null ? String(plan.startMeter) : ''\n );\n const [leadTimeDays, setLeadTimeDays] = useState(\n plan?.leadTimeDays != null ? String(plan.leadTimeDays) : ''\n );\n const [estimatedCost, setEstimatedCost] = useState(\n plan?.estimatedCost != null ? String(plan.estimatedCost) : ''\n );\n const [checklist, setChecklist] = useState<string[]>(checklistTasks(plan));\n\n const setChecklistItem = (index: number, value: string) =>\n setChecklist((prev) => prev.map((task, i) => (i === index ? value : task)));\n const addChecklistItem = () => setChecklist((prev) => [...prev, '']);\n const removeChecklistItem = (index: number) =>\n setChecklist((prev) => (prev.length === 1 ? prev : prev.filter((_, i) => i !== index)));\n\n const handleSubmit = () => {\n if (!isEdit && !assetId) {\n notify.warning('Asset required', 'Pick the machine this plan is for.');\n return;\n }\n if (!title.trim()) {\n notify.warning('Title required', 'Give the plan a name, e.g. \"250-hour tractor service\".');\n return;\n }\n const days = numberOrNull(intervalDays);\n const meter = numberOrNull(intervalMeter);\n if (days === null && meter === null) {\n notify.warning(\n 'This plan needs an interval',\n 'Set a day interval, a usage interval, or both — otherwise it can never come due.'\n );\n return;\n }\n\n const cleanedChecklist = checklist\n .map((task) => task.trim())\n .filter((task) => task !== '')\n .map((task) => ({ task, completed: false }));\n\n const input = {\n title: title.trim(),\n description: description.trim() || null,\n type,\n priority,\n intervalDays: days,\n intervalMeter: meter,\n startAt: startAt ? new Date(startAt).toISOString() : null,\n startMeter: numberOrNull(startMeter),\n leadTimeDays: numberOrNull(leadTimeDays),\n estimatedCost: numberOrNull(estimatedCost),\n checklistTemplate: cleanedChecklist,\n };\n\n const done = () => onOpenChange(false);\n if (isEdit && plan) {\n void updatePlan({ variables: { id: plan.id, input } }).then(done);\n } else {\n void createPlan({ variables: { input: { ...input, assetId } } }).then(done);\n }\n };\n\n return (\n <>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {isEdit ? 'Edit maintenance plan' : 'New maintenance plan'}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n A recurring schedule. When it falls due it raises a maintenance job, and completing that\n job starts the next cycle.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"space-y-4\">\n {!isEdit && (\n <div className=\"space-y-2\">\n <Label>Asset *</Label>\n <Select value={assetId} onValueChange={setAssetId}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select an asset\" />\n </SelectTrigger>\n <SelectContent>\n {assets.map((asset) => (\n <SelectItem key={asset.id} value={asset.id}>\n {asset.name}\n {asset.assetTag ? ` · ${asset.assetTag}` : ''}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n <div className=\"space-y-2\">\n <Label>Title *</Label>\n <Input\n placeholder=\"e.g., 250-hour tractor service\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n </div>\n\n <div className=\"space-y-2\">\n <Label>Description</Label>\n <Textarea\n placeholder=\"What does this service involve?\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Type</Label>\n <Select value={type} onValueChange={setType}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PLAN_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {humanize(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Priority</Label>\n <Select value={priority} onValueChange={setPriority}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PLAN_PRIORITIES.map((p) => (\n <SelectItem key={p} value={p}>\n {humanize(p)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken p-4\">\n <p className=\"text-sm font-medium text-text-primary\">How often? *</p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Set at least one. With both, the plan comes due at whichever arrives first — the way a\n service book reads “every 90 days or 250 hours”.\n </p>\n <div className=\"mt-3 grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Every N days</Label>\n <Input\n type=\"number\"\n min={1}\n placeholder=\"90\"\n value={intervalDays}\n onChange={(e) => setIntervalDays(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Every N usage units</Label>\n <Input\n type=\"number\"\n min={1}\n placeholder=\"250\"\n value={intervalMeter}\n onChange={(e) => setIntervalMeter(e.target.value)}\n />\n <p className=\"text-xs text-text-tertiary\">\n Counts against the asset’s usage meter. Assets without one never come due this\n way.\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Start counting from</Label>\n <Input type=\"date\" value={startAt} onChange={(e) => setStartAt(e.target.value)} />\n <p className=\"text-xs text-text-tertiary\">Defaults to today.</p>\n </div>\n <div className=\"space-y-2\">\n <Label>Meter at start</Label>\n <Input\n type=\"number\"\n min={0}\n placeholder=\"Current reading\"\n value={startMeter}\n onChange={(e) => setStartMeter(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Raise work N days early</Label>\n <Input\n type=\"number\"\n min={0}\n placeholder=\"0\"\n value={leadTimeDays}\n onChange={(e) => setLeadTimeDays(e.target.value)}\n />\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label>Estimated cost</Label>\n <Input\n type=\"number\"\n min={0}\n placeholder=\"0.00\"\n value={estimatedCost}\n onChange={(e) => setEstimatedCost(e.target.value)}\n />\n </div>\n\n <div className=\"space-y-2\">\n <Label>Checklist template</Label>\n <p className=\"text-xs text-text-tertiary\">\n Copied onto every job this plan raises, so the procedure travels with the schedule.\n </p>\n {checklist.map((task, index) => (\n <div key={index} className=\"flex items-center gap-2\">\n <Input\n placeholder={`Step ${index + 1}`}\n value={task}\n onChange={(e) => setChecklistItem(index, e.target.value)}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeChecklistItem(index)}\n aria-label={`Remove step ${index + 1}`}\n >\n <X className=\"size-4\" />\n </Button>\n </div>\n ))}\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={addChecklistItem}>\n <Plus className=\"mr-1 size-4\" /> Add step\n </Button>\n </div>\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={busy}>\n {busy && <Loader2 className=\"mr-2 size-4 animate-spin\" />}\n {isEdit ? 'Save plan' : 'Create plan'}\n </Button>\n </ResponsiveDialogFooter>\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetsAssetCategoriesPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AssetsAssetCategoriesPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"names":[],"mappings":"AA8GA,wBAAgB,yBAAyB,gCA0VxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetsAssetCategoriesPage.js","names":[],"sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n GradientText,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Box,\n Monitor,\n Server,\n Wifi,\n Truck,\n Wrench,\n Code,\n MoreHorizontal,\n Copy,\n ExternalLink,\n ArrowUpDown,\n Layers,\n} from 'lucide-react';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { formatAssetCategory, useAssetCategoryPresentation, useAssetTerminology, useHiddenCategoryIds } from '../providers/AssetHandlerProvider';\nimport { useAssetHandlerStore, type AssetStatus } from '../store/assethandlerStore';\nimport { notify } from '../utils/toast';\nimport {\n ChartCard,\n StatTile,\n BarsChart,\n formatCompact,\n formatCurrencyCompact,\n} from '../components';\n\nconst categoryIcons: Record<string, React.ReactNode> = {\n electronics: <Monitor className=\"h-5 w-5\" />,\n it_hardware: <Monitor className=\"h-5 w-5\" />,\n furniture: <Box className=\"h-5 w-5\" />,\n vehicles: <Truck className=\"h-5 w-5\" />,\n equipment: <Wrench className=\"h-5 w-5\" />,\n software: <Code className=\"h-5 w-5\" />,\n networking: <Wifi className=\"h-5 w-5\" />,\n servers: <Server className=\"h-5 w-5\" />,\n};\n\nconst categoryColors: Record<string, string> = {\n electronics: 'bg-status-info-bg-subtle text-status-info-text',\n it_hardware: 'bg-status-info-bg-subtle text-status-info-text',\n furniture: 'bg-status-warning-bg-subtle text-status-warning-text',\n vehicles: 'bg-status-success-bg-subtle text-status-success-text',\n equipment: 'bg-status-warning-bg-subtle text-status-warning-text',\n software: 'bg-action-primary-bg/10 text-action-primary-bg',\n networking: 'bg-status-info-bg-subtle text-status-info-text',\n servers: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\n// Stable fallback tint for any category outside the known slug map — hashes the\n// key into the semantic palette so every real category is visually differentiated.\nconst FALLBACK_TINTS = [\n 'bg-status-info-bg-subtle text-status-info-text',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-status-error-bg-subtle text-status-error-text',\n 'bg-action-primary-bg/10 text-action-primary-bg',\n];\nfunction tintFor(key: string): string {\n if (categoryColors[key]) return categoryColors[key];\n let hash = 0;\n for (let i = 0; i < key.length; i += 1) hash = (hash * 31 + key.charCodeAt(i)) >>> 0;\n return FALLBACK_TINTS[hash % FALLBACK_TINTS.length];\n}\n\n// Proportional status-bar segments — semantic tokens (CSS vars), theme-aware.\nconst STATUS_SEGMENTS: { key: AssetStatus; label: string; varName: string }[] = [\n { key: 'active', label: 'Active', varName: 'var(--color-status-success-bg)' },\n { key: 'maintenance', label: 'Maintenance', varName: 'var(--color-status-warning-bg)' },\n { key: 'retired', label: 'Retired', varName: 'var(--color-status-error-bg)' },\n { key: 'inactive', label: 'Inactive', varName: 'var(--color-status-info-bg)' },\n { key: 'disposed', label: 'Disposed', varName: 'var(--color-text-muted)' },\n];\n\nconst CONDITION_SCORE: Record<string, number> = {\n excellent: 5,\n good: 4,\n fair: 3,\n poor: 2,\n damaged: 1,\n};\n\ninterface CategoryAggregate {\n key: string;\n count: number;\n totalValue: number;\n statuses: Record<string, number>;\n conditions: Record<string, number>;\n avgCondition: number;\n}\n\nconst money = (value: number): string => `$${value.toLocaleString()}`;\n\nexport function AssetsAssetCategoriesPage() {\n const navigate = useAssetHandlerNavigate();\n const term = useAssetTerminology();\n const hiddenCategories = useHiddenCategoryIds();\n const { labels: categoryLabels } = useAssetCategoryPresentation();\n const { assets: allAssets } = useAssetHandlerStore();\n const assets = useMemo(\n () => allAssets.filter((a) => !hiddenCategories.has(a.category)),\n [allAssets, hiddenCategories]\n );\n const [chartMetric, setChartMetric] = useState<'count' | 'value'>('count');\n const [sortKey, setSortKey] = useState<'count' | 'value'>('count');\n\n const aggregates = useMemo<CategoryAggregate[]>(() => {\n const map: Record<string, CategoryAggregate> = {};\n for (const asset of assets) {\n if (!map[asset.category]) {\n map[asset.category] = {\n key: asset.category,\n count: 0,\n totalValue: 0,\n statuses: {},\n conditions: {},\n avgCondition: 0,\n };\n }\n const cat = map[asset.category];\n cat.count += 1;\n cat.totalValue += asset.currentValue ?? asset.purchaseValue ?? 0;\n cat.statuses[asset.status] = (cat.statuses[asset.status] || 0) + 1;\n cat.conditions[asset.condition] = (cat.conditions[asset.condition] || 0) + 1;\n }\n for (const cat of Object.values(map)) {\n let scoreSum = 0;\n let scoreN = 0;\n for (const [cond, n] of Object.entries(cat.conditions)) {\n const score = CONDITION_SCORE[cond];\n if (score) {\n scoreSum += score * n;\n scoreN += n;\n }\n }\n cat.avgCondition = scoreN > 0 ? scoreSum / scoreN : 0;\n }\n return Object.values(map).sort((a, b) => b.count - a.count);\n }, [assets]);\n\n const totalAssets = assets.length;\n const totalValue = useMemo(\n () => assets.reduce((sum, a) => sum + (a.currentValue ?? a.purchaseValue ?? 0), 0),\n [assets]\n );\n\n const chartData = useMemo(\n () =>\n aggregates.map((cat) => ({\n category: formatAssetCategory(cat.key, categoryLabels),\n count: cat.count,\n value: cat.totalValue,\n })),\n [aggregates, categoryLabels]\n );\n\n const sortedRows = useMemo(\n () =>\n [...aggregates].sort((a, b) =>\n sortKey === 'count' ? b.count - a.count : b.totalValue - a.totalValue\n ),\n [aggregates, sortKey]\n );\n\n const copyId = (key: string) => {\n void navigator.clipboard?.writeText(key);\n notify.success('Category copied', `\"${formatAssetCategory(key, categoryLabels)}\" copied to clipboard.`);\n };\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <div>\n <h1 className=\"text-2xl font-bold\">\n <GradientText>{term.asset} Categories</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {aggregates.length} categories · {totalAssets} total {term.assets.toLowerCase()}\n </p>\n </div>\n\n <PagePurpose>\n Group and compare your assets by category to see where value and volume are concentrated.\n Each card rolls up how many assets you hold, their total book value and a proportional\n status mix — so you can plan procurement and replacements by category, not one asset at a\n time.\n </PagePurpose>\n\n {aggregates.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No categories yet\"\n description={`Add ${term.assets.toLowerCase()} and they will be grouped into categories here automatically.`}\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <StatTile\n label=\"Categories\"\n value={aggregates.length}\n format={formatCompact}\n icon={Layers}\n hero\n />\n <StatTile\n label={`${term.assets} tracked`}\n value={totalAssets}\n format={formatCompact}\n icon={Box}\n />\n <StatTile\n label=\"Total book value\"\n value={totalValue}\n format={formatCurrencyCompact('USD')}\n icon={Box}\n />\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {aggregates.map((cat, idx) => {\n const pct = totalAssets > 0 ? Math.round((cat.count / totalAssets) * 100) : 0;\n const segments = STATUS_SEGMENTS.filter((s) => (cat.statuses[s.key] || 0) > 0);\n return (\n <GlassCard\n key={cat.key}\n treatment={idx === 0 ? 'glass' : 'plain'}\n glow={idx === 0}\n className=\"border-border-seam\"\n >\n <div className=\"space-y-4 p-5\">\n {/* Header: icon + title + overflow */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className={`shrink-0 rounded-lg p-2 ${tintFor(cat.key)}`}>\n {categoryIcons[cat.key] || <Box className=\"h-5 w-5\" />}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-base font-semibold capitalize\">\n {formatAssetCategory(cat.key, categoryLabels)}\n </p>\n <Badge variant=\"outline\" className=\"mt-0.5 text-xs\">\n {pct}% of estate\n </Badge>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 shrink-0\"\n aria-label={`Actions for ${formatAssetCategory(cat.key, categoryLabels)}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => navigate('/assets/asset-list')}>\n <ExternalLink className=\"mr-2 h-4 w-4\" />\n View {term.assets.toLowerCase()}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => copyId(cat.key)}>\n <Copy className=\"mr-2 h-4 w-4\" />\n Copy category ID\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n\n {/* Dual metric row */}\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">{term.asset} count</p>\n <p className=\"text-2xl font-bold tabular-nums\">{cat.count}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-xs text-text-secondary\">Total value</p>\n <p className=\"text-2xl font-bold tabular-nums\">\n {formatCurrencyCompact('USD')(cat.totalValue)}\n </p>\n </div>\n </div>\n\n {/* Proportional status bar */}\n <div>\n <div\n className=\"flex h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={segments\n .map((s) => `${s.label}: ${cat.statuses[s.key]}`)\n .join(', ')}\n >\n {segments.map((s) => (\n <div\n key={s.key}\n style={{\n width: `${((cat.statuses[s.key] || 0) / cat.count) * 100}%`,\n backgroundColor: s.varName,\n }}\n />\n ))}\n </div>\n <div className=\"mt-2 flex flex-wrap gap-x-3 gap-y-1\">\n {segments.map((s) => (\n <span\n key={s.key}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n <span\n className=\"h-2.5 w-2.5 rounded-[3px]\"\n style={{ backgroundColor: s.varName }}\n />\n {s.label}\n <span className=\"tabular-nums text-text-muted\">\n {cat.statuses[s.key]}\n </span>\n </span>\n ))}\n </div>\n </div>\n\n {/* Avg condition */}\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3 text-sm\">\n <span className=\"text-text-secondary\">Avg. condition</span>\n <span className=\"font-semibold tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n <span className=\"text-xs font-normal text-text-muted\"> / 5</span>\n </span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {/* Cross-category comparison chart */}\n <ChartCard\n title=\"Category comparison\"\n subtitle={\n chartMetric === 'count' ? `${term.assets} per category` : 'Book value per category'\n }\n actions={\n <div className=\"flex gap-1 rounded-md border border-border-seam p-0.5\">\n <Button\n variant={chartMetric === 'count' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('count')}\n >\n Count\n </Button>\n <Button\n variant={chartMetric === 'value' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('value')}\n >\n Value\n </Button>\n </div>\n }\n >\n <BarsChart\n data={chartData}\n xKey=\"category\"\n orientation=\"horizontal\"\n height={Math.max(200, chartData.length * 44)}\n categoryWidth={120}\n valueFormat={chartMetric === 'value' ? formatCurrencyCompact('USD') : formatCompact}\n series={[\n {\n key: chartMetric,\n label: chartMetric === 'count' ? term.assets : 'Book value',\n },\n ]}\n />\n </ChartCard>\n\n {/* Summary comparison table */}\n <ChartCard\n title=\"Category summary\"\n subtitle=\"Sortable roll-up across every category\"\n icon={Layers}\n >\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[520px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-4 font-medium\">Category</th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('count')}\n >\n Count\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'count' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('value')}\n >\n Value\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'value' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 font-medium\">Avg. condition</th>\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((cat) => (\n <tr key={cat.key} className=\"border-b border-border-seam/60 last:border-0\">\n <td className=\"py-2.5 pr-4 font-medium capitalize\">\n {formatAssetCategory(cat.key, categoryLabels)}\n </td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{cat.count}</td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{money(cat.totalValue)}</td>\n <td className=\"py-2.5 tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </ChartCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetsAssetCategoriesPage.js","names":[],"sources":["../../../src/assethandler/pages/AssetsAssetCategoriesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n GradientText,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Box,\n Monitor,\n Server,\n Wifi,\n Truck,\n Wrench,\n Code,\n MoreHorizontal,\n Copy,\n ExternalLink,\n ArrowUpDown,\n Layers,\n} from 'lucide-react';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport {\n formatAssetCategory,\n useAssetCategoryPresentation,\n useAssetTerminology,\n useHiddenCategoryIds,\n} from '../providers/AssetHandlerProvider';\nimport { useAssetHandlerStore, type AssetStatus } from '../store/assethandlerStore';\nimport { notify } from '../utils/toast';\nimport {\n ChartCard,\n StatTile,\n BarsChart,\n formatCompact,\n formatCurrencyCompact,\n} from '../components';\n\nconst categoryIcons: Record<string, React.ReactNode> = {\n electronics: <Monitor className=\"h-5 w-5\" />,\n it_hardware: <Monitor className=\"h-5 w-5\" />,\n furniture: <Box className=\"h-5 w-5\" />,\n vehicles: <Truck className=\"h-5 w-5\" />,\n equipment: <Wrench className=\"h-5 w-5\" />,\n software: <Code className=\"h-5 w-5\" />,\n networking: <Wifi className=\"h-5 w-5\" />,\n servers: <Server className=\"h-5 w-5\" />,\n};\n\nconst categoryColors: Record<string, string> = {\n electronics: 'bg-status-info-bg-subtle text-status-info-text',\n it_hardware: 'bg-status-info-bg-subtle text-status-info-text',\n furniture: 'bg-status-warning-bg-subtle text-status-warning-text',\n vehicles: 'bg-status-success-bg-subtle text-status-success-text',\n equipment: 'bg-status-warning-bg-subtle text-status-warning-text',\n software: 'bg-action-primary-bg/10 text-action-primary-bg',\n networking: 'bg-status-info-bg-subtle text-status-info-text',\n servers: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\n// Stable fallback tint for any category outside the known slug map — hashes the\n// key into the semantic palette so every real category is visually differentiated.\nconst FALLBACK_TINTS = [\n 'bg-status-info-bg-subtle text-status-info-text',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-status-error-bg-subtle text-status-error-text',\n 'bg-action-primary-bg/10 text-action-primary-bg',\n];\nfunction tintFor(key: string): string {\n if (categoryColors[key]) return categoryColors[key];\n let hash = 0;\n for (let i = 0; i < key.length; i += 1) hash = (hash * 31 + key.charCodeAt(i)) >>> 0;\n return FALLBACK_TINTS[hash % FALLBACK_TINTS.length];\n}\n\n// Proportional status-bar segments — semantic tokens (CSS vars), theme-aware.\nconst STATUS_SEGMENTS: { key: AssetStatus; label: string; varName: string }[] = [\n { key: 'active', label: 'Active', varName: 'var(--color-status-success-bg)' },\n { key: 'maintenance', label: 'Maintenance', varName: 'var(--color-status-warning-bg)' },\n { key: 'retired', label: 'Retired', varName: 'var(--color-status-error-bg)' },\n { key: 'inactive', label: 'Inactive', varName: 'var(--color-status-info-bg)' },\n { key: 'disposed', label: 'Disposed', varName: 'var(--color-text-muted)' },\n];\n\nconst CONDITION_SCORE: Record<string, number> = {\n excellent: 5,\n good: 4,\n fair: 3,\n poor: 2,\n damaged: 1,\n};\n\ninterface CategoryAggregate {\n key: string;\n count: number;\n totalValue: number;\n statuses: Record<string, number>;\n conditions: Record<string, number>;\n avgCondition: number;\n}\n\nconst money = (value: number): string => `$${value.toLocaleString()}`;\n\nexport function AssetsAssetCategoriesPage() {\n const navigate = useAssetHandlerNavigate();\n const term = useAssetTerminology();\n const hiddenCategories = useHiddenCategoryIds();\n const { labels: categoryLabels } = useAssetCategoryPresentation();\n const { assets: allAssets } = useAssetHandlerStore();\n const assets = useMemo(\n () => allAssets.filter((a) => !hiddenCategories.has(a.category)),\n [allAssets, hiddenCategories]\n );\n const [chartMetric, setChartMetric] = useState<'count' | 'value'>('count');\n const [sortKey, setSortKey] = useState<'count' | 'value'>('count');\n\n const aggregates = useMemo<CategoryAggregate[]>(() => {\n const map: Record<string, CategoryAggregate> = {};\n for (const asset of assets) {\n if (!map[asset.category]) {\n map[asset.category] = {\n key: asset.category,\n count: 0,\n totalValue: 0,\n statuses: {},\n conditions: {},\n avgCondition: 0,\n };\n }\n const cat = map[asset.category];\n cat.count += 1;\n cat.totalValue += asset.currentValue ?? asset.purchaseValue ?? 0;\n cat.statuses[asset.status] = (cat.statuses[asset.status] || 0) + 1;\n cat.conditions[asset.condition] = (cat.conditions[asset.condition] || 0) + 1;\n }\n for (const cat of Object.values(map)) {\n let scoreSum = 0;\n let scoreN = 0;\n for (const [cond, n] of Object.entries(cat.conditions)) {\n const score = CONDITION_SCORE[cond];\n if (score) {\n scoreSum += score * n;\n scoreN += n;\n }\n }\n cat.avgCondition = scoreN > 0 ? scoreSum / scoreN : 0;\n }\n return Object.values(map).sort((a, b) => b.count - a.count);\n }, [assets]);\n\n const totalAssets = assets.length;\n const totalValue = useMemo(\n () => assets.reduce((sum, a) => sum + (a.currentValue ?? a.purchaseValue ?? 0), 0),\n [assets]\n );\n\n const chartData = useMemo(\n () =>\n aggregates.map((cat) => ({\n category: formatAssetCategory(cat.key, categoryLabels),\n count: cat.count,\n value: cat.totalValue,\n })),\n [aggregates, categoryLabels]\n );\n\n const sortedRows = useMemo(\n () =>\n [...aggregates].sort((a, b) =>\n sortKey === 'count' ? b.count - a.count : b.totalValue - a.totalValue\n ),\n [aggregates, sortKey]\n );\n\n const copyId = (key: string) => {\n void navigator.clipboard?.writeText(key);\n notify.success(\n 'Category copied',\n `\"${formatAssetCategory(key, categoryLabels)}\" copied to clipboard.`\n );\n };\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <div>\n <h1 className=\"text-2xl font-bold\">\n <GradientText>{term.asset} Categories</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {aggregates.length} categories · {totalAssets} total {term.assets.toLowerCase()}\n </p>\n </div>\n\n <PagePurpose>\n Group and compare your assets by category to see where value and volume are concentrated.\n Each card rolls up how many assets you hold, their total book value and a proportional\n status mix — so you can plan procurement and replacements by category, not one asset at a\n time.\n </PagePurpose>\n\n {aggregates.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No categories yet\"\n description={`Add ${term.assets.toLowerCase()} and they will be grouped into categories here automatically.`}\n />\n ) : (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <StatTile\n label=\"Categories\"\n value={aggregates.length}\n format={formatCompact}\n icon={Layers}\n hero\n />\n <StatTile\n label={`${term.assets} tracked`}\n value={totalAssets}\n format={formatCompact}\n icon={Box}\n />\n <StatTile\n label=\"Total book value\"\n value={totalValue}\n format={formatCurrencyCompact('USD')}\n icon={Box}\n />\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {aggregates.map((cat, idx) => {\n const pct = totalAssets > 0 ? Math.round((cat.count / totalAssets) * 100) : 0;\n const segments = STATUS_SEGMENTS.filter((s) => (cat.statuses[s.key] || 0) > 0);\n return (\n <GlassCard\n key={cat.key}\n treatment={idx === 0 ? 'glass' : 'plain'}\n glow={idx === 0}\n className=\"border-border-seam\"\n >\n <div className=\"space-y-4 p-5\">\n {/* Header: icon + title + overflow */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className={`shrink-0 rounded-lg p-2 ${tintFor(cat.key)}`}>\n {categoryIcons[cat.key] || <Box className=\"h-5 w-5\" />}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-base font-semibold capitalize\">\n {formatAssetCategory(cat.key, categoryLabels)}\n </p>\n <Badge variant=\"outline\" className=\"mt-0.5 text-xs\">\n {pct}% of estate\n </Badge>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 shrink-0\"\n aria-label={`Actions for ${formatAssetCategory(cat.key, categoryLabels)}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => navigate('/assets/asset-list')}>\n <ExternalLink className=\"mr-2 h-4 w-4\" />\n View {term.assets.toLowerCase()}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => copyId(cat.key)}>\n <Copy className=\"mr-2 h-4 w-4\" />\n Copy category ID\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n\n {/* Dual metric row */}\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">{term.asset} count</p>\n <p className=\"text-2xl font-bold tabular-nums\">{cat.count}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-xs text-text-secondary\">Total value</p>\n <p className=\"text-2xl font-bold tabular-nums\">\n {formatCurrencyCompact('USD')(cat.totalValue)}\n </p>\n </div>\n </div>\n\n {/* Proportional status bar */}\n <div>\n <div\n className=\"flex h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={segments\n .map((s) => `${s.label}: ${cat.statuses[s.key]}`)\n .join(', ')}\n >\n {segments.map((s) => (\n <div\n key={s.key}\n style={{\n width: `${((cat.statuses[s.key] || 0) / cat.count) * 100}%`,\n backgroundColor: s.varName,\n }}\n />\n ))}\n </div>\n <div className=\"mt-2 flex flex-wrap gap-x-3 gap-y-1\">\n {segments.map((s) => (\n <span\n key={s.key}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n <span\n className=\"h-2.5 w-2.5 rounded-[3px]\"\n style={{ backgroundColor: s.varName }}\n />\n {s.label}\n <span className=\"tabular-nums text-text-muted\">\n {cat.statuses[s.key]}\n </span>\n </span>\n ))}\n </div>\n </div>\n\n {/* Avg condition */}\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3 text-sm\">\n <span className=\"text-text-secondary\">Avg. condition</span>\n <span className=\"font-semibold tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n <span className=\"text-xs font-normal text-text-muted\"> / 5</span>\n </span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {/* Cross-category comparison chart */}\n <ChartCard\n title=\"Category comparison\"\n subtitle={\n chartMetric === 'count' ? `${term.assets} per category` : 'Book value per category'\n }\n actions={\n <div className=\"flex gap-1 rounded-md border border-border-seam p-0.5\">\n <Button\n variant={chartMetric === 'count' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('count')}\n >\n Count\n </Button>\n <Button\n variant={chartMetric === 'value' ? 'default' : 'ghost'}\n size=\"sm\"\n className=\"h-7 px-2 text-xs\"\n onClick={() => setChartMetric('value')}\n >\n Value\n </Button>\n </div>\n }\n >\n <BarsChart\n data={chartData}\n xKey=\"category\"\n orientation=\"horizontal\"\n height={Math.max(200, chartData.length * 44)}\n categoryWidth={120}\n valueFormat={chartMetric === 'value' ? formatCurrencyCompact('USD') : formatCompact}\n series={[\n {\n key: chartMetric,\n label: chartMetric === 'count' ? term.assets : 'Book value',\n },\n ]}\n />\n </ChartCard>\n\n {/* Summary comparison table */}\n <ChartCard\n title=\"Category summary\"\n subtitle=\"Sortable roll-up across every category\"\n icon={Layers}\n >\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[520px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-4 font-medium\">Category</th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('count')}\n >\n Count\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'count' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 pr-4 font-medium\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 hover:text-text-primary\"\n onClick={() => setSortKey('value')}\n >\n Value\n <ArrowUpDown\n className={`h-3 w-3 ${sortKey === 'value' ? 'text-action-primary-bg' : ''}`}\n />\n </button>\n </th>\n <th className=\"py-2 font-medium\">Avg. condition</th>\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((cat) => (\n <tr key={cat.key} className=\"border-b border-border-seam/60 last:border-0\">\n <td className=\"py-2.5 pr-4 font-medium capitalize\">\n {formatAssetCategory(cat.key, categoryLabels)}\n </td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{cat.count}</td>\n <td className=\"py-2.5 pr-4 tabular-nums\">{money(cat.totalValue)}</td>\n <td className=\"py-2.5 tabular-nums\">\n {cat.avgCondition > 0 ? cat.avgCondition.toFixed(1) : '—'}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </ChartCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetsAssetCreatePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/AssetsAssetCreatePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AssetsAssetCreatePage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/AssetsAssetCreatePage.tsx"],"names":[],"mappings":"AAiLA,wBAAgB,qBAAqB,gCA6jBpC"}
|