@burdenoff/microfe-assethandler 2026.703.11 → 2026.703.12
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/pages/AiPredictionsPage.js.map +1 -1
- package/dist/assethandler/pages/AnalyticsPage.js.map +1 -1
- package/dist/assethandler/pages/CmdbBrowserPage.js.map +1 -1
- package/dist/assethandler/pages/DashboardPage.js.map +1 -1
- package/dist/assethandler/pages/EsgDashboardPage.js.map +1 -1
- package/dist/assethandler/pages/FinanceFinanceHubPage.js.map +1 -1
- package/dist/assethandler/pages/FinanceInvoiceDetailPage.js.map +1 -1
- package/dist/assethandler/pages/ItilChangeManagementPage.js.map +1 -1
- package/dist/assethandler/pages/ItilIncidentManagementPage.js +2 -1
- package/dist/assethandler/pages/ItilIncidentManagementPage.js.map +1 -1
- package/dist/assethandler/pages/ItilProblemManagementPage.js.map +1 -1
- package/dist/assethandler/pages/KnowledgeBasePage.js.map +1 -1
- package/dist/assethandler/pages/OperationsOperationsHubPage.js.map +1 -1
- package/dist/assethandler/pages/OperationsWorkOrderDetailPage.js.map +1 -1
- package/dist/assethandler/pages/PortfolioPortfolioOverviewPage.js.map +1 -1
- package/dist/assethandler/pages/ProvenancePage.js.map +1 -1
- package/dist/assethandler/pages/ReportsDashboardPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiPredictionsPage.js","names":[],"sources":["../../../src/assethandler/pages/AiPredictionsPage.tsx"],"sourcesContent":["import type React from 'react';\nimport { BrainCircuit, Plus, Wrench, Loader2, BarChart3 } from 'lucide-react';\nimport {\n Button,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Label,\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 { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n ChartCard,\n DonutChart,\n BarsChart,\n RadialGauge,\n seriesColor,\n type DonutSlice,\n} from '../components';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst AI_PREDICTIONS_QUERY = gql`\n query AssetHandlerAiPredictions {\n aiPredictions {\n id\n assetId\n predictionType\n score\n confidence\n recommendation\n features\n status\n generatedAt\n modelVersion\n }\n }\n`;\n\nconst REQUEST_AI_PREDICTION_MUTATION = gql`\n mutation AssetHandlerRequestAiPrediction($input: RequestAIPredictionInput!) {\n requestAIPrediction(input: $input) {\n id\n }\n }\n`;\n\ninterface AiPrediction {\n id: string;\n assetId: string;\n predictionType: string;\n score: number;\n confidence: number;\n recommendation: string;\n features: Record<string, unknown> | null;\n status: string;\n generatedAt: string;\n modelVersion: string;\n}\n\nconst PREDICTION_TYPES = [\n { value: 'maintenance-risk', label: 'Maintenance risk' },\n { value: 'failure-forecast', label: 'Failure forecast' },\n { value: 'utilization-forecast', label: 'Utilization forecast' },\n { value: 'anomaly-detection', label: 'Anomaly detection' },\n { value: 'end-of-life', label: 'End-of-life projection' },\n] as const;\n\nconst humanizePredictionType = (value: string): string =>\n PREDICTION_TYPES.find((t) => t.value === value)?.label ?? value.replace(/-/g, ' ');\n\ntype RiskBand = 'high' | 'medium' | 'low';\n\nfunction riskBandOf(score: number): { band: RiskBand; label: string; tone: 'danger' | 'warning' | 'success' } {\n if (score >= 0.66) return { band: 'high', label: 'High', tone: 'danger' };\n if (score >= 0.33) return { band: 'medium', label: 'Medium', tone: 'warning' };\n return { band: 'low', label: 'Low', tone: 'success' };\n}\n\n/** Extract numeric feature importances from the backend `features` JSONObject. */\nfunction featureBars(features: Record<string, unknown> | null | undefined): { name: string; importance: number }[] {\n if (!features) return [];\n return Object.entries(features)\n .map(([name, value]) => ({ name, importance: typeof value === 'number' ? value : Number(value) }))\n .filter((f) => Number.isFinite(f.importance))\n .sort((a, b) => b.importance - a.importance)\n .slice(0, 8);\n}\n\nfunction RunPredictionDialog({\n open,\n onOpenChange,\n onRun,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onRun: () => void;\n}) {\n const assets = useAssetHandlerStore((s) => s.assets);\n const [assetId, setAssetId] = useState('');\n const [predictionType, setPredictionType] =\n useState<(typeof PREDICTION_TYPES)[number]['value']>('maintenance-risk');\n\n const [requestPrediction, { loading: requesting }] = useMutation(REQUEST_AI_PREDICTION_MUTATION, {\n onCompleted: () => {\n notify.success('Prediction requested', 'The model result will appear shortly.');\n onOpenChange(false);\n onRun();\n },\n onError: (error) => notify.error('Could not run prediction', error.message),\n });\n\n const handleRun = (event: React.FormEvent) => {\n event.preventDefault();\n if (!assetId) {\n notify.warning('Asset required', 'Select an asset to run a prediction against.');\n return;\n }\n void requestPrediction({ variables: { input: { assetId, predictionType } } });\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Run Prediction</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Pick an asset and a model to forecast failure risk and get a recommended action.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form className=\"space-y-4\" onSubmit={handleRun}>\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.assetTag} — {asset.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Prediction Type</Label>\n <Select\n value={predictionType}\n onValueChange={(v) => setPredictionType(v as typeof predictionType)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PREDICTION_TYPES.map((t) => (\n <SelectItem key={t.value} value={t.value}>\n {t.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)} disabled={requesting}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={requesting}>\n {requesting ? 'Running…' : 'Run Prediction'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function AiPredictionsPage() {\n const assets = useAssetHandlerStore((s) => s.assets);\n const navigate = useAssetHandlerNavigate();\n const [dialogOpen, setDialogOpen] = useState(false);\n const [typeFilter, setTypeFilter] = useState<string>('ALL');\n\n const { data, loading, refetch } = useQuery<{ aiPredictions: AiPrediction[] }>(\n AI_PREDICTIONS_QUERY,\n { fetchPolicy: 'cache-and-network' }\n );\n\n const predictions = useMemo(() => data?.aiPredictions ?? [], [data?.aiPredictions]);\n\n const assetLabelById = useMemo(() => {\n const map = new Map<string, string>();\n for (const a of assets) map.set(a.id, `${a.assetTag} — ${a.name}`);\n return map;\n }, [assets]);\n\n const labelFor = (assetId: string): string =>\n assetLabelById.get(assetId) ?? `Asset ${assetId.slice(0, 8)}`;\n\n const sorted = useMemo(() => {\n const list =\n typeFilter === 'ALL'\n ? predictions\n : predictions.filter((p) => p.predictionType === typeFilter);\n return [...list].sort((a, b) => b.score - a.score);\n }, [predictions, typeFilter]);\n\n const riskMix = useMemo<DonutSlice[]>(() => {\n const counts: Record<RiskBand, number> = { high: 0, medium: 0, low: 0 };\n for (const p of predictions) counts[riskBandOf(p.score).band] += 1;\n const order: { band: RiskBand; label: string }[] = [\n { band: 'high', label: 'High' },\n { band: 'medium', label: 'Medium' },\n { band: 'low', label: 'Low' },\n ];\n return order\n .filter((o) => counts[o.band] > 0)\n .map((o, i) => ({ name: o.label, value: counts[o.band], color: seriesColor(i) }));\n }, [predictions]);\n\n // Feature importance is shown for the highest-risk prediction that carries features.\n const featureFocus = useMemo(() => {\n const withFeatures = sorted.find((p) => featureBars(p.features).length > 0);\n return withFeatures ?? null;\n }, [sorted]);\n const focusBars = featureBars(featureFocus?.features);\n\n const pagination = usePagination(sorted, { pageSize: 6, resetKey: typeFilter });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={BrainCircuit}\n title=\"AI Predictions\"\n description=\"Forecast failures before they happen and prioritise which assets to service next.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Run Prediction\n </Button>\n }\n />\n <PagePurpose>\n Pick an asset and a model to get a risk score, confidence level and a concrete\n recommendation — so you can schedule preventive maintenance instead of reacting to\n breakdowns.\n </PagePurpose>\n\n <RunPredictionDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n onRun={() => void refetch()}\n />\n\n {predictions.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Risk distribution\" subtitle=\"Predicted failure risk across assets\">\n <DonutChart\n data={riskMix}\n height={210}\n centerLabel=\"Assets\"\n centerValue={predictions.length}\n ariaLabel=\"Risk distribution across predictions\"\n />\n </ChartCard>\n <ChartCard\n title=\"Feature importance\"\n subtitle={\n featureFocus\n ? `Top drivers · ${labelFor(featureFocus.assetId)}`\n : 'Model feature drivers'\n }\n icon={BarChart3}\n >\n {focusBars.length > 0 ? (\n <BarsChart\n data={focusBars}\n xKey=\"name\"\n series={[{ key: 'importance', label: 'Importance' }]}\n orientation=\"horizontal\"\n height={210}\n categoryWidth={140}\n ariaLabel=\"Feature importance\"\n />\n ) : (\n <div className=\"flex h-[210px] items-center justify-center px-4 text-center text-sm text-text-secondary\">\n Feature importance appears here once a model returns feature drivers.\n </div>\n )}\n </ChartCard>\n </div>\n ) : null}\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <h3 className=\"text-base font-semibold text-text-primary\">Predicted failure risk per asset</h3>\n <div className=\"w-full sm:w-64\">\n <Select value={typeFilter} onValueChange={setTypeFilter}>\n <SelectTrigger>\n <SelectValue placeholder=\"Prediction type\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All prediction types</SelectItem>\n {PREDICTION_TYPES.map((t) => (\n <SelectItem key={t.value} value={t.value}>\n {t.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {loading && predictions.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-56 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\" />\n ))}\n </div>\n ) : null}\n {!loading && sorted.length === 0 ? (\n <IllustratedEmptyState\n illustration={typeFilter === 'ALL' ? 'empty-data' : 'empty-search'}\n title={\n typeFilter === 'ALL'\n ? 'No AI predictions available yet'\n : 'No predictions of this type'\n }\n description={\n typeFilter === 'ALL'\n ? 'Run a prediction to generate a risk score and recommendation.'\n : 'Try a different prediction type or run a new prediction.'\n }\n />\n ) : null}\n\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {pagination.pageItems.map((prediction, index) => {\n const risk = riskBandOf(prediction.score);\n const isTop = index === 0 && pagination.page === 1 && risk.band === 'high';\n const pending = prediction.status && prediction.status.toLowerCase() !== 'completed';\n const confidencePct = Math.round(prediction.confidence * 100);\n return (\n <GlassCard\n key={prediction.id}\n treatment=\"glass\"\n glow={isTop}\n className={cn(\n 'border-border-seam',\n isTop && 'ring-1 ring-status-error-border'\n )}\n >\n <div className=\"flex h-full flex-col gap-3 p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(`/assets/${prediction.assetId}`)}\n className=\"truncate text-left text-sm font-semibold text-text-primary hover:underline\"\n >\n {labelFor(prediction.assetId)}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {humanizePredictionType(prediction.predictionType)}\n </p>\n </div>\n {pending ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-info-bg-subtle px-2 py-0.5 text-xs font-medium text-status-info-text\">\n <Loader2 className=\"h-3 w-3 motion-safe:animate-spin\" />\n Processing\n </span>\n ) : null}\n </div>\n\n <div className=\"flex items-center gap-3\">\n <div className=\"w-[120px] shrink-0\">\n <RadialGauge\n value={prediction.score * 100}\n tone={risk.tone}\n label={risk.label}\n height={110}\n format={(v) => `${Math.round(v)}%`}\n ariaLabel={`${risk.label} risk, ${Math.round(prediction.score * 100)} percent`}\n />\n </div>\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"text-xs text-text-secondary\">Confidence</p>\n <div className=\"flex items-center gap-2\">\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${confidencePct}%` }}\n />\n </div>\n <span className=\"text-xs font-medium tabular-nums text-text-primary\">\n {confidencePct}%\n </span>\n </div>\n </div>\n </div>\n\n <p className=\"flex-1 text-sm text-text-secondary\">{prediction.recommendation}</p>\n\n <div className=\"flex items-center justify-between gap-2 border-t border-border-seam/60 pt-3\">\n <span className=\"truncate text-xs text-text-muted\">\n {prediction.modelVersion} ·{' '}\n {new Date(prediction.generatedAt).toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => navigate('/maintenance/maintenance-scheduler')}\n >\n <Wrench className=\"mr-1.5 h-3.5 w-3.5\" />\n Schedule\n </Button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {sorted.length > 0 ? (\n <Pagination\n page={pagination.page}\n pageCount={pagination.pageCount}\n rangeStart={pagination.rangeStart}\n rangeEnd={pagination.rangeEnd}\n totalItems={pagination.totalItems}\n canPrev={pagination.canPrev}\n canNext={pagination.canNext}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"predictions\"\n />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiPredictionsPage.js","names":[],"sources":["../../../src/assethandler/pages/AiPredictionsPage.tsx"],"sourcesContent":["import type React from 'react';\nimport { BrainCircuit, Plus, Wrench, Loader2, BarChart3 } from 'lucide-react';\nimport {\n Button,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Label,\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 { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n ChartCard,\n DonutChart,\n BarsChart,\n RadialGauge,\n seriesColor,\n type DonutSlice,\n} from '../components';\nimport { useAssetHandlerNavigate } from '../hooks/useAssetHandlerNavigate';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst AI_PREDICTIONS_QUERY = gql`\n query AssetHandlerAiPredictions {\n aiPredictions {\n id\n assetId\n predictionType\n score\n confidence\n recommendation\n features\n status\n generatedAt\n modelVersion\n }\n }\n`;\n\nconst REQUEST_AI_PREDICTION_MUTATION = gql`\n mutation AssetHandlerRequestAiPrediction($input: RequestAIPredictionInput!) {\n requestAIPrediction(input: $input) {\n id\n }\n }\n`;\n\ninterface AiPrediction {\n id: string;\n assetId: string;\n predictionType: string;\n score: number;\n confidence: number;\n recommendation: string;\n features: Record<string, unknown> | null;\n status: string;\n generatedAt: string;\n modelVersion: string;\n}\n\nconst PREDICTION_TYPES = [\n { value: 'maintenance-risk', label: 'Maintenance risk' },\n { value: 'failure-forecast', label: 'Failure forecast' },\n { value: 'utilization-forecast', label: 'Utilization forecast' },\n { value: 'anomaly-detection', label: 'Anomaly detection' },\n { value: 'end-of-life', label: 'End-of-life projection' },\n] as const;\n\nconst humanizePredictionType = (value: string): string =>\n PREDICTION_TYPES.find((t) => t.value === value)?.label ?? value.replace(/-/g, ' ');\n\ntype RiskBand = 'high' | 'medium' | 'low';\n\nfunction riskBandOf(score: number): {\n band: RiskBand;\n label: string;\n tone: 'danger' | 'warning' | 'success';\n} {\n if (score >= 0.66) return { band: 'high', label: 'High', tone: 'danger' };\n if (score >= 0.33) return { band: 'medium', label: 'Medium', tone: 'warning' };\n return { band: 'low', label: 'Low', tone: 'success' };\n}\n\n/** Extract numeric feature importances from the backend `features` JSONObject. */\nfunction featureBars(\n features: Record<string, unknown> | null | undefined\n): { name: string; importance: number }[] {\n if (!features) return [];\n return Object.entries(features)\n .map(([name, value]) => ({\n name,\n importance: typeof value === 'number' ? value : Number(value),\n }))\n .filter((f) => Number.isFinite(f.importance))\n .sort((a, b) => b.importance - a.importance)\n .slice(0, 8);\n}\n\nfunction RunPredictionDialog({\n open,\n onOpenChange,\n onRun,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onRun: () => void;\n}) {\n const assets = useAssetHandlerStore((s) => s.assets);\n const [assetId, setAssetId] = useState('');\n const [predictionType, setPredictionType] =\n useState<(typeof PREDICTION_TYPES)[number]['value']>('maintenance-risk');\n\n const [requestPrediction, { loading: requesting }] = useMutation(REQUEST_AI_PREDICTION_MUTATION, {\n onCompleted: () => {\n notify.success('Prediction requested', 'The model result will appear shortly.');\n onOpenChange(false);\n onRun();\n },\n onError: (error) => notify.error('Could not run prediction', error.message),\n });\n\n const handleRun = (event: React.FormEvent) => {\n event.preventDefault();\n if (!assetId) {\n notify.warning('Asset required', 'Select an asset to run a prediction against.');\n return;\n }\n void requestPrediction({ variables: { input: { assetId, predictionType } } });\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Run Prediction</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Pick an asset and a model to forecast failure risk and get a recommended action.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form className=\"space-y-4\" onSubmit={handleRun}>\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.assetTag} — {asset.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Prediction Type</Label>\n <Select\n value={predictionType}\n onValueChange={(v) => setPredictionType(v as typeof predictionType)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PREDICTION_TYPES.map((t) => (\n <SelectItem key={t.value} value={t.value}>\n {t.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={requesting}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={requesting}>\n {requesting ? 'Running…' : 'Run Prediction'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function AiPredictionsPage() {\n const assets = useAssetHandlerStore((s) => s.assets);\n const navigate = useAssetHandlerNavigate();\n const [dialogOpen, setDialogOpen] = useState(false);\n const [typeFilter, setTypeFilter] = useState<string>('ALL');\n\n const { data, loading, refetch } = useQuery<{ aiPredictions: AiPrediction[] }>(\n AI_PREDICTIONS_QUERY,\n { fetchPolicy: 'cache-and-network' }\n );\n\n const predictions = useMemo(() => data?.aiPredictions ?? [], [data?.aiPredictions]);\n\n const assetLabelById = useMemo(() => {\n const map = new Map<string, string>();\n for (const a of assets) map.set(a.id, `${a.assetTag} — ${a.name}`);\n return map;\n }, [assets]);\n\n const labelFor = (assetId: string): string =>\n assetLabelById.get(assetId) ?? `Asset ${assetId.slice(0, 8)}`;\n\n const sorted = useMemo(() => {\n const list =\n typeFilter === 'ALL'\n ? predictions\n : predictions.filter((p) => p.predictionType === typeFilter);\n return [...list].sort((a, b) => b.score - a.score);\n }, [predictions, typeFilter]);\n\n const riskMix = useMemo<DonutSlice[]>(() => {\n const counts: Record<RiskBand, number> = { high: 0, medium: 0, low: 0 };\n for (const p of predictions) counts[riskBandOf(p.score).band] += 1;\n const order: { band: RiskBand; label: string }[] = [\n { band: 'high', label: 'High' },\n { band: 'medium', label: 'Medium' },\n { band: 'low', label: 'Low' },\n ];\n return order\n .filter((o) => counts[o.band] > 0)\n .map((o, i) => ({ name: o.label, value: counts[o.band], color: seriesColor(i) }));\n }, [predictions]);\n\n // Feature importance is shown for the highest-risk prediction that carries features.\n const featureFocus = useMemo(() => {\n const withFeatures = sorted.find((p) => featureBars(p.features).length > 0);\n return withFeatures ?? null;\n }, [sorted]);\n const focusBars = featureBars(featureFocus?.features);\n\n const pagination = usePagination(sorted, { pageSize: 6, resetKey: typeFilter });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={BrainCircuit}\n title=\"AI Predictions\"\n description=\"Forecast failures before they happen and prioritise which assets to service next.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Run Prediction\n </Button>\n }\n />\n <PagePurpose>\n Pick an asset and a model to get a risk score, confidence level and a concrete\n recommendation — so you can schedule preventive maintenance instead of reacting to\n breakdowns.\n </PagePurpose>\n\n <RunPredictionDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n onRun={() => void refetch()}\n />\n\n {predictions.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Risk distribution\" subtitle=\"Predicted failure risk across assets\">\n <DonutChart\n data={riskMix}\n height={210}\n centerLabel=\"Assets\"\n centerValue={predictions.length}\n ariaLabel=\"Risk distribution across predictions\"\n />\n </ChartCard>\n <ChartCard\n title=\"Feature importance\"\n subtitle={\n featureFocus\n ? `Top drivers · ${labelFor(featureFocus.assetId)}`\n : 'Model feature drivers'\n }\n icon={BarChart3}\n >\n {focusBars.length > 0 ? (\n <BarsChart\n data={focusBars}\n xKey=\"name\"\n series={[{ key: 'importance', label: 'Importance' }]}\n orientation=\"horizontal\"\n height={210}\n categoryWidth={140}\n ariaLabel=\"Feature importance\"\n />\n ) : (\n <div className=\"flex h-[210px] items-center justify-center px-4 text-center text-sm text-text-secondary\">\n Feature importance appears here once a model returns feature drivers.\n </div>\n )}\n </ChartCard>\n </div>\n ) : null}\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n Predicted failure risk per asset\n </h3>\n <div className=\"w-full sm:w-64\">\n <Select value={typeFilter} onValueChange={setTypeFilter}>\n <SelectTrigger>\n <SelectValue placeholder=\"Prediction type\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All prediction types</SelectItem>\n {PREDICTION_TYPES.map((t) => (\n <SelectItem key={t.value} value={t.value}>\n {t.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {loading && predictions.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-56 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\" />\n ))}\n </div>\n ) : null}\n {!loading && sorted.length === 0 ? (\n <IllustratedEmptyState\n illustration={typeFilter === 'ALL' ? 'empty-data' : 'empty-search'}\n title={\n typeFilter === 'ALL' ? 'No AI predictions available yet' : 'No predictions of this type'\n }\n description={\n typeFilter === 'ALL'\n ? 'Run a prediction to generate a risk score and recommendation.'\n : 'Try a different prediction type or run a new prediction.'\n }\n />\n ) : null}\n\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {pagination.pageItems.map((prediction, index) => {\n const risk = riskBandOf(prediction.score);\n const isTop = index === 0 && pagination.page === 1 && risk.band === 'high';\n const pending = prediction.status && prediction.status.toLowerCase() !== 'completed';\n const confidencePct = Math.round(prediction.confidence * 100);\n return (\n <GlassCard\n key={prediction.id}\n treatment=\"glass\"\n glow={isTop}\n className={cn('border-border-seam', isTop && 'ring-1 ring-status-error-border')}\n >\n <div className=\"flex h-full flex-col gap-3 p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(`/assets/${prediction.assetId}`)}\n className=\"truncate text-left text-sm font-semibold text-text-primary hover:underline\"\n >\n {labelFor(prediction.assetId)}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {humanizePredictionType(prediction.predictionType)}\n </p>\n </div>\n {pending ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-info-bg-subtle px-2 py-0.5 text-xs font-medium text-status-info-text\">\n <Loader2 className=\"h-3 w-3 motion-safe:animate-spin\" />\n Processing\n </span>\n ) : null}\n </div>\n\n <div className=\"flex items-center gap-3\">\n <div className=\"w-[120px] shrink-0\">\n <RadialGauge\n value={prediction.score * 100}\n tone={risk.tone}\n label={risk.label}\n height={110}\n format={(v) => `${Math.round(v)}%`}\n ariaLabel={`${risk.label} risk, ${Math.round(prediction.score * 100)} percent`}\n />\n </div>\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"text-xs text-text-secondary\">Confidence</p>\n <div className=\"flex items-center gap-2\">\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${confidencePct}%` }}\n />\n </div>\n <span className=\"text-xs font-medium tabular-nums text-text-primary\">\n {confidencePct}%\n </span>\n </div>\n </div>\n </div>\n\n <p className=\"flex-1 text-sm text-text-secondary\">{prediction.recommendation}</p>\n\n <div className=\"flex items-center justify-between gap-2 border-t border-border-seam/60 pt-3\">\n <span className=\"truncate text-xs text-text-muted\">\n {prediction.modelVersion} ·{' '}\n {new Date(prediction.generatedAt).toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => navigate('/maintenance/maintenance-scheduler')}\n >\n <Wrench className=\"mr-1.5 h-3.5 w-3.5\" />\n Schedule\n </Button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {sorted.length > 0 ? (\n <Pagination\n page={pagination.page}\n pageCount={pagination.pageCount}\n rangeStart={pagination.rangeStart}\n rangeEnd={pagination.rangeEnd}\n totalItems={pagination.totalItems}\n canPrev={pagination.canPrev}\n canNext={pagination.canNext}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"predictions\"\n />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/assethandler/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n Separator,\n GradientText,\n EmphasisPanel,\n PagePurpose,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Box,\n PieChart,\n Activity,\n Wrench,\n AlertTriangle,\n Wallet,\n DollarSign,\n TrendingDown,\n Users,\n Gauge,\n Receipt,\n ClipboardList,\n ShieldCheck,\n} from 'lucide-react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { cn } from '../utils/cn';\nimport {\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n TrendChart,\n RadialGauge,\n ProgressMeter,\n formatCompact,\n formatCurrencyCompact,\n seriesColor,\n type ChartDatum,\n type DonutSlice,\n} from '../components';\n\n// ─── Backend rollups (wspace-assethandler-svc, @rbac-guarded, no args) ────────\ninterface AssetHandlerFinancialSummary {\n totalAssetValue: number;\n totalDepreciation: number;\n totalMaintenanceSpend: number;\n budgetAllocated: number;\n budgetSpent: number;\n invoicesOutstanding: number;\n invoicesOutstandingCount: number;\n transactionExpense: number;\n}\n\ninterface AssetHandlerStatusCount {\n status: string;\n count: number;\n}\n\ninterface AssetHandlerUtilizationReport {\n totalAssets: number;\n assignedAssets: number;\n unassignedAssets: number;\n idleAssets: number;\n overdueAssignments: number;\n byStatus: AssetHandlerStatusCount[];\n}\n\nconst ANALYTICS_ROLLUPS_QUERY = gql`\n query AssetHandlerAnalyticsRollups {\n financialSummary {\n totalAssetValue\n totalDepreciation\n totalMaintenanceSpend\n budgetAllocated\n budgetSpent\n invoicesOutstanding\n invoicesOutstandingCount\n transactionExpense\n }\n utilizationReport {\n totalAssets\n assignedAssets\n unassignedAssets\n idleAssets\n overdueAssignments\n byStatus {\n status\n count\n }\n }\n }\n`;\n\nconst usd = formatCurrencyCompact('USD');\n\nfunction titleCase(value: string): string {\n return value.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\nfunction parseDate(value: string | undefined | null): Date | null {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\nfunction monthKey(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;\n}\nfunction monthLabel(key: string): string {\n const [year, month] = key.split('-').map(Number);\n return new Date(year, month - 1, 1).toLocaleString(undefined, { month: 'short' });\n}\n\ntype PillTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';\nconst PILL_CLASS: Record<PillTone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-status-info-bg-subtle text-status-info-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\nfunction StatusPill({ tone, children }: { tone: PillTone; children: React.ReactNode }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium capitalize whitespace-nowrap',\n PILL_CLASS[tone]\n )}\n >\n {children}\n </span>\n );\n}\nconst txStatusTone = (status: string): PillTone =>\n status === 'completed' || status === 'approved'\n ? 'success'\n : status === 'rejected'\n ? 'error'\n : status === 'pending'\n ? 'warning'\n : 'neutral';\n\nexport function AnalyticsPage() {\n const { assets, transactions, incidents, maintenanceRecords, budgets, invoices, workOrders } =\n useAssetHandlerStore();\n\n const { data: rollupData } = useQuery<{\n financialSummary: AssetHandlerFinancialSummary;\n utilizationReport: AssetHandlerUtilizationReport;\n }>(ANALYTICS_ROLLUPS_QUERY, { fetchPolicy: 'cache-and-network', errorPolicy: 'all' });\n const financialSummary = rollupData?.financialSummary;\n const utilizationReport = rollupData?.utilizationReport;\n\n // ─── Asset aggregations (count-based) ────────────────────────────────────────\n const categoryDonut = useMemo<DonutSlice[]>(() => {\n const counts = assets.reduce<Record<string, number>>((acc, a) => {\n const cat = titleCase(a.category);\n acc[cat] = (acc[cat] || 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([name, value]) => ({ name, value }));\n }, [assets]);\n\n const categoryBars = useMemo<ChartDatum[]>(\n () => categoryDonut.map((s) => ({ category: s.name, count: s.value })),\n [categoryDonut]\n );\n\n const statusDonut = useMemo<DonutSlice[]>(() => {\n const counts = assets.reduce<Record<string, number>>((acc, a) => {\n acc[a.status] = (acc[a.status] || 0) + 1;\n return acc;\n }, {});\n const toneFor: Record<string, PillTone> = {\n active: 'success',\n maintenance: 'warning',\n retired: 'neutral',\n inactive: 'neutral',\n disposed: 'error',\n };\n const toneToken: Record<PillTone, string> = {\n success: 'var(--color-status-success-text)',\n warning: 'var(--color-status-warning-text)',\n error: 'var(--color-status-error-text)',\n info: 'var(--color-status-info-text)',\n neutral: 'var(--color-text-muted)',\n };\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([status, value]) => ({\n name: titleCase(status),\n value,\n color: toneToken[toneFor[status] ?? 'neutral'],\n }));\n }, [assets]);\n\n const conditionBars = useMemo<ChartDatum[]>(() => {\n const order = ['excellent', 'good', 'fair', 'poor', 'damaged'];\n const counts = assets.reduce<Record<string, number>>((acc, a) => {\n acc[a.condition] = (acc[a.condition] || 0) + 1;\n return acc;\n }, {});\n return order\n .filter((c) => counts[c])\n .map((c) => ({ condition: titleCase(c), count: counts[c] }))\n .concat(\n Object.entries(counts)\n .filter(([c]) => !order.includes(c))\n .map(([c, count]) => ({ condition: titleCase(c), count }))\n );\n }, [assets]);\n\n const totalAssets = assets.length;\n const activeAssets = assets.filter((a) => a.status === 'active').length;\n const distinctCategories = categoryDonut.length;\n const attentionAssets = assets.filter(\n (a) => a.condition === 'poor' || a.condition === 'damaged' || a.status === 'maintenance'\n );\n\n // ─── Financial aggregations ──────────────────────────────────────────────────\n const transactionsTotal = transactions.reduce((s, t) => s + Math.abs(t.amount), 0);\n const budgetAllocated = budgets.reduce((s, b) => s + b.allocated, 0);\n const budgetSpent = budgets.reduce((s, b) => s + b.spent, 0);\n const budgetRemaining = budgets.reduce((s, b) => s + b.remaining, 0);\n const maintenanceCost = maintenanceRecords.reduce(\n (s, m) => s + (m.actualCost ?? m.estimatedCost ?? 0),\n 0\n );\n const outstandingInvoices = invoices.filter(\n (i) => i.status !== 'paid' && i.status !== 'cancelled'\n );\n const outstandingInvoiceTotal = outstandingInvoices.reduce((s, i) => s + i.amount, 0);\n\n const budgetBars = useMemo<ChartDatum[]>(\n () =>\n budgets.map((b) => ({\n name: b.name,\n allocated: b.allocated,\n spent: b.spent,\n })),\n [budgets]\n );\n\n const spendTrend = useMemo<ChartDatum[]>(() => {\n const byMonth = new Map<string, number>();\n for (const tx of transactions) {\n const d = parseDate(tx.date);\n if (!d) continue;\n const key = monthKey(d);\n byMonth.set(key, (byMonth.get(key) ?? 0) + Math.abs(tx.amount));\n }\n return [...byMonth.keys()]\n .sort()\n .map((k) => ({ month: monthLabel(k), spend: Math.round(byMonth.get(k) ?? 0) }));\n }, [transactions]);\n\n // ─── Operational aggregations ────────────────────────────────────────────────\n const resolvedIncidents = incidents.filter(\n (i) => i.status === 'resolved' || i.status === 'closed'\n );\n const slaBreached = incidents.filter((i) => i.slaBreached);\n const slaCompliancePct =\n incidents.length > 0\n ? Math.round((1 - slaBreached.length / incidents.length) * 100)\n : 100;\n const completedMaintenance = maintenanceRecords.filter((m) => m.status === 'completed');\n const scheduledMaintenance = maintenanceRecords.filter((m) => m.status === 'scheduled');\n const openWorkOrders = workOrders.filter(\n (w) => w.status !== 'completed' && w.status !== 'cancelled'\n );\n\n const utilStatusBars = useMemo<ChartDatum[]>(\n () =>\n (utilizationReport?.byStatus ?? []).map((s) => ({\n status: titleCase(s.status),\n count: s.count,\n })),\n [utilizationReport]\n );\n\n const workOrderDonut = useMemo<DonutSlice[]>(() => {\n const counts = workOrders.reduce<Record<string, number>>((acc, w) => {\n acc[w.status] = (acc[w.status] || 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([name, value]) => ({ name: titleCase(name), value }));\n }, [workOrders]);\n\n const maintenanceCostTrend = useMemo<ChartDatum[]>(() => {\n const byMonth = new Map<string, number>();\n for (const m of maintenanceRecords) {\n const d = parseDate(m.completedDate ?? m.scheduledDate);\n const cost = m.actualCost ?? m.estimatedCost;\n if (!d || cost === undefined) continue;\n const key = monthKey(d);\n byMonth.set(key, (byMonth.get(key) ?? 0) + cost);\n }\n return [...byMonth.keys()]\n .sort()\n .map((k) => ({ month: monthLabel(k), cost: Math.round(byMonth.get(k) ?? 0) }));\n }, [maintenanceRecords]);\n\n const financialTiles = financialSummary\n ? [\n { label: 'Total Asset Value', value: financialSummary.totalAssetValue, icon: DollarSign },\n {\n label: 'Total Depreciation',\n value: financialSummary.totalDepreciation,\n icon: TrendingDown,\n },\n {\n label: 'Maintenance Spend',\n value: financialSummary.totalMaintenanceSpend,\n icon: Wrench,\n },\n {\n label: 'Transaction Expense',\n value: financialSummary.transactionExpense,\n icon: Receipt,\n },\n { label: 'Budget Allocated', value: financialSummary.budgetAllocated, icon: Wallet },\n { label: 'Budget Spent', value: financialSummary.budgetSpent, icon: Wallet },\n {\n label: 'Invoices Outstanding',\n value: financialSummary.invoicesOutstanding,\n icon: AlertTriangle,\n },\n ]\n : [];\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold sm:text-3xl\">\n <GradientText>Analytics & Business Intelligence</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n Asset, financial and operational insights across your estate\n </p>\n </div>\n\n <PagePurpose>\n Turn raw asset data into decisions. See how your estate is distributed by category, status\n and condition, where budget is being spent, and how operations are trending — so you can\n plan work, justify replacements and spot risk before it shows up as downtime.\n </PagePurpose>\n\n <Tabs defaultValue=\"overview\">\n <TabsList className=\"max-w-full justify-start overflow-x-auto\">\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"financial\">Financial</TabsTrigger>\n <TabsTrigger value=\"operational\">Operational</TabsTrigger>\n <TabsTrigger value=\"insights\">Insights</TabsTrigger>\n </TabsList>\n\n {/* ─── Overview Tab ─────────────────────────────────────────────────── */}\n <TabsContent value=\"overview\" className=\"space-y-6 mt-6\">\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n hero\n label=\"Total Assets\"\n value={totalAssets}\n format={formatCompact}\n icon={Box}\n />\n <StatTile label=\"Categories\" value={distinctCategories} icon={PieChart} />\n <StatTile label=\"Needs Attention\" value={attentionAssets.length} icon={AlertTriangle} />\n <StatTile\n label=\"Open Incidents\"\n value={incidents.length - resolvedIncidents.length}\n icon={Activity}\n />\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Category\" subtitle=\"Computed from real data\" icon={PieChart}>\n <BarsChart\n data={categoryBars}\n xKey=\"category\"\n series={[{ key: 'count', label: 'Assets' }]}\n orientation=\"horizontal\"\n height={Math.max(220, categoryBars.length * 40)}\n categoryWidth={120}\n emptyLabel=\"No assets yet\"\n ariaLabel=\"Assets by category\"\n />\n </ChartCard>\n\n <ChartCard title=\"Assets by Status\" subtitle=\"Computed from real data\" icon={Activity}>\n <DonutChart\n data={statusDonut}\n centerLabel=\"Assets\"\n centerValue={totalAssets}\n height={260}\n emptyLabel=\"No assets yet\"\n ariaLabel=\"Assets by status\"\n />\n </ChartCard>\n </div>\n\n <ChartCard title=\"Asset Condition Mix\" subtitle=\"Computed from real data\" icon={Gauge}>\n <BarsChart\n data={conditionBars}\n xKey=\"condition\"\n series={[{ key: 'count', label: 'Assets' }]}\n height={260}\n emptyLabel=\"No assets yet\"\n ariaLabel=\"Assets by condition\"\n />\n </ChartCard>\n </TabsContent>\n\n {/* ─── Financial Tab ────────────────────────────────────────────────── */}\n <TabsContent value=\"financial\" className=\"space-y-6 mt-6\">\n {financialSummary ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {financialTiles.slice(0, 4).map((tile, i) => (\n <StatTile\n key={tile.label}\n hero={i === 0}\n label={tile.label}\n value={tile.value}\n format={usd}\n icon={tile.icon}\n />\n ))}\n </div>\n ) : (\n <Card>\n <CardContent className=\"py-8\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Financial summary unavailable\"\n description=\"Portfolio valuation and budget rollups will appear once financial data is recorded.\"\n />\n </CardContent>\n </Card>\n )}\n\n {financialSummary ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {financialTiles.slice(4).map((tile) => (\n <StatTile\n key={tile.label}\n label={tile.label}\n value={tile.value}\n format={usd}\n icon={tile.icon}\n delta={\n tile.label === 'Invoices Outstanding'\n ? `${financialSummary.invoicesOutstandingCount} unpaid`\n : undefined\n }\n deltaTone=\"neutral\"\n />\n ))}\n </div>\n ) : null}\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Budget: Allocated vs Spent\"\n subtitle=\"Per budget, from real data\"\n icon={Wallet}\n >\n {budgetBars.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No budgets yet\"\n description=\"Create a budget to track allocation and spend here.\"\n />\n ) : (\n <BarsChart\n data={budgetBars}\n xKey=\"name\"\n series={[\n { key: 'allocated', label: 'Allocated', color: seriesColor(1) },\n { key: 'spent', label: 'Spent' },\n ]}\n height={Math.max(220, budgetBars.length * 56)}\n orientation=\"horizontal\"\n categoryWidth={120}\n valueFormat={usd}\n ariaLabel=\"Budget allocated versus spent\"\n />\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Spend Over Time\"\n subtitle=\"Monthly transaction spend\"\n icon={Receipt}\n >\n {spendTrend.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No transactions yet\"\n description=\"Recorded purchases, leases and disposals will chart here.\"\n />\n ) : (\n <TrendChart\n data={spendTrend}\n xKey=\"month\"\n series={[{ key: 'spend', label: 'Spend' }]}\n valueFormat={usd}\n height={260}\n ariaLabel=\"Monthly transaction spend\"\n />\n )}\n </ChartCard>\n </div>\n\n {/* Budget utilization meters */}\n <ChartCard title=\"Budget Utilization\" subtitle=\"Allocated vs. spent per budget\" icon={Wallet}>\n {budgets.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No budgets yet\"\n description=\"Create a budget to track allocation and spend here.\"\n />\n ) : (\n <div className=\"space-y-4\">\n {budgets.map((budget) => (\n <ProgressMeter\n key={budget.id}\n label={budget.name}\n value={budget.spent}\n max={budget.allocated}\n format={(v) => `$${Math.round(v).toLocaleString()}`}\n />\n ))}\n <Separator />\n <div className=\"flex items-center justify-between text-sm font-medium\">\n <span>Remaining across all budgets</span>\n <span className=\"tabular-nums\">${budgetRemaining.toLocaleString()}</span>\n </div>\n </div>\n )}\n </ChartCard>\n\n {/* Financial quick stats */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n label=\"Transactions\"\n value={transactions.length}\n icon={Receipt}\n delta={`$${transactionsTotal.toLocaleString()} value`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"Budget Allocated\"\n value={budgetAllocated}\n format={usd}\n icon={Wallet}\n delta={`$${budgetSpent.toLocaleString()} spent`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"Maintenance Cost\"\n value={maintenanceCost}\n format={usd}\n icon={Wrench}\n />\n <StatTile\n label=\"Outstanding Invoices\"\n value={outstandingInvoiceTotal}\n format={usd}\n icon={AlertTriangle}\n delta={`${outstandingInvoices.length} of ${invoices.length}`}\n deltaTone=\"neutral\"\n />\n </div>\n\n {/* Recent transactions */}\n <ChartCard title=\"Recent Transactions\" icon={Receipt}>\n {transactions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No transactions\"\n description=\"Asset purchases, leases and disposals will appear here.\"\n />\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-text-secondary\">\n <th className=\"py-2 text-left font-medium\">Type</th>\n <th className=\"py-2 text-left font-medium\">Description</th>\n <th className=\"py-2 text-right font-medium\">Amount</th>\n <th className=\"py-2 text-left font-medium\">Status</th>\n </tr>\n </thead>\n <tbody>\n {transactions.slice(0, 6).map((txn) => (\n <tr\n key={txn.id}\n className=\"border-b border-border-seam hover:bg-bg-sunken/50\"\n >\n <td className=\"py-2.5 capitalize\">{txn.type}</td>\n <td className=\"max-w-xs truncate py-2.5\">{txn.description}</td>\n <td\n className={cn(\n 'py-2.5 text-right font-medium tabular-nums',\n txn.amount < 0 && 'text-status-error-text'\n )}\n >\n ${Math.abs(txn.amount).toLocaleString()}\n </td>\n <td className=\"py-2.5\">\n <StatusPill tone={txStatusTone(txn.status)}>{txn.status}</StatusPill>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </ChartCard>\n </TabsContent>\n\n {/* ─── Operational Tab ──────────────────────────────────────────────── */}\n <TabsContent value=\"operational\" className=\"space-y-6 mt-6\">\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-5\">\n <StatTile\n label=\"Total Assets\"\n value={utilizationReport?.totalAssets ?? totalAssets}\n icon={Box}\n />\n <StatTile\n label=\"Assigned\"\n value={utilizationReport?.assignedAssets ?? '—'}\n icon={Users}\n />\n <StatTile label=\"Unassigned\" value={utilizationReport?.unassignedAssets ?? '—'} icon={Box} />\n <StatTile label=\"Idle\" value={utilizationReport?.idleAssets ?? '—'} icon={Activity} />\n <StatTile\n label=\"Overdue\"\n value={utilizationReport?.overdueAssignments ?? '—'}\n icon={AlertTriangle}\n delta={\n utilizationReport && utilizationReport.overdueAssignments > 0 ? 'action needed' : undefined\n }\n deltaTone=\"negative\"\n deltaDirection=\"up\"\n />\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Status\" subtitle=\"Assignment coverage\" icon={Gauge}>\n <BarsChart\n data={utilStatusBars}\n xKey=\"status\"\n series={[{ key: 'count', label: 'Assets' }]}\n orientation=\"horizontal\"\n height={Math.max(200, utilStatusBars.length * 40)}\n categoryWidth={120}\n emptyLabel=\"No utilization data\"\n ariaLabel=\"Assets by status\"\n />\n </ChartCard>\n\n <ChartCard title=\"Work Orders by Status\" subtitle=\"From real data\" icon={ClipboardList}>\n <DonutChart\n data={workOrderDonut}\n centerLabel=\"Work orders\"\n centerValue={workOrders.length}\n height={260}\n emptyLabel=\"No work orders\"\n ariaLabel=\"Work orders by status\"\n />\n </ChartCard>\n </div>\n\n <ChartCard\n title=\"Maintenance Cost Over Time\"\n subtitle=\"Monthly recorded maintenance cost\"\n icon={Wrench}\n >\n {maintenanceCostTrend.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No maintenance cost yet\"\n description=\"Once maintenance records carry cost, monthly spend will chart here.\"\n />\n ) : (\n <TrendChart\n data={maintenanceCostTrend}\n xKey=\"month\"\n series={[{ key: 'cost', label: 'Cost' }]}\n valueFormat={usd}\n height={260}\n ariaLabel=\"Monthly maintenance cost\"\n />\n )}\n </ChartCard>\n\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n label=\"Incidents\"\n value={incidents.length}\n icon={Activity}\n delta={`${resolvedIncidents.length} resolved`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"SLA Breaches\"\n value={slaBreached.length}\n icon={AlertTriangle}\n delta={slaBreached.length > 0 ? 'over SLA' : 'all within SLA'}\n deltaTone={slaBreached.length > 0 ? 'negative' : 'positive'}\n deltaDirection={slaBreached.length > 0 ? 'up' : 'neutral'}\n />\n <StatTile\n label=\"Maintenance Done\"\n value={completedMaintenance.length}\n icon={Wrench}\n delta={`of ${maintenanceRecords.length}`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"Open Work Orders\"\n value={openWorkOrders.length}\n icon={ClipboardList}\n delta={`of ${workOrders.length}`}\n deltaTone=\"neutral\"\n />\n </div>\n </TabsContent>\n\n {/* ─── Insights Tab ─────────────────────────────────────────────────── */}\n <TabsContent value=\"insights\" className=\"space-y-6 mt-6\">\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <ChartCard title=\"SLA Compliance\" subtitle=\"Incidents resolved within SLA\" icon={ShieldCheck}>\n <RadialGauge\n value={slaCompliancePct}\n label=\"within SLA\"\n sublabel={`${slaBreached.length} of ${incidents.length} breached`}\n format={(v) => `${Math.round(v)}%`}\n height={220}\n ariaLabel=\"SLA compliance percentage\"\n />\n </ChartCard>\n\n <div className=\"lg:col-span-2 grid gap-4 sm:grid-cols-3\">\n <Card className=\"border-status-info-border bg-status-info-bg-subtle\">\n <CardHeader>\n <CardTitle className=\"text-status-info-text text-base\">Diversification</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-status-info-text\">\n Your estate spans {distinctCategories} categories across {totalAssets} assets.{' '}\n {activeAssets} are currently active.\n </p>\n </CardContent>\n </Card>\n <Card className=\"border-status-success-border bg-status-success-bg-subtle\">\n <CardHeader>\n <CardTitle className=\"text-status-success-text text-base\">Budget</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-status-success-text\">\n ${budgetSpent.toLocaleString()} spent of ${budgetAllocated.toLocaleString()}{' '}\n allocated, leaving ${budgetRemaining.toLocaleString()} across {budgets.length}{' '}\n budgets.\n </p>\n </CardContent>\n </Card>\n <Card className=\"border-status-warning-border bg-status-warning-bg-subtle\">\n <CardHeader>\n <CardTitle className=\"text-status-warning-text text-base\">Maintenance</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-status-warning-text\">\n {scheduledMaintenance.length} tasks scheduled. Total maintenance spend is $\n {maintenanceCost.toLocaleString()}. Stay on top of preventive maintenance.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n\n {/* Assets needing attention */}\n <EmphasisPanel>\n <CardHeader className=\"px-0 pt-0\">\n <CardTitle className=\"flex items-center gap-2\">\n <Wrench className=\"h-4 w-4\" />\n Assets Needing Attention\n </CardTitle>\n </CardHeader>\n <CardContent className=\"px-0 pb-0\">\n {attentionAssets.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-success\"\n title=\"Everything looks healthy\"\n description=\"No assets are flagged as poor, damaged or in maintenance right now.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {attentionAssets.slice(0, 6).map((asset, i) => (\n <div\n key={asset.id}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-gloss-card p-3\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-medium text-primary\">\n {i + 1}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{asset.name}</p>\n <p className=\"truncate text-xs text-text-secondary capitalize\">\n {asset.category.replace('_', ' ')}\n {asset.location ? ` · ${asset.location}` : ''}\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusPill tone={asset.condition === 'damaged' ? 'error' : 'warning'}>\n {asset.condition}\n </StatusPill>\n <StatusPill tone={asset.status === 'active' ? 'success' : 'warning'}>\n {asset.status.replace('_', ' ')}\n </StatusPill>\n </div>\n </div>\n ))}\n </div>\n )}\n </CardContent>\n </EmphasisPanel>\n </TabsContent>\n </Tabs>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/assethandler/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n Separator,\n GradientText,\n EmphasisPanel,\n PagePurpose,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Box,\n PieChart,\n Activity,\n Wrench,\n AlertTriangle,\n Wallet,\n DollarSign,\n TrendingDown,\n Users,\n Gauge,\n Receipt,\n ClipboardList,\n ShieldCheck,\n} from 'lucide-react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { cn } from '../utils/cn';\nimport {\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n TrendChart,\n RadialGauge,\n ProgressMeter,\n formatCompact,\n formatCurrencyCompact,\n seriesColor,\n type ChartDatum,\n type DonutSlice,\n} from '../components';\n\n// ─── Backend rollups (wspace-assethandler-svc, @rbac-guarded, no args) ────────\ninterface AssetHandlerFinancialSummary {\n totalAssetValue: number;\n totalDepreciation: number;\n totalMaintenanceSpend: number;\n budgetAllocated: number;\n budgetSpent: number;\n invoicesOutstanding: number;\n invoicesOutstandingCount: number;\n transactionExpense: number;\n}\n\ninterface AssetHandlerStatusCount {\n status: string;\n count: number;\n}\n\ninterface AssetHandlerUtilizationReport {\n totalAssets: number;\n assignedAssets: number;\n unassignedAssets: number;\n idleAssets: number;\n overdueAssignments: number;\n byStatus: AssetHandlerStatusCount[];\n}\n\nconst ANALYTICS_ROLLUPS_QUERY = gql`\n query AssetHandlerAnalyticsRollups {\n financialSummary {\n totalAssetValue\n totalDepreciation\n totalMaintenanceSpend\n budgetAllocated\n budgetSpent\n invoicesOutstanding\n invoicesOutstandingCount\n transactionExpense\n }\n utilizationReport {\n totalAssets\n assignedAssets\n unassignedAssets\n idleAssets\n overdueAssignments\n byStatus {\n status\n count\n }\n }\n }\n`;\n\nconst usd = formatCurrencyCompact('USD');\n\nfunction titleCase(value: string): string {\n return value.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\nfunction parseDate(value: string | undefined | null): Date | null {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\nfunction monthKey(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;\n}\nfunction monthLabel(key: string): string {\n const [year, month] = key.split('-').map(Number);\n return new Date(year, month - 1, 1).toLocaleString(undefined, { month: 'short' });\n}\n\ntype PillTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';\nconst PILL_CLASS: Record<PillTone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-status-info-bg-subtle text-status-info-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\nfunction StatusPill({ tone, children }: { tone: PillTone; children: React.ReactNode }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium capitalize whitespace-nowrap',\n PILL_CLASS[tone]\n )}\n >\n {children}\n </span>\n );\n}\nconst txStatusTone = (status: string): PillTone =>\n status === 'completed' || status === 'approved'\n ? 'success'\n : status === 'rejected'\n ? 'error'\n : status === 'pending'\n ? 'warning'\n : 'neutral';\n\nexport function AnalyticsPage() {\n const { assets, transactions, incidents, maintenanceRecords, budgets, invoices, workOrders } =\n useAssetHandlerStore();\n\n const { data: rollupData } = useQuery<{\n financialSummary: AssetHandlerFinancialSummary;\n utilizationReport: AssetHandlerUtilizationReport;\n }>(ANALYTICS_ROLLUPS_QUERY, { fetchPolicy: 'cache-and-network', errorPolicy: 'all' });\n const financialSummary = rollupData?.financialSummary;\n const utilizationReport = rollupData?.utilizationReport;\n\n // ─── Asset aggregations (count-based) ────────────────────────────────────────\n const categoryDonut = useMemo<DonutSlice[]>(() => {\n const counts = assets.reduce<Record<string, number>>((acc, a) => {\n const cat = titleCase(a.category);\n acc[cat] = (acc[cat] || 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([name, value]) => ({ name, value }));\n }, [assets]);\n\n const categoryBars = useMemo<ChartDatum[]>(\n () => categoryDonut.map((s) => ({ category: s.name, count: s.value })),\n [categoryDonut]\n );\n\n const statusDonut = useMemo<DonutSlice[]>(() => {\n const counts = assets.reduce<Record<string, number>>((acc, a) => {\n acc[a.status] = (acc[a.status] || 0) + 1;\n return acc;\n }, {});\n const toneFor: Record<string, PillTone> = {\n active: 'success',\n maintenance: 'warning',\n retired: 'neutral',\n inactive: 'neutral',\n disposed: 'error',\n };\n const toneToken: Record<PillTone, string> = {\n success: 'var(--color-status-success-text)',\n warning: 'var(--color-status-warning-text)',\n error: 'var(--color-status-error-text)',\n info: 'var(--color-status-info-text)',\n neutral: 'var(--color-text-muted)',\n };\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([status, value]) => ({\n name: titleCase(status),\n value,\n color: toneToken[toneFor[status] ?? 'neutral'],\n }));\n }, [assets]);\n\n const conditionBars = useMemo<ChartDatum[]>(() => {\n const order = ['excellent', 'good', 'fair', 'poor', 'damaged'];\n const counts = assets.reduce<Record<string, number>>((acc, a) => {\n acc[a.condition] = (acc[a.condition] || 0) + 1;\n return acc;\n }, {});\n return order\n .filter((c) => counts[c])\n .map((c) => ({ condition: titleCase(c), count: counts[c] }))\n .concat(\n Object.entries(counts)\n .filter(([c]) => !order.includes(c))\n .map(([c, count]) => ({ condition: titleCase(c), count }))\n );\n }, [assets]);\n\n const totalAssets = assets.length;\n const activeAssets = assets.filter((a) => a.status === 'active').length;\n const distinctCategories = categoryDonut.length;\n const attentionAssets = assets.filter(\n (a) => a.condition === 'poor' || a.condition === 'damaged' || a.status === 'maintenance'\n );\n\n // ─── Financial aggregations ──────────────────────────────────────────────────\n const transactionsTotal = transactions.reduce((s, t) => s + Math.abs(t.amount), 0);\n const budgetAllocated = budgets.reduce((s, b) => s + b.allocated, 0);\n const budgetSpent = budgets.reduce((s, b) => s + b.spent, 0);\n const budgetRemaining = budgets.reduce((s, b) => s + b.remaining, 0);\n const maintenanceCost = maintenanceRecords.reduce(\n (s, m) => s + (m.actualCost ?? m.estimatedCost ?? 0),\n 0\n );\n const outstandingInvoices = invoices.filter(\n (i) => i.status !== 'paid' && i.status !== 'cancelled'\n );\n const outstandingInvoiceTotal = outstandingInvoices.reduce((s, i) => s + i.amount, 0);\n\n const budgetBars = useMemo<ChartDatum[]>(\n () =>\n budgets.map((b) => ({\n name: b.name,\n allocated: b.allocated,\n spent: b.spent,\n })),\n [budgets]\n );\n\n const spendTrend = useMemo<ChartDatum[]>(() => {\n const byMonth = new Map<string, number>();\n for (const tx of transactions) {\n const d = parseDate(tx.date);\n if (!d) continue;\n const key = monthKey(d);\n byMonth.set(key, (byMonth.get(key) ?? 0) + Math.abs(tx.amount));\n }\n return [...byMonth.keys()]\n .sort()\n .map((k) => ({ month: monthLabel(k), spend: Math.round(byMonth.get(k) ?? 0) }));\n }, [transactions]);\n\n // ─── Operational aggregations ────────────────────────────────────────────────\n const resolvedIncidents = incidents.filter(\n (i) => i.status === 'resolved' || i.status === 'closed'\n );\n const slaBreached = incidents.filter((i) => i.slaBreached);\n const slaCompliancePct =\n incidents.length > 0 ? Math.round((1 - slaBreached.length / incidents.length) * 100) : 100;\n const completedMaintenance = maintenanceRecords.filter((m) => m.status === 'completed');\n const scheduledMaintenance = maintenanceRecords.filter((m) => m.status === 'scheduled');\n const openWorkOrders = workOrders.filter(\n (w) => w.status !== 'completed' && w.status !== 'cancelled'\n );\n\n const utilStatusBars = useMemo<ChartDatum[]>(\n () =>\n (utilizationReport?.byStatus ?? []).map((s) => ({\n status: titleCase(s.status),\n count: s.count,\n })),\n [utilizationReport]\n );\n\n const workOrderDonut = useMemo<DonutSlice[]>(() => {\n const counts = workOrders.reduce<Record<string, number>>((acc, w) => {\n acc[w.status] = (acc[w.status] || 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([name, value]) => ({ name: titleCase(name), value }));\n }, [workOrders]);\n\n const maintenanceCostTrend = useMemo<ChartDatum[]>(() => {\n const byMonth = new Map<string, number>();\n for (const m of maintenanceRecords) {\n const d = parseDate(m.completedDate ?? m.scheduledDate);\n const cost = m.actualCost ?? m.estimatedCost;\n if (!d || cost === undefined) continue;\n const key = monthKey(d);\n byMonth.set(key, (byMonth.get(key) ?? 0) + cost);\n }\n return [...byMonth.keys()]\n .sort()\n .map((k) => ({ month: monthLabel(k), cost: Math.round(byMonth.get(k) ?? 0) }));\n }, [maintenanceRecords]);\n\n const financialTiles = financialSummary\n ? [\n { label: 'Total Asset Value', value: financialSummary.totalAssetValue, icon: DollarSign },\n {\n label: 'Total Depreciation',\n value: financialSummary.totalDepreciation,\n icon: TrendingDown,\n },\n {\n label: 'Maintenance Spend',\n value: financialSummary.totalMaintenanceSpend,\n icon: Wrench,\n },\n {\n label: 'Transaction Expense',\n value: financialSummary.transactionExpense,\n icon: Receipt,\n },\n { label: 'Budget Allocated', value: financialSummary.budgetAllocated, icon: Wallet },\n { label: 'Budget Spent', value: financialSummary.budgetSpent, icon: Wallet },\n {\n label: 'Invoices Outstanding',\n value: financialSummary.invoicesOutstanding,\n icon: AlertTriangle,\n },\n ]\n : [];\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold sm:text-3xl\">\n <GradientText>Analytics & Business Intelligence</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n Asset, financial and operational insights across your estate\n </p>\n </div>\n\n <PagePurpose>\n Turn raw asset data into decisions. See how your estate is distributed by category, status\n and condition, where budget is being spent, and how operations are trending — so you can\n plan work, justify replacements and spot risk before it shows up as downtime.\n </PagePurpose>\n\n <Tabs defaultValue=\"overview\">\n <TabsList className=\"max-w-full justify-start overflow-x-auto\">\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"financial\">Financial</TabsTrigger>\n <TabsTrigger value=\"operational\">Operational</TabsTrigger>\n <TabsTrigger value=\"insights\">Insights</TabsTrigger>\n </TabsList>\n\n {/* ─── Overview Tab ─────────────────────────────────────────────────── */}\n <TabsContent value=\"overview\" className=\"space-y-6 mt-6\">\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n hero\n label=\"Total Assets\"\n value={totalAssets}\n format={formatCompact}\n icon={Box}\n />\n <StatTile label=\"Categories\" value={distinctCategories} icon={PieChart} />\n <StatTile label=\"Needs Attention\" value={attentionAssets.length} icon={AlertTriangle} />\n <StatTile\n label=\"Open Incidents\"\n value={incidents.length - resolvedIncidents.length}\n icon={Activity}\n />\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Assets by Category\"\n subtitle=\"Computed from real data\"\n icon={PieChart}\n >\n <BarsChart\n data={categoryBars}\n xKey=\"category\"\n series={[{ key: 'count', label: 'Assets' }]}\n orientation=\"horizontal\"\n height={Math.max(220, categoryBars.length * 40)}\n categoryWidth={120}\n emptyLabel=\"No assets yet\"\n ariaLabel=\"Assets by category\"\n />\n </ChartCard>\n\n <ChartCard title=\"Assets by Status\" subtitle=\"Computed from real data\" icon={Activity}>\n <DonutChart\n data={statusDonut}\n centerLabel=\"Assets\"\n centerValue={totalAssets}\n height={260}\n emptyLabel=\"No assets yet\"\n ariaLabel=\"Assets by status\"\n />\n </ChartCard>\n </div>\n\n <ChartCard title=\"Asset Condition Mix\" subtitle=\"Computed from real data\" icon={Gauge}>\n <BarsChart\n data={conditionBars}\n xKey=\"condition\"\n series={[{ key: 'count', label: 'Assets' }]}\n height={260}\n emptyLabel=\"No assets yet\"\n ariaLabel=\"Assets by condition\"\n />\n </ChartCard>\n </TabsContent>\n\n {/* ─── Financial Tab ────────────────────────────────────────────────── */}\n <TabsContent value=\"financial\" className=\"space-y-6 mt-6\">\n {financialSummary ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {financialTiles.slice(0, 4).map((tile, i) => (\n <StatTile\n key={tile.label}\n hero={i === 0}\n label={tile.label}\n value={tile.value}\n format={usd}\n icon={tile.icon}\n />\n ))}\n </div>\n ) : (\n <Card>\n <CardContent className=\"py-8\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Financial summary unavailable\"\n description=\"Portfolio valuation and budget rollups will appear once financial data is recorded.\"\n />\n </CardContent>\n </Card>\n )}\n\n {financialSummary ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {financialTiles.slice(4).map((tile) => (\n <StatTile\n key={tile.label}\n label={tile.label}\n value={tile.value}\n format={usd}\n icon={tile.icon}\n delta={\n tile.label === 'Invoices Outstanding'\n ? `${financialSummary.invoicesOutstandingCount} unpaid`\n : undefined\n }\n deltaTone=\"neutral\"\n />\n ))}\n </div>\n ) : null}\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Budget: Allocated vs Spent\"\n subtitle=\"Per budget, from real data\"\n icon={Wallet}\n >\n {budgetBars.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No budgets yet\"\n description=\"Create a budget to track allocation and spend here.\"\n />\n ) : (\n <BarsChart\n data={budgetBars}\n xKey=\"name\"\n series={[\n { key: 'allocated', label: 'Allocated', color: seriesColor(1) },\n { key: 'spent', label: 'Spent' },\n ]}\n height={Math.max(220, budgetBars.length * 56)}\n orientation=\"horizontal\"\n categoryWidth={120}\n valueFormat={usd}\n ariaLabel=\"Budget allocated versus spent\"\n />\n )}\n </ChartCard>\n\n <ChartCard title=\"Spend Over Time\" subtitle=\"Monthly transaction spend\" icon={Receipt}>\n {spendTrend.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No transactions yet\"\n description=\"Recorded purchases, leases and disposals will chart here.\"\n />\n ) : (\n <TrendChart\n data={spendTrend}\n xKey=\"month\"\n series={[{ key: 'spend', label: 'Spend' }]}\n valueFormat={usd}\n height={260}\n ariaLabel=\"Monthly transaction spend\"\n />\n )}\n </ChartCard>\n </div>\n\n {/* Budget utilization meters */}\n <ChartCard\n title=\"Budget Utilization\"\n subtitle=\"Allocated vs. spent per budget\"\n icon={Wallet}\n >\n {budgets.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No budgets yet\"\n description=\"Create a budget to track allocation and spend here.\"\n />\n ) : (\n <div className=\"space-y-4\">\n {budgets.map((budget) => (\n <ProgressMeter\n key={budget.id}\n label={budget.name}\n value={budget.spent}\n max={budget.allocated}\n format={(v) => `$${Math.round(v).toLocaleString()}`}\n />\n ))}\n <Separator />\n <div className=\"flex items-center justify-between text-sm font-medium\">\n <span>Remaining across all budgets</span>\n <span className=\"tabular-nums\">${budgetRemaining.toLocaleString()}</span>\n </div>\n </div>\n )}\n </ChartCard>\n\n {/* Financial quick stats */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n label=\"Transactions\"\n value={transactions.length}\n icon={Receipt}\n delta={`$${transactionsTotal.toLocaleString()} value`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"Budget Allocated\"\n value={budgetAllocated}\n format={usd}\n icon={Wallet}\n delta={`$${budgetSpent.toLocaleString()} spent`}\n deltaTone=\"neutral\"\n />\n <StatTile label=\"Maintenance Cost\" value={maintenanceCost} format={usd} icon={Wrench} />\n <StatTile\n label=\"Outstanding Invoices\"\n value={outstandingInvoiceTotal}\n format={usd}\n icon={AlertTriangle}\n delta={`${outstandingInvoices.length} of ${invoices.length}`}\n deltaTone=\"neutral\"\n />\n </div>\n\n {/* Recent transactions */}\n <ChartCard title=\"Recent Transactions\" icon={Receipt}>\n {transactions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No transactions\"\n description=\"Asset purchases, leases and disposals will appear here.\"\n />\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-text-secondary\">\n <th className=\"py-2 text-left font-medium\">Type</th>\n <th className=\"py-2 text-left font-medium\">Description</th>\n <th className=\"py-2 text-right font-medium\">Amount</th>\n <th className=\"py-2 text-left font-medium\">Status</th>\n </tr>\n </thead>\n <tbody>\n {transactions.slice(0, 6).map((txn) => (\n <tr\n key={txn.id}\n className=\"border-b border-border-seam hover:bg-bg-sunken/50\"\n >\n <td className=\"py-2.5 capitalize\">{txn.type}</td>\n <td className=\"max-w-xs truncate py-2.5\">{txn.description}</td>\n <td\n className={cn(\n 'py-2.5 text-right font-medium tabular-nums',\n txn.amount < 0 && 'text-status-error-text'\n )}\n >\n ${Math.abs(txn.amount).toLocaleString()}\n </td>\n <td className=\"py-2.5\">\n <StatusPill tone={txStatusTone(txn.status)}>{txn.status}</StatusPill>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </ChartCard>\n </TabsContent>\n\n {/* ─── Operational Tab ──────────────────────────────────────────────── */}\n <TabsContent value=\"operational\" className=\"space-y-6 mt-6\">\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-5\">\n <StatTile\n label=\"Total Assets\"\n value={utilizationReport?.totalAssets ?? totalAssets}\n icon={Box}\n />\n <StatTile\n label=\"Assigned\"\n value={utilizationReport?.assignedAssets ?? '—'}\n icon={Users}\n />\n <StatTile\n label=\"Unassigned\"\n value={utilizationReport?.unassignedAssets ?? '—'}\n icon={Box}\n />\n <StatTile label=\"Idle\" value={utilizationReport?.idleAssets ?? '—'} icon={Activity} />\n <StatTile\n label=\"Overdue\"\n value={utilizationReport?.overdueAssignments ?? '—'}\n icon={AlertTriangle}\n delta={\n utilizationReport && utilizationReport.overdueAssignments > 0\n ? 'action needed'\n : undefined\n }\n deltaTone=\"negative\"\n deltaDirection=\"up\"\n />\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Status\" subtitle=\"Assignment coverage\" icon={Gauge}>\n <BarsChart\n data={utilStatusBars}\n xKey=\"status\"\n series={[{ key: 'count', label: 'Assets' }]}\n orientation=\"horizontal\"\n height={Math.max(200, utilStatusBars.length * 40)}\n categoryWidth={120}\n emptyLabel=\"No utilization data\"\n ariaLabel=\"Assets by status\"\n />\n </ChartCard>\n\n <ChartCard title=\"Work Orders by Status\" subtitle=\"From real data\" icon={ClipboardList}>\n <DonutChart\n data={workOrderDonut}\n centerLabel=\"Work orders\"\n centerValue={workOrders.length}\n height={260}\n emptyLabel=\"No work orders\"\n ariaLabel=\"Work orders by status\"\n />\n </ChartCard>\n </div>\n\n <ChartCard\n title=\"Maintenance Cost Over Time\"\n subtitle=\"Monthly recorded maintenance cost\"\n icon={Wrench}\n >\n {maintenanceCostTrend.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No maintenance cost yet\"\n description=\"Once maintenance records carry cost, monthly spend will chart here.\"\n />\n ) : (\n <TrendChart\n data={maintenanceCostTrend}\n xKey=\"month\"\n series={[{ key: 'cost', label: 'Cost' }]}\n valueFormat={usd}\n height={260}\n ariaLabel=\"Monthly maintenance cost\"\n />\n )}\n </ChartCard>\n\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n label=\"Incidents\"\n value={incidents.length}\n icon={Activity}\n delta={`${resolvedIncidents.length} resolved`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"SLA Breaches\"\n value={slaBreached.length}\n icon={AlertTriangle}\n delta={slaBreached.length > 0 ? 'over SLA' : 'all within SLA'}\n deltaTone={slaBreached.length > 0 ? 'negative' : 'positive'}\n deltaDirection={slaBreached.length > 0 ? 'up' : 'neutral'}\n />\n <StatTile\n label=\"Maintenance Done\"\n value={completedMaintenance.length}\n icon={Wrench}\n delta={`of ${maintenanceRecords.length}`}\n deltaTone=\"neutral\"\n />\n <StatTile\n label=\"Open Work Orders\"\n value={openWorkOrders.length}\n icon={ClipboardList}\n delta={`of ${workOrders.length}`}\n deltaTone=\"neutral\"\n />\n </div>\n </TabsContent>\n\n {/* ─── Insights Tab ─────────────────────────────────────────────────── */}\n <TabsContent value=\"insights\" className=\"space-y-6 mt-6\">\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <ChartCard\n title=\"SLA Compliance\"\n subtitle=\"Incidents resolved within SLA\"\n icon={ShieldCheck}\n >\n <RadialGauge\n value={slaCompliancePct}\n label=\"within SLA\"\n sublabel={`${slaBreached.length} of ${incidents.length} breached`}\n format={(v) => `${Math.round(v)}%`}\n height={220}\n ariaLabel=\"SLA compliance percentage\"\n />\n </ChartCard>\n\n <div className=\"lg:col-span-2 grid gap-4 sm:grid-cols-3\">\n <Card className=\"border-status-info-border bg-status-info-bg-subtle\">\n <CardHeader>\n <CardTitle className=\"text-status-info-text text-base\">Diversification</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-status-info-text\">\n Your estate spans {distinctCategories} categories across {totalAssets} assets.{' '}\n {activeAssets} are currently active.\n </p>\n </CardContent>\n </Card>\n <Card className=\"border-status-success-border bg-status-success-bg-subtle\">\n <CardHeader>\n <CardTitle className=\"text-status-success-text text-base\">Budget</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-status-success-text\">\n ${budgetSpent.toLocaleString()} spent of ${budgetAllocated.toLocaleString()}{' '}\n allocated, leaving ${budgetRemaining.toLocaleString()} across {budgets.length}{' '}\n budgets.\n </p>\n </CardContent>\n </Card>\n <Card className=\"border-status-warning-border bg-status-warning-bg-subtle\">\n <CardHeader>\n <CardTitle className=\"text-status-warning-text text-base\">Maintenance</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-status-warning-text\">\n {scheduledMaintenance.length} tasks scheduled. Total maintenance spend is $\n {maintenanceCost.toLocaleString()}. Stay on top of preventive maintenance.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n\n {/* Assets needing attention */}\n <EmphasisPanel>\n <CardHeader className=\"px-0 pt-0\">\n <CardTitle className=\"flex items-center gap-2\">\n <Wrench className=\"h-4 w-4\" />\n Assets Needing Attention\n </CardTitle>\n </CardHeader>\n <CardContent className=\"px-0 pb-0\">\n {attentionAssets.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-success\"\n title=\"Everything looks healthy\"\n description=\"No assets are flagged as poor, damaged or in maintenance right now.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {attentionAssets.slice(0, 6).map((asset, i) => (\n <div\n key={asset.id}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-gloss-card p-3\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-medium text-primary\">\n {i + 1}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{asset.name}</p>\n <p className=\"truncate text-xs text-text-secondary capitalize\">\n {asset.category.replace('_', ' ')}\n {asset.location ? ` · ${asset.location}` : ''}\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusPill tone={asset.condition === 'damaged' ? 'error' : 'warning'}>\n {asset.condition}\n </StatusPill>\n <StatusPill tone={asset.status === 'active' ? 'success' : 'warning'}>\n {asset.status.replace('_', ' ')}\n </StatusPill>\n </div>\n </div>\n ))}\n </div>\n )}\n </CardContent>\n </EmphasisPanel>\n </TabsContent>\n </Tabs>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CmdbBrowserPage.js","names":[],"sources":["../../../src/assethandler/pages/CmdbBrowserPage.tsx"],"sourcesContent":["import {\n Network,\n Plus,\n Search,\n Server,\n Database,\n AppWindow,\n HardDrive,\n Cog,\n ChevronDown,\n ChevronRight,\n Pencil,\n X,\n Trash2,\n} from 'lucide-react';\nimport type { ComponentType, ReactNode } from 'react';\nimport { useMemo, useState, useSyncExternalStore } from 'react';\nimport {\n Button,\n GradientText,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Input,\n Label,\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 { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { StatTile, ChartCard, DonutChart, BarsChart, seriesColor, type DonutSlice } from '../components';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst CONFIGURATION_ITEMS_QUERY = gql`\n query AssetHandlerConfigurationItems {\n configurationItems {\n id\n ciNumber\n name\n ciType\n status\n environment\n owner\n attributes\n updatedAt\n }\n }\n`;\n\nconst UPSERT_CONFIGURATION_ITEM_MUTATION = gql`\n mutation AssetHandlerUpsertConfigurationItem($input: UpsertConfigurationItemInput!) {\n upsertConfigurationItem(input: $input) {\n id\n ciNumber\n }\n }\n`;\n\ninterface ConfigurationItem {\n id: string;\n ciNumber: string;\n name: string;\n ciType: string;\n status: string;\n environment: string | null;\n owner: string | null;\n attributes: Record<string, unknown> | null;\n updatedAt: string;\n}\n\ninterface UpsertConfigurationItemResult {\n upsertConfigurationItem: { id: string; ciNumber: string };\n}\n\nconst CI_TYPES = ['SERVER', 'SERVICE', 'DATABASE', 'NETWORK', 'APPLICATION', 'STORAGE'] as const;\nconst CI_STATUSES = ['ACTIVE', 'INACTIVE', 'MAINTENANCE', 'RETIRED'] as const;\nconst CI_ENVIRONMENTS = ['PRODUCTION', 'STAGING', 'DEVELOPMENT', 'DR'] as const;\n\nconst TYPE_ICON: Record<string, ComponentType<{ className?: string }>> = {\n SERVER: Server,\n SERVICE: Cog,\n DATABASE: Database,\n NETWORK: Network,\n APPLICATION: AppWindow,\n STORAGE: HardDrive,\n};\n\nconst STATUS_META: Record<string, { label: string; className: string }> = {\n ACTIVE: { label: 'Operational', className: 'bg-status-success-bg-subtle text-status-success-text' },\n MAINTENANCE: { label: 'Maintenance', className: 'bg-status-warning-bg-subtle text-status-warning-text' },\n INACTIVE: { label: 'Inactive', className: 'bg-bg-sunken text-text-secondary' },\n RETIRED: { label: 'Retired', className: 'bg-status-error-bg-subtle text-status-error-text' },\n};\n\nfunction StatusPill({ status }: { status: string }) {\n const meta = STATUS_META[status] ?? { label: status, className: 'bg-bg-sunken text-text-secondary' };\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',\n meta.className\n )}\n >\n {meta.label}\n </span>\n );\n}\n\n/** Track a min-width breakpoint so the CI detail can render as a rail (desktop) or sheet (mobile). */\nfunction useIsLargeScreen(): boolean {\n return useSyncExternalStore(\n (onChange) => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return () => {};\n const mq = window.matchMedia('(min-width: 1024px)');\n mq.addEventListener('change', onChange);\n return () => mq.removeEventListener('change', onChange);\n },\n () => (typeof window !== 'undefined' ? window.matchMedia('(min-width: 1024px)').matches : true),\n () => true\n );\n}\n\ntype AttrRow = { key: string; value: string };\n\nfunction toAttrRows(attributes: Record<string, unknown> | null | undefined): AttrRow[] {\n if (!attributes) return [];\n return Object.entries(attributes).map(([key, value]) => ({\n key,\n value: typeof value === 'string' ? value : JSON.stringify(value),\n }));\n}\n\nfunction UpsertConfigurationItemDialog({\n open,\n onOpenChange,\n seed,\n onSaved,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n seed: ConfigurationItem | null;\n onSaved: () => void;\n}) {\n const editing = Boolean(seed);\n const [ciNumber, setCiNumber] = useState('');\n const [name, setName] = useState('');\n const [ciType, setCiType] = useState<string>('SERVER');\n const [status, setStatus] = useState<string>('ACTIVE');\n const [environment, setEnvironment] = useState<string>('PRODUCTION');\n const [owner, setOwner] = useState('');\n const [attrs, setAttrs] = useState<AttrRow[]>([]);\n // Re-seed the form each time the dialog opens against a (possibly new) CI.\n const [seededId, setSeededId] = useState<string | null>(null);\n if (open && seededId !== (seed?.id ?? '__new__')) {\n setSeededId(seed?.id ?? '__new__');\n setCiNumber(seed?.ciNumber ?? '');\n setName(seed?.name ?? '');\n setCiType(seed?.ciType ?? 'SERVER');\n setStatus(seed?.status ?? 'ACTIVE');\n setEnvironment(seed?.environment ?? 'PRODUCTION');\n setOwner(seed?.owner ?? '');\n setAttrs(toAttrRows(seed?.attributes));\n }\n if (!open && seededId !== null) setSeededId(null);\n\n const [upsertItem, { loading }] = useMutation<UpsertConfigurationItemResult>(\n UPSERT_CONFIGURATION_ITEM_MUTATION,\n {\n onCompleted: (data) => {\n notify.success(\n editing ? 'Configuration item updated' : 'Configuration item saved',\n `${data.upsertConfigurationItem.ciNumber} is now in the CMDB.`\n );\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not save configuration item', error.message),\n }\n );\n\n const handleSubmit = () => {\n if (!ciNumber.trim() || !name.trim()) {\n notify.warning('Missing details', 'CI number and name are required.');\n return;\n }\n const attributes = attrs.reduce<Record<string, string>>((acc, row) => {\n if (row.key.trim()) acc[row.key.trim()] = row.value;\n return acc;\n }, {});\n void upsertItem({\n variables: {\n input: {\n ...(seed ? { id: seed.id } : {}),\n ciNumber: ciNumber.trim(),\n name: name.trim(),\n ciType,\n status,\n environment,\n owner: owner.trim() || null,\n attributes,\n },\n },\n });\n };\n\n const updateAttr = (index: number, patch: Partial<AttrRow>) =>\n setAttrs((rows) => rows.map((r, i) => (i === index ? { ...r, ...patch } : r)));\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{editing ? 'Edit CI' : 'Add CI'}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Register a configuration item in the CMDB and map it to your estate.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>CI Number *</Label>\n <Input\n placeholder=\"e.g., CI-1042\"\n value={ciNumber}\n onChange={(e) => setCiNumber(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Name *</Label>\n <Input value={name} onChange={(e) => setName(e.target.value)} />\n </div>\n </div>\n <div className=\"grid gap-4 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Type</Label>\n <Select value={ciType} onValueChange={setCiType}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CI_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {t}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Status</Label>\n <Select value={status} onValueChange={setStatus}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CI_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Environment</Label>\n <Select value={environment} onValueChange={setEnvironment}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CI_ENVIRONMENTS.map((e) => (\n <SelectItem key={e} value={e}>\n {e}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label>Owner</Label>\n <Input\n placeholder=\"e.g., Platform team\"\n value={owner}\n onChange={(e) => setOwner(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between\">\n <Label>Attributes</Label>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setAttrs((rows) => [...rows, { key: '', value: '' }])}\n >\n <Plus className=\"mr-1 h-3.5 w-3.5\" />\n Add\n </Button>\n </div>\n {attrs.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n Add structured key/value attributes (e.g. hostname, region, tier).\n </p>\n ) : (\n <div className=\"space-y-2\">\n {attrs.map((row, i) => (\n <div key={i} className=\"flex items-center gap-2\">\n <Input\n placeholder=\"key\"\n value={row.key}\n onChange={(e) => updateAttr(i, { key: e.target.value })}\n className=\"flex-1\"\n />\n <Input\n placeholder=\"value\"\n value={row.value}\n onChange={(e) => updateAttr(i, { value: e.target.value })}\n className=\"flex-1\"\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setAttrs((rows) => rows.filter((_, idx) => idx !== i))}\n aria-label=\"Remove attribute\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)} disabled={loading}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={loading}>\n {loading ? 'Saving…' : editing ? 'Save changes' : 'Add CI'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction DetailRow({ label, value }: { label: string; value: ReactNode }) {\n return (\n <div className=\"flex items-start justify-between gap-3 py-2\">\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span className=\"min-w-0 text-right text-sm font-medium text-text-primary\">{value}</span>\n </div>\n );\n}\n\nfunction CiDetail({\n ci,\n onEdit,\n onClose,\n}: {\n ci: ConfigurationItem;\n onEdit: () => void;\n onClose?: () => void;\n}) {\n const attrRows = toAttrRows(ci.attributes);\n const TypeIcon = TYPE_ICON[ci.ciType] ?? Cog;\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <TypeIcon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-base font-semibold text-text-primary\">{ci.name}</p>\n <p className=\"font-mono text-xs text-text-secondary\">{ci.ciNumber}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n <Button variant=\"outline\" size=\"sm\" onClick={onEdit}>\n <Pencil className=\"mr-1 h-3.5 w-3.5\" />\n Edit\n </Button>\n {onClose ? (\n <Button variant=\"ghost\" size=\"icon\" onClick={onClose} aria-label=\"Close detail\">\n <X className=\"h-4 w-4\" />\n </Button>\n ) : null}\n </div>\n </div>\n\n <div className=\"divide-y divide-border-seam/60\">\n <DetailRow label=\"Type\" value={ci.ciType} />\n <DetailRow label=\"Environment\" value={ci.environment ?? '—'} />\n <DetailRow label=\"Owner\" value={ci.owner ?? '—'} />\n <DetailRow label=\"Status\" value={<StatusPill status={ci.status} />} />\n <DetailRow\n label=\"Updated\"\n value={new Date(ci.updatedAt).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })}\n />\n </div>\n\n <div>\n <h4 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Attributes\n </h4>\n {attrRows.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">No attributes recorded for this CI.</p>\n ) : (\n <div className=\"divide-y divide-border-seam/60 rounded-lg bg-bg-sunken/40\">\n {attrRows.map((row) => (\n <div key={row.key} className=\"flex items-start justify-between gap-3 px-3 py-2\">\n <span className=\"text-sm text-text-secondary\">{row.key}</span>\n <span className=\"min-w-0 break-words text-right text-sm font-medium text-text-primary\">\n {row.value}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport function CmdbBrowserPage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [editSeed, setEditSeed] = useState<ConfigurationItem | null>(null);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [mobileDetailOpen, setMobileDetailOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<string>('ALL');\n const [envFilter, setEnvFilter] = useState<string>('ALL');\n const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});\n const isLarge = useIsLargeScreen();\n\n const { data, loading, refetch } = useQuery<{ configurationItems: ConfigurationItem[] }>(\n CONFIGURATION_ITEMS_QUERY,\n { fetchPolicy: 'cache-and-network' }\n );\n\n const cis = useMemo(() => data?.configurationItems ?? [], [data?.configurationItems]);\n\n const filtered = useMemo(() => {\n const term = search.trim().toLowerCase();\n return cis.filter((ci) => {\n if (statusFilter !== 'ALL' && ci.status !== statusFilter) return false;\n if (envFilter !== 'ALL' && ci.environment !== envFilter) return false;\n if (!term) return true;\n return [ci.ciNumber, ci.name, ci.ciType, ci.owner ?? '']\n .join(' ')\n .toLowerCase()\n .includes(term);\n });\n }, [cis, search, statusFilter, envFilter]);\n\n const grouped = useMemo(() => {\n const map = new Map<string, ConfigurationItem[]>();\n for (const ci of filtered) {\n const key = ci.ciType || 'OTHER';\n const bucket = map.get(key);\n if (bucket) bucket.push(ci);\n else map.set(key, [ci]);\n }\n return [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]));\n }, [filtered]);\n\n const stats = useMemo(() => {\n const total = cis.length;\n const operational = cis.filter((c) => c.status === 'ACTIVE').length;\n const maintenance = cis.filter((c) => c.status === 'MAINTENANCE').length;\n const retired = cis.filter((c) => c.status === 'RETIRED').length;\n return {\n total,\n operationalPct: total > 0 ? Math.round((operational / total) * 100) : 0,\n maintenance,\n retired,\n };\n }, [cis]);\n\n const statusMix = useMemo<DonutSlice[]>(() => {\n const counts = cis.reduce<Record<string, number>>((acc, c) => {\n acc[c.status] = (acc[c.status] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([status, value], i) => ({\n name: STATUS_META[status]?.label ?? status,\n value,\n color: seriesColor(i),\n }));\n }, [cis]);\n\n const typeBars = useMemo(() => {\n const counts = cis.reduce<Record<string, number>>((acc, c) => {\n acc[c.ciType] = (acc[c.ciType] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([type, count]) => ({ name: type, count }));\n }, [cis]);\n\n const selected = useMemo(\n () => cis.find((c) => c.id === selectedId) ?? null,\n [cis, selectedId]\n );\n\n const handleSelect = (ci: ConfigurationItem) => {\n setSelectedId(ci.id);\n if (!isLarge) setMobileDetailOpen(true);\n };\n\n const openCreate = () => {\n setEditSeed(null);\n setDialogOpen(true);\n };\n\n const openEdit = (ci: ConfigurationItem) => {\n setEditSeed(ci);\n setDialogOpen(true);\n setMobileDetailOpen(false);\n };\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <Network className=\"mt-1 h-6 w-6 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0\">\n <GradientText as=\"h1\" className=\"text-2xl font-bold sm:text-3xl\">\n CMDB — Configuration Items\n </GradientText>\n <p className=\"mt-1 max-w-2xl text-sm text-text-secondary\">\n A living map of every configuration item and its attributes.\n </p>\n </div>\n </div>\n <Button className=\"w-full sm:w-auto\" onClick={openCreate}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Add CI\n </Button>\n </div>\n <PagePurpose>\n Your configuration management database (CMDB): servers, services, network gear and how they\n relate. Browse, search and inspect each configuration item to understand blast radius before\n a change and keep asset relationships accurate.\n </PagePurpose>\n\n <UpsertConfigurationItemDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n seed={editSeed}\n onSaved={() => void refetch()}\n />\n\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile label=\"Total CIs\" value={stats.total} icon={Network} />\n <StatTile\n label=\"Operational\"\n value={stats.operationalPct}\n format={(n) => `${n}%`}\n icon={Server}\n deltaTone=\"positive\"\n />\n <StatTile label=\"In Maintenance\" value={stats.maintenance} icon={Cog} deltaTone=\"neutral\" />\n <StatTile label=\"Retired\" value={stats.retired} icon={HardDrive} deltaTone=\"neutral\" />\n </div>\n\n {cis.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Status mix\" subtitle=\"Configuration items by status\">\n <DonutChart\n data={statusMix}\n height={220}\n centerLabel=\"CIs\"\n centerValue={stats.total}\n ariaLabel=\"Configuration items by status\"\n />\n </ChartCard>\n <ChartCard title=\"By type\" subtitle=\"Configuration items per type\">\n <BarsChart\n data={typeBars}\n xKey=\"name\"\n series={[{ key: 'count', label: 'Items' }]}\n orientation=\"horizontal\"\n height={220}\n categoryWidth={110}\n ariaLabel=\"Configuration items per type\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n <div className=\"grid gap-4 lg:grid-cols-[1fr_360px]\">\n {/* Navigator */}\n <GlassCard treatment=\"glass\" className=\"border-border-seam\">\n <div className=\"space-y-3 p-4\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n className=\"pl-9\"\n placeholder=\"Search CIs by number, name, type or owner…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label=\"Search configuration items\"\n />\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger>\n <SelectValue placeholder=\"Status\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All statuses</SelectItem>\n {CI_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select value={envFilter} onValueChange={setEnvFilter}>\n <SelectTrigger>\n <SelectValue placeholder=\"Environment\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All environments</SelectItem>\n {CI_ENVIRONMENTS.map((e) => (\n <SelectItem key={e} value={e}>\n {e}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"border-t border-border-seam\">\n {loading && cis.length === 0 ? (\n <div className=\"space-y-2 p-4\">\n {Array.from({ length: 5 }).map((_, i) => (\n <div\n key={i}\n className=\"h-9 rounded-md bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : null}\n {!loading && filtered.length === 0 ? (\n <IllustratedEmptyState\n illustration={search || statusFilter !== 'ALL' || envFilter !== 'ALL' ? 'empty-search' : 'empty-data'}\n title={\n search || statusFilter !== 'ALL' || envFilter !== 'ALL'\n ? 'No configuration items match'\n : 'No configuration items found'\n }\n description={\n search || statusFilter !== 'ALL' || envFilter !== 'ALL'\n ? 'Try a different search or filter.'\n : 'Add a configuration item to start mapping your estate.'\n }\n />\n ) : null}\n <div className=\"max-h-[540px] overflow-y-auto\">\n {grouped.map(([type, items]) => {\n const TypeIcon = TYPE_ICON[type] ?? Cog;\n const isCollapsed = collapsed[type];\n return (\n <div key={type}>\n <button\n type=\"button\"\n onClick={() => setCollapsed((c) => ({ ...c, [type]: !c[type] }))}\n className=\"flex w-full items-center gap-2 border-b border-border-seam/60 bg-bg-sunken/30 px-4 py-2 text-left text-sm font-semibold text-text-primary\"\n >\n {isCollapsed ? (\n <ChevronRight className=\"h-4 w-4 text-text-secondary\" />\n ) : (\n <ChevronDown className=\"h-4 w-4 text-text-secondary\" />\n )}\n <TypeIcon className=\"h-4 w-4 text-primary\" />\n <span className=\"capitalize\">{type.toLowerCase()}</span>\n <span className=\"ml-auto rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary tabular-nums\">\n {items.length}\n </span>\n </button>\n {!isCollapsed\n ? items.map((ci) => (\n <button\n key={ci.id}\n type=\"button\"\n onClick={() => handleSelect(ci)}\n aria-current={selectedId === ci.id}\n className={cn(\n 'flex w-full items-center gap-3 border-b border-border-seam/40 px-4 py-2.5 pl-10 text-left last:border-b-0',\n selectedId === ci.id ? 'bg-action-primary-bg/10' : 'hover:bg-bg-sunken/40'\n )}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {ci.name}\n </p>\n <p className=\"truncate font-mono text-xs text-text-secondary\">\n {ci.ciNumber}\n {ci.environment ? ` · ${ci.environment}` : ''}\n </p>\n </div>\n <StatusPill status={ci.status} />\n </button>\n ))\n : null}\n </div>\n );\n })}\n </div>\n </div>\n </GlassCard>\n\n {/* Detail rail (desktop) */}\n <div className=\"hidden lg:block\">\n <GlassCard treatment=\"glass\" className=\"sticky top-4 border-border-seam\">\n <div className=\"p-4\">\n {selected ? (\n <CiDetail ci={selected} onEdit={() => openEdit(selected)} />\n ) : (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-center\">\n <Network className=\"h-8 w-8 text-text-muted\" />\n <p className=\"text-sm font-medium text-text-primary\">Select a configuration item</p>\n <p className=\"text-xs text-text-secondary\">\n Choose a CI from the navigator to inspect its attributes.\n </p>\n </div>\n )}\n </div>\n </GlassCard>\n </div>\n </div>\n\n {/* Detail sheet (mobile) */}\n <ResponsiveDialog open={mobileDetailOpen && !isLarge} onOpenChange={setMobileDetailOpen}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Configuration item</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n {selected ? (\n <CiDetail ci={selected} onEdit={() => openEdit(selected)} />\n ) : null}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CmdbBrowserPage.js","names":[],"sources":["../../../src/assethandler/pages/CmdbBrowserPage.tsx"],"sourcesContent":["import {\n Network,\n Plus,\n Search,\n Server,\n Database,\n AppWindow,\n HardDrive,\n Cog,\n ChevronDown,\n ChevronRight,\n Pencil,\n X,\n Trash2,\n} from 'lucide-react';\nimport type { ComponentType, ReactNode } from 'react';\nimport { useMemo, useState, useSyncExternalStore } from 'react';\nimport {\n Button,\n GradientText,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Input,\n Label,\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 { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n type DonutSlice,\n} from '../components';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst CONFIGURATION_ITEMS_QUERY = gql`\n query AssetHandlerConfigurationItems {\n configurationItems {\n id\n ciNumber\n name\n ciType\n status\n environment\n owner\n attributes\n updatedAt\n }\n }\n`;\n\nconst UPSERT_CONFIGURATION_ITEM_MUTATION = gql`\n mutation AssetHandlerUpsertConfigurationItem($input: UpsertConfigurationItemInput!) {\n upsertConfigurationItem(input: $input) {\n id\n ciNumber\n }\n }\n`;\n\ninterface ConfigurationItem {\n id: string;\n ciNumber: string;\n name: string;\n ciType: string;\n status: string;\n environment: string | null;\n owner: string | null;\n attributes: Record<string, unknown> | null;\n updatedAt: string;\n}\n\ninterface UpsertConfigurationItemResult {\n upsertConfigurationItem: { id: string; ciNumber: string };\n}\n\nconst CI_TYPES = ['SERVER', 'SERVICE', 'DATABASE', 'NETWORK', 'APPLICATION', 'STORAGE'] as const;\nconst CI_STATUSES = ['ACTIVE', 'INACTIVE', 'MAINTENANCE', 'RETIRED'] as const;\nconst CI_ENVIRONMENTS = ['PRODUCTION', 'STAGING', 'DEVELOPMENT', 'DR'] as const;\n\nconst TYPE_ICON: Record<string, ComponentType<{ className?: string }>> = {\n SERVER: Server,\n SERVICE: Cog,\n DATABASE: Database,\n NETWORK: Network,\n APPLICATION: AppWindow,\n STORAGE: HardDrive,\n};\n\nconst STATUS_META: Record<string, { label: string; className: string }> = {\n ACTIVE: {\n label: 'Operational',\n className: 'bg-status-success-bg-subtle text-status-success-text',\n },\n MAINTENANCE: {\n label: 'Maintenance',\n className: 'bg-status-warning-bg-subtle text-status-warning-text',\n },\n INACTIVE: { label: 'Inactive', className: 'bg-bg-sunken text-text-secondary' },\n RETIRED: { label: 'Retired', className: 'bg-status-error-bg-subtle text-status-error-text' },\n};\n\nfunction StatusPill({ status }: { status: string }) {\n const meta = STATUS_META[status] ?? {\n label: status,\n className: 'bg-bg-sunken text-text-secondary',\n };\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',\n meta.className\n )}\n >\n {meta.label}\n </span>\n );\n}\n\n/** Track a min-width breakpoint so the CI detail can render as a rail (desktop) or sheet (mobile). */\nfunction useIsLargeScreen(): boolean {\n return useSyncExternalStore(\n (onChange) => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return () => {};\n const mq = window.matchMedia('(min-width: 1024px)');\n mq.addEventListener('change', onChange);\n return () => mq.removeEventListener('change', onChange);\n },\n () => (typeof window !== 'undefined' ? window.matchMedia('(min-width: 1024px)').matches : true),\n () => true\n );\n}\n\ntype AttrRow = { key: string; value: string };\n\nfunction toAttrRows(attributes: Record<string, unknown> | null | undefined): AttrRow[] {\n if (!attributes) return [];\n return Object.entries(attributes).map(([key, value]) => ({\n key,\n value: typeof value === 'string' ? value : JSON.stringify(value),\n }));\n}\n\nfunction UpsertConfigurationItemDialog({\n open,\n onOpenChange,\n seed,\n onSaved,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n seed: ConfigurationItem | null;\n onSaved: () => void;\n}) {\n const editing = Boolean(seed);\n const [ciNumber, setCiNumber] = useState('');\n const [name, setName] = useState('');\n const [ciType, setCiType] = useState<string>('SERVER');\n const [status, setStatus] = useState<string>('ACTIVE');\n const [environment, setEnvironment] = useState<string>('PRODUCTION');\n const [owner, setOwner] = useState('');\n const [attrs, setAttrs] = useState<AttrRow[]>([]);\n // Re-seed the form each time the dialog opens against a (possibly new) CI.\n const [seededId, setSeededId] = useState<string | null>(null);\n if (open && seededId !== (seed?.id ?? '__new__')) {\n setSeededId(seed?.id ?? '__new__');\n setCiNumber(seed?.ciNumber ?? '');\n setName(seed?.name ?? '');\n setCiType(seed?.ciType ?? 'SERVER');\n setStatus(seed?.status ?? 'ACTIVE');\n setEnvironment(seed?.environment ?? 'PRODUCTION');\n setOwner(seed?.owner ?? '');\n setAttrs(toAttrRows(seed?.attributes));\n }\n if (!open && seededId !== null) setSeededId(null);\n\n const [upsertItem, { loading }] = useMutation<UpsertConfigurationItemResult>(\n UPSERT_CONFIGURATION_ITEM_MUTATION,\n {\n onCompleted: (data) => {\n notify.success(\n editing ? 'Configuration item updated' : 'Configuration item saved',\n `${data.upsertConfigurationItem.ciNumber} is now in the CMDB.`\n );\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not save configuration item', error.message),\n }\n );\n\n const handleSubmit = () => {\n if (!ciNumber.trim() || !name.trim()) {\n notify.warning('Missing details', 'CI number and name are required.');\n return;\n }\n const attributes = attrs.reduce<Record<string, string>>((acc, row) => {\n if (row.key.trim()) acc[row.key.trim()] = row.value;\n return acc;\n }, {});\n void upsertItem({\n variables: {\n input: {\n ...(seed ? { id: seed.id } : {}),\n ciNumber: ciNumber.trim(),\n name: name.trim(),\n ciType,\n status,\n environment,\n owner: owner.trim() || null,\n attributes,\n },\n },\n });\n };\n\n const updateAttr = (index: number, patch: Partial<AttrRow>) =>\n setAttrs((rows) => rows.map((r, i) => (i === index ? { ...r, ...patch } : r)));\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{editing ? 'Edit CI' : 'Add CI'}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Register a configuration item in the CMDB and map it to your estate.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>CI Number *</Label>\n <Input\n placeholder=\"e.g., CI-1042\"\n value={ciNumber}\n onChange={(e) => setCiNumber(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Name *</Label>\n <Input value={name} onChange={(e) => setName(e.target.value)} />\n </div>\n </div>\n <div className=\"grid gap-4 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Type</Label>\n <Select value={ciType} onValueChange={setCiType}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CI_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {t}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Status</Label>\n <Select value={status} onValueChange={setStatus}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CI_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Environment</Label>\n <Select value={environment} onValueChange={setEnvironment}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CI_ENVIRONMENTS.map((e) => (\n <SelectItem key={e} value={e}>\n {e}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label>Owner</Label>\n <Input\n placeholder=\"e.g., Platform team\"\n value={owner}\n onChange={(e) => setOwner(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between\">\n <Label>Attributes</Label>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setAttrs((rows) => [...rows, { key: '', value: '' }])}\n >\n <Plus className=\"mr-1 h-3.5 w-3.5\" />\n Add\n </Button>\n </div>\n {attrs.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n Add structured key/value attributes (e.g. hostname, region, tier).\n </p>\n ) : (\n <div className=\"space-y-2\">\n {attrs.map((row, i) => (\n <div key={i} className=\"flex items-center gap-2\">\n <Input\n placeholder=\"key\"\n value={row.key}\n onChange={(e) => updateAttr(i, { key: e.target.value })}\n className=\"flex-1\"\n />\n <Input\n placeholder=\"value\"\n value={row.value}\n onChange={(e) => updateAttr(i, { value: e.target.value })}\n className=\"flex-1\"\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setAttrs((rows) => rows.filter((_, idx) => idx !== i))}\n aria-label=\"Remove attribute\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)} disabled={loading}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={loading}>\n {loading ? 'Saving…' : editing ? 'Save changes' : 'Add CI'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction DetailRow({ label, value }: { label: string; value: ReactNode }) {\n return (\n <div className=\"flex items-start justify-between gap-3 py-2\">\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span className=\"min-w-0 text-right text-sm font-medium text-text-primary\">{value}</span>\n </div>\n );\n}\n\nfunction CiDetail({\n ci,\n onEdit,\n onClose,\n}: {\n ci: ConfigurationItem;\n onEdit: () => void;\n onClose?: () => void;\n}) {\n const attrRows = toAttrRows(ci.attributes);\n const TypeIcon = TYPE_ICON[ci.ciType] ?? Cog;\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <TypeIcon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-base font-semibold text-text-primary\">{ci.name}</p>\n <p className=\"font-mono text-xs text-text-secondary\">{ci.ciNumber}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n <Button variant=\"outline\" size=\"sm\" onClick={onEdit}>\n <Pencil className=\"mr-1 h-3.5 w-3.5\" />\n Edit\n </Button>\n {onClose ? (\n <Button variant=\"ghost\" size=\"icon\" onClick={onClose} aria-label=\"Close detail\">\n <X className=\"h-4 w-4\" />\n </Button>\n ) : null}\n </div>\n </div>\n\n <div className=\"divide-y divide-border-seam/60\">\n <DetailRow label=\"Type\" value={ci.ciType} />\n <DetailRow label=\"Environment\" value={ci.environment ?? '—'} />\n <DetailRow label=\"Owner\" value={ci.owner ?? '—'} />\n <DetailRow label=\"Status\" value={<StatusPill status={ci.status} />} />\n <DetailRow\n label=\"Updated\"\n value={new Date(ci.updatedAt).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })}\n />\n </div>\n\n <div>\n <h4 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Attributes\n </h4>\n {attrRows.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">No attributes recorded for this CI.</p>\n ) : (\n <div className=\"divide-y divide-border-seam/60 rounded-lg bg-bg-sunken/40\">\n {attrRows.map((row) => (\n <div key={row.key} className=\"flex items-start justify-between gap-3 px-3 py-2\">\n <span className=\"text-sm text-text-secondary\">{row.key}</span>\n <span className=\"min-w-0 break-words text-right text-sm font-medium text-text-primary\">\n {row.value}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport function CmdbBrowserPage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [editSeed, setEditSeed] = useState<ConfigurationItem | null>(null);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [mobileDetailOpen, setMobileDetailOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<string>('ALL');\n const [envFilter, setEnvFilter] = useState<string>('ALL');\n const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});\n const isLarge = useIsLargeScreen();\n\n const { data, loading, refetch } = useQuery<{ configurationItems: ConfigurationItem[] }>(\n CONFIGURATION_ITEMS_QUERY,\n { fetchPolicy: 'cache-and-network' }\n );\n\n const cis = useMemo(() => data?.configurationItems ?? [], [data?.configurationItems]);\n\n const filtered = useMemo(() => {\n const term = search.trim().toLowerCase();\n return cis.filter((ci) => {\n if (statusFilter !== 'ALL' && ci.status !== statusFilter) return false;\n if (envFilter !== 'ALL' && ci.environment !== envFilter) return false;\n if (!term) return true;\n return [ci.ciNumber, ci.name, ci.ciType, ci.owner ?? '']\n .join(' ')\n .toLowerCase()\n .includes(term);\n });\n }, [cis, search, statusFilter, envFilter]);\n\n const grouped = useMemo(() => {\n const map = new Map<string, ConfigurationItem[]>();\n for (const ci of filtered) {\n const key = ci.ciType || 'OTHER';\n const bucket = map.get(key);\n if (bucket) bucket.push(ci);\n else map.set(key, [ci]);\n }\n return [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]));\n }, [filtered]);\n\n const stats = useMemo(() => {\n const total = cis.length;\n const operational = cis.filter((c) => c.status === 'ACTIVE').length;\n const maintenance = cis.filter((c) => c.status === 'MAINTENANCE').length;\n const retired = cis.filter((c) => c.status === 'RETIRED').length;\n return {\n total,\n operationalPct: total > 0 ? Math.round((operational / total) * 100) : 0,\n maintenance,\n retired,\n };\n }, [cis]);\n\n const statusMix = useMemo<DonutSlice[]>(() => {\n const counts = cis.reduce<Record<string, number>>((acc, c) => {\n acc[c.status] = (acc[c.status] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([status, value], i) => ({\n name: STATUS_META[status]?.label ?? status,\n value,\n color: seriesColor(i),\n }));\n }, [cis]);\n\n const typeBars = useMemo(() => {\n const counts = cis.reduce<Record<string, number>>((acc, c) => {\n acc[c.ciType] = (acc[c.ciType] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts)\n .sort((a, b) => b[1] - a[1])\n .map(([type, count]) => ({ name: type, count }));\n }, [cis]);\n\n const selected = useMemo(() => cis.find((c) => c.id === selectedId) ?? null, [cis, selectedId]);\n\n const handleSelect = (ci: ConfigurationItem) => {\n setSelectedId(ci.id);\n if (!isLarge) setMobileDetailOpen(true);\n };\n\n const openCreate = () => {\n setEditSeed(null);\n setDialogOpen(true);\n };\n\n const openEdit = (ci: ConfigurationItem) => {\n setEditSeed(ci);\n setDialogOpen(true);\n setMobileDetailOpen(false);\n };\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <Network className=\"mt-1 h-6 w-6 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0\">\n <GradientText as=\"h1\" className=\"text-2xl font-bold sm:text-3xl\">\n CMDB — Configuration Items\n </GradientText>\n <p className=\"mt-1 max-w-2xl text-sm text-text-secondary\">\n A living map of every configuration item and its attributes.\n </p>\n </div>\n </div>\n <Button className=\"w-full sm:w-auto\" onClick={openCreate}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Add CI\n </Button>\n </div>\n <PagePurpose>\n Your configuration management database (CMDB): servers, services, network gear and how they\n relate. Browse, search and inspect each configuration item to understand blast radius before\n a change and keep asset relationships accurate.\n </PagePurpose>\n\n <UpsertConfigurationItemDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n seed={editSeed}\n onSaved={() => void refetch()}\n />\n\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile label=\"Total CIs\" value={stats.total} icon={Network} />\n <StatTile\n label=\"Operational\"\n value={stats.operationalPct}\n format={(n) => `${n}%`}\n icon={Server}\n deltaTone=\"positive\"\n />\n <StatTile label=\"In Maintenance\" value={stats.maintenance} icon={Cog} deltaTone=\"neutral\" />\n <StatTile label=\"Retired\" value={stats.retired} icon={HardDrive} deltaTone=\"neutral\" />\n </div>\n\n {cis.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Status mix\" subtitle=\"Configuration items by status\">\n <DonutChart\n data={statusMix}\n height={220}\n centerLabel=\"CIs\"\n centerValue={stats.total}\n ariaLabel=\"Configuration items by status\"\n />\n </ChartCard>\n <ChartCard title=\"By type\" subtitle=\"Configuration items per type\">\n <BarsChart\n data={typeBars}\n xKey=\"name\"\n series={[{ key: 'count', label: 'Items' }]}\n orientation=\"horizontal\"\n height={220}\n categoryWidth={110}\n ariaLabel=\"Configuration items per type\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n <div className=\"grid gap-4 lg:grid-cols-[1fr_360px]\">\n {/* Navigator */}\n <GlassCard treatment=\"glass\" className=\"border-border-seam\">\n <div className=\"space-y-3 p-4\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n className=\"pl-9\"\n placeholder=\"Search CIs by number, name, type or owner…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n aria-label=\"Search configuration items\"\n />\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger>\n <SelectValue placeholder=\"Status\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All statuses</SelectItem>\n {CI_STATUSES.map((s) => (\n <SelectItem key={s} value={s}>\n {s}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select value={envFilter} onValueChange={setEnvFilter}>\n <SelectTrigger>\n <SelectValue placeholder=\"Environment\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All environments</SelectItem>\n {CI_ENVIRONMENTS.map((e) => (\n <SelectItem key={e} value={e}>\n {e}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"border-t border-border-seam\">\n {loading && cis.length === 0 ? (\n <div className=\"space-y-2 p-4\">\n {Array.from({ length: 5 }).map((_, i) => (\n <div\n key={i}\n className=\"h-9 rounded-md bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : null}\n {!loading && filtered.length === 0 ? (\n <IllustratedEmptyState\n illustration={\n search || statusFilter !== 'ALL' || envFilter !== 'ALL'\n ? 'empty-search'\n : 'empty-data'\n }\n title={\n search || statusFilter !== 'ALL' || envFilter !== 'ALL'\n ? 'No configuration items match'\n : 'No configuration items found'\n }\n description={\n search || statusFilter !== 'ALL' || envFilter !== 'ALL'\n ? 'Try a different search or filter.'\n : 'Add a configuration item to start mapping your estate.'\n }\n />\n ) : null}\n <div className=\"max-h-[540px] overflow-y-auto\">\n {grouped.map(([type, items]) => {\n const TypeIcon = TYPE_ICON[type] ?? Cog;\n const isCollapsed = collapsed[type];\n return (\n <div key={type}>\n <button\n type=\"button\"\n onClick={() => setCollapsed((c) => ({ ...c, [type]: !c[type] }))}\n className=\"flex w-full items-center gap-2 border-b border-border-seam/60 bg-bg-sunken/30 px-4 py-2 text-left text-sm font-semibold text-text-primary\"\n >\n {isCollapsed ? (\n <ChevronRight className=\"h-4 w-4 text-text-secondary\" />\n ) : (\n <ChevronDown className=\"h-4 w-4 text-text-secondary\" />\n )}\n <TypeIcon className=\"h-4 w-4 text-primary\" />\n <span className=\"capitalize\">{type.toLowerCase()}</span>\n <span className=\"ml-auto rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary tabular-nums\">\n {items.length}\n </span>\n </button>\n {!isCollapsed\n ? items.map((ci) => (\n <button\n key={ci.id}\n type=\"button\"\n onClick={() => handleSelect(ci)}\n aria-current={selectedId === ci.id}\n className={cn(\n 'flex w-full items-center gap-3 border-b border-border-seam/40 px-4 py-2.5 pl-10 text-left last:border-b-0',\n selectedId === ci.id\n ? 'bg-action-primary-bg/10'\n : 'hover:bg-bg-sunken/40'\n )}\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {ci.name}\n </p>\n <p className=\"truncate font-mono text-xs text-text-secondary\">\n {ci.ciNumber}\n {ci.environment ? ` · ${ci.environment}` : ''}\n </p>\n </div>\n <StatusPill status={ci.status} />\n </button>\n ))\n : null}\n </div>\n );\n })}\n </div>\n </div>\n </GlassCard>\n\n {/* Detail rail (desktop) */}\n <div className=\"hidden lg:block\">\n <GlassCard treatment=\"glass\" className=\"sticky top-4 border-border-seam\">\n <div className=\"p-4\">\n {selected ? (\n <CiDetail ci={selected} onEdit={() => openEdit(selected)} />\n ) : (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-center\">\n <Network className=\"h-8 w-8 text-text-muted\" />\n <p className=\"text-sm font-medium text-text-primary\">\n Select a configuration item\n </p>\n <p className=\"text-xs text-text-secondary\">\n Choose a CI from the navigator to inspect its attributes.\n </p>\n </div>\n )}\n </div>\n </GlassCard>\n </div>\n </div>\n\n {/* Detail sheet (mobile) */}\n <ResponsiveDialog open={mobileDetailOpen && !isLarge} onOpenChange={setMobileDetailOpen}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Configuration item</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n {selected ? <CiDetail ci={selected} onEdit={() => openEdit(selected)} /> : null}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </div>\n );\n}\n"],"mappings":"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"}
|