@burdenoff/microfe-assethandler 2026.825.1 → 2026.826.2
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/components/ActivityTimeline.d.ts.map +1 -1
- package/dist/assethandler/components/ActivityTimeline.js +2 -0
- package/dist/assethandler/components/ActivityTimeline.js.map +1 -1
- package/dist/assethandler/components/AttachmentsPanel.d.ts.map +1 -1
- package/dist/assethandler/components/AttachmentsPanel.js.map +1 -1
- package/dist/assethandler/pages/EsgDashboardPage.d.ts +43 -0
- package/dist/assethandler/pages/EsgDashboardPage.d.ts.map +1 -1
- package/dist/assethandler/pages/EsgDashboardPage.js +263 -165
- package/dist/assethandler/pages/EsgDashboardPage.js.map +1 -1
- package/dist/types/assistant.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityTimeline.d.ts","sourceRoot":"","sources":["../../../src/assethandler/components/ActivityTimeline.tsx"],"names":[],"mappings":"AA2BA;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+ED,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,KAAU,EAAE,EAAE,qBAAqB,+
|
|
1
|
+
{"version":3,"file":"ActivityTimeline.d.ts","sourceRoot":"","sources":["../../../src/assethandler/components/ActivityTimeline.tsx"],"names":[],"mappings":"AA2BA;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+ED,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,KAAU,EAAE,EAAE,qBAAqB,+BA4IjF"}
|
|
@@ -102,6 +102,8 @@ function k({ resourceId: i, limit: a = 20 }) {
|
|
|
102
102
|
let { workspaceId: o, currentUser: s } = e(), { data: c, loading: l } = g(w, {
|
|
103
103
|
variables: { filter: {
|
|
104
104
|
resourceId: i,
|
|
105
|
+
resourceType: "asset",
|
|
106
|
+
productId: "assethandler",
|
|
105
107
|
...o ? { workspaceId: o } : {},
|
|
106
108
|
limit: a
|
|
107
109
|
} },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityTimeline.js","names":[],"sources":["../../../src/assethandler/components/ActivityTimeline.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Activity,\n Plus,\n Pencil,\n Trash2,\n CheckCircle2,\n LogOut,\n LogIn,\n RefreshCw,\n Paperclip,\n MessageSquare,\n Wrench,\n ShieldAlert,\n} from 'lucide-react';\nimport { useAssetHandler } from '../providers/AssetHandlerProvider';\nimport { Sparkline } from './charts';\n\n/**\n * Recent activity for a single AssetHandler resource, sourced from the\n * federated wspace-activity-svc `getActivities` query (fed by be-sdk\n * `trackOperation` on the backend). Read-only, with a graceful empty state.\n *\n * The header carries an activity-volume sparkline bucketed from the real\n * timestamps — it only renders when there is enough history to be meaningful,\n * never fabricated.\n */\nexport interface ActivityTimelineProps {\n resourceId: string;\n /** Optional cap on rows shown. Defaults to 20. */\n limit?: number;\n}\n\ninterface ActivityLogNode {\n id: string;\n action: string;\n description?: string | null;\n timestamp: string;\n userId: string;\n resourceType?: string | null;\n}\n\ninterface GetActivitiesResult {\n getActivities: ActivityLogNode[];\n}\n\nconst GET_ACTIVITIES_QUERY = gql`\n query AssetHandlerResourceActivities($filter: ActivityLogFilter) {\n getActivities(filter: $filter) {\n id\n action\n description\n timestamp\n userId\n resourceType\n }\n }\n`;\n\n/** Compact relative time (\"2 hrs ago\"), with the full timestamp on hover. */\nfunction relativeTime(iso: string): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diff = Date.now() - then;\n const sec = Math.round(diff / 1000);\n if (sec < 45) return 'just now';\n const min = Math.round(sec / 60);\n if (min < 60) return `${min} min ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr} hr${hr === 1 ? '' : 's'} ago`;\n const day = Math.round(hr / 24);\n if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`;\n return new Date(iso).toLocaleDateString();\n}\n\ntype Tone = 'success' | 'info' | 'warning' | 'error' | 'neutral';\n\nconst TONE_CLASS: Record<Tone, { icon: string; tile: string }> = {\n success: { icon: 'text-status-success-text', tile: 'bg-status-success-bg-subtle' },\n info: { icon: 'text-status-info-text', tile: 'bg-status-info-bg-subtle' },\n warning: { icon: 'text-status-warning-text', tile: 'bg-status-warning-bg-subtle' },\n error: { icon: 'text-status-error-text', tile: 'bg-status-error-bg-subtle' },\n neutral: { icon: 'text-text-secondary', tile: 'bg-bg-sunken' },\n};\n\n/** Map an activity action verb to a node icon + status tone. */\nfunction actionVisual(action: string): { Icon: typeof Activity; tone: Tone } {\n const a = action.toLowerCase();\n if (/(complete|resolve|close|done|approve|pass|success)/.test(a))\n return { Icon: CheckCircle2, tone: 'success' };\n if (/(delete|remove|dispose|retire|archive|reject|fail)/.test(a))\n return { Icon: Trash2, tone: 'error' };\n if (/(check[_.\\s]?out|assign|issue|lend)/.test(a)) return { Icon: LogOut, tone: 'warning' };\n if (/(check[_.\\s]?in|return|receive)/.test(a)) return { Icon: LogIn, tone: 'success' };\n if (/(maintenance|repair|service|calibrat)/.test(a)) return { Icon: Wrench, tone: 'warning' };\n if (/(incident|breach|alert|violation|risk|security)/.test(a))\n return { Icon: ShieldAlert, tone: 'error' };\n if (/(upload|attach|file)/.test(a)) return { Icon: Paperclip, tone: 'info' };\n if (/(comment|note|reply|discuss)/.test(a)) return { Icon: MessageSquare, tone: 'info' };\n if (/(create|add|new|register|provision)/.test(a)) return { Icon: Plus, tone: 'success' };\n if (/(update|edit|change|modify|revise|status)/.test(a)) return { Icon: Pencil, tone: 'info' };\n if (/(sync|refresh|move|transfer|relocat)/.test(a)) return { Icon: RefreshCw, tone: 'info' };\n return { Icon: Activity, tone: 'neutral' };\n}\n\nfunction titleize(action: string): string {\n const s = action.replace(/[._]/g, ' ').trim();\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function ActivityTimeline({ resourceId, limit = 20 }: ActivityTimelineProps) {\n const { workspaceId, currentUser } = useAssetHandler();\n\n const { data, loading } = useQuery<GetActivitiesResult>(GET_ACTIVITIES_QUERY, {\n variables: {\n filter: {\n resourceId,\n ...(workspaceId ? { workspaceId } : {}),\n limit,\n },\n },\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n skip: !resourceId,\n });\n\n const activities = useMemo(() => data?.getActivities ?? [], [data]);\n\n // Bucket real timestamps into per-day volume for a header sparkline. Only\n // meaningful when there is enough history spanning multiple days — otherwise\n // we omit it rather than draw a flat/fabricated line.\n const volumeSeries = useMemo(() => {\n if (activities.length < 6) return null;\n const DAYS = 14;\n const now = new Date();\n const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();\n const buckets = new Array<number>(DAYS).fill(0);\n let distinctDays = 0;\n const seen = new Set<number>();\n for (const item of activities) {\n const t = new Date(item.timestamp).getTime();\n if (Number.isNaN(t)) continue;\n const dayIndex = DAYS - 1 - Math.floor((startOfToday - t) / 86_400_000);\n if (dayIndex >= 0 && dayIndex < DAYS) {\n buckets[dayIndex] += 1;\n const dayKey = Math.floor(t / 86_400_000);\n if (!seen.has(dayKey)) {\n seen.add(dayKey);\n distinctDays += 1;\n }\n }\n }\n return distinctDays >= 3 ? buckets : null;\n }, [activities]);\n\n const actorLabel = (userId: string): string =>\n currentUser?.id && userId === currentUser.id\n ? currentUser.name ||\n [currentUser.firstName, currentUser.lastName].filter(Boolean).join(' ') ||\n currentUser.username ||\n currentUser.email?.split('@')[0] ||\n 'You'\n : userId === 'system' || userId === 'SYSTEM'\n ? 'System'\n : `Member ${userId.slice(0, 6)}`;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-3 space-y-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Activity className=\"h-4 w-4\" />\n Activity\n </CardTitle>\n {volumeSeries ? (\n <div className=\"flex items-center gap-2\" title=\"Activity over the last 14 days\">\n <span className=\"hidden text-xs text-text-secondary sm:inline\">Last 14 days</span>\n <Sparkline\n data={volumeSeries}\n type=\"area\"\n height={28}\n className=\"w-24\"\n ariaLabel=\"Activity volume over the last 14 days\"\n />\n </div>\n ) : null}\n </CardHeader>\n <CardContent>\n {loading && activities.length === 0 ? (\n <div className=\"space-y-2\">\n {Array.from({ length: 4 }).map((_, i) => (\n <div\n key={i}\n className=\"h-12 rounded-md bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : activities.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"No activity yet\"\n description=\"Changes to this record — updates, status changes and assignments — will show up here.\"\n />\n ) : (\n <ol className=\"space-y-1\">\n {activities.map((item, index) => {\n const { Icon, tone } = actionVisual(item.action);\n const tc = TONE_CLASS[tone];\n const isLast = index === activities.length - 1;\n return (\n <li key={item.id} className=\"relative flex gap-3 pb-3\">\n {/* Connector line between nodes. */}\n {!isLast ? (\n <span\n className=\"absolute left-[0.9375rem] top-8 h-[calc(100%-1rem)] w-px bg-border-subtle\"\n aria-hidden=\"true\"\n />\n ) : null}\n <span\n className={`relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${tc.tile}`}\n aria-hidden=\"true\"\n >\n <Icon className={`h-4 w-4 ${tc.icon}`} />\n </span>\n <div className=\"min-w-0 flex-1 pt-0.5\">\n <p className=\"text-sm font-medium text-text-primary\">{titleize(item.action)}</p>\n {item.description ? (\n <p className=\"truncate text-sm text-text-secondary\" title={item.description}>\n {item.description}\n </p>\n ) : null}\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n <span className=\"font-medium text-text-secondary\">\n {actorLabel(item.userId)}\n </span>{' '}\n ·{' '}\n <span title={new Date(item.timestamp).toLocaleString()}>\n {relativeTime(item.timestamp)}\n </span>\n </p>\n </div>\n </li>\n );\n })}\n </ol>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ActivityTimeline.js","names":[],"sources":["../../../src/assethandler/components/ActivityTimeline.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Activity,\n Plus,\n Pencil,\n Trash2,\n CheckCircle2,\n LogOut,\n LogIn,\n RefreshCw,\n Paperclip,\n MessageSquare,\n Wrench,\n ShieldAlert,\n} from 'lucide-react';\nimport { useAssetHandler } from '../providers/AssetHandlerProvider';\nimport { Sparkline } from './charts';\n\n/**\n * Recent activity for a single AssetHandler resource, sourced from the\n * federated wspace-activity-svc `getActivities` query (fed by be-sdk\n * `trackOperation` on the backend). Read-only, with a graceful empty state.\n *\n * The header carries an activity-volume sparkline bucketed from the real\n * timestamps — it only renders when there is enough history to be meaningful,\n * never fabricated.\n */\nexport interface ActivityTimelineProps {\n resourceId: string;\n /** Optional cap on rows shown. Defaults to 20. */\n limit?: number;\n}\n\ninterface ActivityLogNode {\n id: string;\n action: string;\n description?: string | null;\n timestamp: string;\n userId: string;\n resourceType?: string | null;\n}\n\ninterface GetActivitiesResult {\n getActivities: ActivityLogNode[];\n}\n\nconst GET_ACTIVITIES_QUERY = gql`\n query AssetHandlerResourceActivities($filter: ActivityLogFilter) {\n getActivities(filter: $filter) {\n id\n action\n description\n timestamp\n userId\n resourceType\n }\n }\n`;\n\n/** Compact relative time (\"2 hrs ago\"), with the full timestamp on hover. */\nfunction relativeTime(iso: string): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diff = Date.now() - then;\n const sec = Math.round(diff / 1000);\n if (sec < 45) return 'just now';\n const min = Math.round(sec / 60);\n if (min < 60) return `${min} min ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr} hr${hr === 1 ? '' : 's'} ago`;\n const day = Math.round(hr / 24);\n if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`;\n return new Date(iso).toLocaleDateString();\n}\n\ntype Tone = 'success' | 'info' | 'warning' | 'error' | 'neutral';\n\nconst TONE_CLASS: Record<Tone, { icon: string; tile: string }> = {\n success: { icon: 'text-status-success-text', tile: 'bg-status-success-bg-subtle' },\n info: { icon: 'text-status-info-text', tile: 'bg-status-info-bg-subtle' },\n warning: { icon: 'text-status-warning-text', tile: 'bg-status-warning-bg-subtle' },\n error: { icon: 'text-status-error-text', tile: 'bg-status-error-bg-subtle' },\n neutral: { icon: 'text-text-secondary', tile: 'bg-bg-sunken' },\n};\n\n/** Map an activity action verb to a node icon + status tone. */\nfunction actionVisual(action: string): { Icon: typeof Activity; tone: Tone } {\n const a = action.toLowerCase();\n if (/(complete|resolve|close|done|approve|pass|success)/.test(a))\n return { Icon: CheckCircle2, tone: 'success' };\n if (/(delete|remove|dispose|retire|archive|reject|fail)/.test(a))\n return { Icon: Trash2, tone: 'error' };\n if (/(check[_.\\s]?out|assign|issue|lend)/.test(a)) return { Icon: LogOut, tone: 'warning' };\n if (/(check[_.\\s]?in|return|receive)/.test(a)) return { Icon: LogIn, tone: 'success' };\n if (/(maintenance|repair|service|calibrat)/.test(a)) return { Icon: Wrench, tone: 'warning' };\n if (/(incident|breach|alert|violation|risk|security)/.test(a))\n return { Icon: ShieldAlert, tone: 'error' };\n if (/(upload|attach|file)/.test(a)) return { Icon: Paperclip, tone: 'info' };\n if (/(comment|note|reply|discuss)/.test(a)) return { Icon: MessageSquare, tone: 'info' };\n if (/(create|add|new|register|provision)/.test(a)) return { Icon: Plus, tone: 'success' };\n if (/(update|edit|change|modify|revise|status)/.test(a)) return { Icon: Pencil, tone: 'info' };\n if (/(sync|refresh|move|transfer|relocat)/.test(a)) return { Icon: RefreshCw, tone: 'info' };\n return { Icon: Activity, tone: 'neutral' };\n}\n\nfunction titleize(action: string): string {\n const s = action.replace(/[._]/g, ' ').trim();\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function ActivityTimeline({ resourceId, limit = 20 }: ActivityTimelineProps) {\n const { workspaceId, currentUser } = useAssetHandler();\n\n const { data, loading } = useQuery<GetActivitiesResult>(GET_ACTIVITIES_QUERY, {\n variables: {\n filter: {\n resourceId,\n resourceType: 'asset',\n productId: 'assethandler',\n ...(workspaceId ? { workspaceId } : {}),\n limit,\n },\n },\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n skip: !resourceId,\n });\n\n const activities = useMemo(() => data?.getActivities ?? [], [data]);\n\n // Bucket real timestamps into per-day volume for a header sparkline. Only\n // meaningful when there is enough history spanning multiple days — otherwise\n // we omit it rather than draw a flat/fabricated line.\n const volumeSeries = useMemo(() => {\n if (activities.length < 6) return null;\n const DAYS = 14;\n const now = new Date();\n const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();\n const buckets = new Array<number>(DAYS).fill(0);\n let distinctDays = 0;\n const seen = new Set<number>();\n for (const item of activities) {\n const t = new Date(item.timestamp).getTime();\n if (Number.isNaN(t)) continue;\n const dayIndex = DAYS - 1 - Math.floor((startOfToday - t) / 86_400_000);\n if (dayIndex >= 0 && dayIndex < DAYS) {\n buckets[dayIndex] += 1;\n const dayKey = Math.floor(t / 86_400_000);\n if (!seen.has(dayKey)) {\n seen.add(dayKey);\n distinctDays += 1;\n }\n }\n }\n return distinctDays >= 3 ? buckets : null;\n }, [activities]);\n\n const actorLabel = (userId: string): string =>\n currentUser?.id && userId === currentUser.id\n ? currentUser.name ||\n [currentUser.firstName, currentUser.lastName].filter(Boolean).join(' ') ||\n currentUser.username ||\n currentUser.email?.split('@')[0] ||\n 'You'\n : userId === 'system' || userId === 'SYSTEM'\n ? 'System'\n : `Member ${userId.slice(0, 6)}`;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-3 space-y-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Activity className=\"h-4 w-4\" />\n Activity\n </CardTitle>\n {volumeSeries ? (\n <div className=\"flex items-center gap-2\" title=\"Activity over the last 14 days\">\n <span className=\"hidden text-xs text-text-secondary sm:inline\">Last 14 days</span>\n <Sparkline\n data={volumeSeries}\n type=\"area\"\n height={28}\n className=\"w-24\"\n ariaLabel=\"Activity volume over the last 14 days\"\n />\n </div>\n ) : null}\n </CardHeader>\n <CardContent>\n {loading && activities.length === 0 ? (\n <div className=\"space-y-2\">\n {Array.from({ length: 4 }).map((_, i) => (\n <div\n key={i}\n className=\"h-12 rounded-md bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : activities.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"No activity yet\"\n description=\"Changes to this record — updates, status changes and assignments — will show up here.\"\n />\n ) : (\n <ol className=\"space-y-1\">\n {activities.map((item, index) => {\n const { Icon, tone } = actionVisual(item.action);\n const tc = TONE_CLASS[tone];\n const isLast = index === activities.length - 1;\n return (\n <li key={item.id} className=\"relative flex gap-3 pb-3\">\n {/* Connector line between nodes. */}\n {!isLast ? (\n <span\n className=\"absolute left-[0.9375rem] top-8 h-[calc(100%-1rem)] w-px bg-border-subtle\"\n aria-hidden=\"true\"\n />\n ) : null}\n <span\n className={`relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${tc.tile}`}\n aria-hidden=\"true\"\n >\n <Icon className={`h-4 w-4 ${tc.icon}`} />\n </span>\n <div className=\"min-w-0 flex-1 pt-0.5\">\n <p className=\"text-sm font-medium text-text-primary\">{titleize(item.action)}</p>\n {item.description ? (\n <p className=\"truncate text-sm text-text-secondary\" title={item.description}>\n {item.description}\n </p>\n ) : null}\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n <span className=\"font-medium text-text-secondary\">\n {actorLabel(item.userId)}\n </span>{' '}\n ·{' '}\n <span title={new Date(item.timestamp).toLocaleString()}>\n {relativeTime(item.timestamp)}\n </span>\n </p>\n </div>\n </li>\n );\n })}\n </ol>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentsPanel.d.ts","sourceRoot":"","sources":["../../../src/assethandler/components/AttachmentsPanel.tsx"],"names":[],"mappings":"AAiCA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"AttachmentsPanel.d.ts","sourceRoot":"","sources":["../../../src/assethandler/components/AttachmentsPanel.tsx"],"names":[],"mappings":"AAiCA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAmND,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,qBAAqB,+BAmP/E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentsPanel.js","names":[],"sources":["../../../src/assethandler/components/AttachmentsPanel.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n IllustratedEmptyState,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Paperclip,\n Upload,\n Link2,\n FileText,\n FileSpreadsheet,\n FileArchive,\n FileCode2,\n FileVideo,\n FileAudio,\n Image as ImageIcon,\n File as FileIcon,\n Loader2,\n Download,\n MoreHorizontal,\n} from 'lucide-react';\nimport { notify } from '../utils/toast';\n\n/**\n * Attach and reference files for an AssetHandler entity, backed by the\n * federated wspace-files-svc (same wspace gateway/Apollo client). Fully real (no fabricated data) —\n * lists workspace files via `getFiles`, uploads new files through the\n * `initiateUpload` → presigned PUT → `completeUpload` flow, and mints public\n * share links via `createFileShareLink`. All entries are live data.\n *\n * `entityType`/`entityId` scope the panel to the record it is mounted on and\n * are used for context labelling; uploaded files land in the workspace file\n * store so they can be shared and reused.\n */\nexport interface AttachmentsPanelProps {\n entityType: string;\n entityId: string;\n}\n\ninterface FileNode {\n id: string;\n name: string;\n extension: string;\n mimeType: string;\n size: number;\n downloadUrl?: string | null;\n createdAt: string;\n}\n\ninterface GetFilesResult {\n getFiles: {\n files: FileNode[];\n total: number;\n hasMore: boolean;\n };\n}\n\nconst GET_FILES_QUERY = gql`\n query AssetHandlerGetFiles($pagination: PaginationInput, $sort: FileSortInput) {\n getFiles(pagination: $pagination, sort: $sort) {\n total\n hasMore\n files {\n id\n name\n extension\n mimeType\n size\n downloadUrl\n createdAt\n }\n }\n }\n`;\n\nconst INITIATE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n }\n }\n }\n`;\n\nconst CREATE_SHARE_LINK_MUTATION = gql`\n mutation AssetHandlerCreateFileShareLink($input: CreateShareGrantLinkInput!) {\n createFileShareLink(input: $input) {\n id\n shareUrl\n }\n }\n`;\n\nfunction formatSize(bytes: number): string {\n if (!bytes) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB'];\n const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);\n const value = bytes / Math.pow(1024, exponent);\n return `${value.toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n}\n\nfunction extensionOf(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n}\n\n/** Compact relative time (\"2 hrs ago\"), with the full timestamp on hover. */\nfunction relativeTime(iso: string): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diff = Date.now() - then;\n const sec = Math.round(diff / 1000);\n if (sec < 45) return 'just now';\n const min = Math.round(sec / 60);\n if (min < 60) return `${min} min ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr} hr${hr === 1 ? '' : 's'} ago`;\n const day = Math.round(hr / 24);\n if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`;\n return new Date(iso).toLocaleDateString();\n}\n\ntype FileKind =\n | 'image'\n | 'pdf'\n | 'sheet'\n | 'doc'\n | 'archive'\n | 'code'\n | 'video'\n | 'audio'\n | 'other';\n\n/** Map a file's extension/mime to a recognisable kind for icon + tint. */\nfunction kindOf(file: FileNode): FileKind {\n const mime = (file.mimeType || '').toLowerCase();\n const ext = (file.extension || extensionOf(file.name)).toLowerCase();\n if (\n mime.startsWith('image/') ||\n ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'bmp'].includes(ext)\n )\n return 'image';\n if (mime.startsWith('video/') || ['mp4', 'mov', 'webm', 'avi', 'mkv'].includes(ext))\n return 'video';\n if (mime.startsWith('audio/') || ['mp3', 'wav', 'ogg', 'm4a', 'flac'].includes(ext))\n return 'audio';\n if (mime === 'application/pdf' || ext === 'pdf') return 'pdf';\n if (['xls', 'xlsx', 'csv', 'tsv', 'ods'].includes(ext)) return 'sheet';\n if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(ext)) return 'archive';\n if (\n [\n 'js',\n 'ts',\n 'tsx',\n 'jsx',\n 'json',\n 'xml',\n 'yml',\n 'yaml',\n 'html',\n 'css',\n 'sh',\n 'py',\n 'sql',\n ].includes(ext)\n )\n return 'code';\n if (['doc', 'docx', 'txt', 'md', 'rtf', 'odt', 'pages'].includes(ext)) return 'doc';\n return 'other';\n}\n\nconst KIND_STYLE: Record<\n FileKind,\n { Icon: typeof FileText; iconClass: string; tileClass: string }\n> = {\n image: {\n Icon: ImageIcon,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n pdf: {\n Icon: FileText,\n iconClass: 'text-status-error-text',\n tileClass: 'bg-status-error-bg-subtle',\n },\n sheet: {\n Icon: FileSpreadsheet,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n doc: {\n Icon: FileText,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n archive: {\n Icon: FileArchive,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n code: {\n Icon: FileCode2,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n video: {\n Icon: FileVideo,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n audio: {\n Icon: FileAudio,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n other: { Icon: FileIcon, iconClass: 'text-text-secondary', tileClass: 'bg-bg-sunken' },\n};\n\n/** File-type badge tile — an inline thumbnail for images, a tinted glyph otherwise. */\nfunction FileBadge({ file }: { file: FileNode }) {\n const kind = kindOf(file);\n const { Icon, iconClass, tileClass } = KIND_STYLE[kind];\n if (kind === 'image' && file.downloadUrl) {\n return (\n <img\n src={file.downloadUrl}\n alt=\"\"\n loading=\"lazy\"\n className=\"h-10 w-10 shrink-0 rounded-lg border border-border-subtle object-cover\"\n />\n );\n }\n return (\n <span\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${tileClass}`}\n aria-hidden=\"true\"\n >\n <Icon className={`h-5 w-5 ${iconClass}`} />\n </span>\n );\n}\n\nexport function AttachmentsPanel({ entityType, entityId }: AttachmentsPanelProps) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [uploading, setUploading] = useState(false);\n const [sharingId, setSharingId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useQuery<GetFilesResult>(GET_FILES_QUERY, {\n variables: {\n pagination: { limit: 25, offset: 0 },\n sort: { field: 'CREATED_AT', order: 'DESC' },\n },\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n });\n\n const [initiateUpload] = useMutation<{\n initiateUpload: { uploadUrl?: string | null; session: { id: string } };\n }>(INITIATE_UPLOAD_MUTATION);\n const [completeUpload] = useMutation(COMPLETE_UPLOAD_MUTATION);\n const [createShareLink] = useMutation<{\n createFileShareLink: { id: string; shareUrl?: string | null };\n }>(CREATE_SHARE_LINK_MUTATION);\n\n const files = useMemo(() => data?.getFiles.files ?? [], [data]);\n\n const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers change.\n event.target.value = '';\n if (!file) return;\n\n setUploading(true);\n try {\n const initiated = await initiateUpload({\n variables: {\n input: {\n fileName: file.name,\n extension: extensionOf(file.name),\n fileSize: file.size,\n mimeType: file.type || 'application/octet-stream',\n visibility: 'WORKSPACE',\n },\n },\n });\n\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('Upload could not be initialised.');\n }\n\n const putResponse = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type || 'application/octet-stream' },\n });\n if (!putResponse.ok) {\n throw new Error(`Storage rejected the upload (HTTP ${putResponse.status}).`);\n }\n\n await completeUpload({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n\n notify.success('File uploaded', `${file.name} is now available on this record.`);\n await refetch();\n } catch (error) {\n notify.error(\n 'Upload failed',\n error instanceof Error ? error.message : 'The file could not be uploaded.'\n );\n } finally {\n setUploading(false);\n }\n };\n\n const handleShare = async (fileId: string) => {\n setSharingId(fileId);\n try {\n const result = await createShareLink({\n variables: { input: { resourceId: fileId, role: 'VIEWER' } },\n });\n const url = result.data?.createFileShareLink.shareUrl;\n if (!url) {\n throw new Error('No share link was returned.');\n }\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(url);\n notify.success('Share link copied', 'A view-only link is on your clipboard.');\n } else {\n notify.info('Share link ready', url);\n }\n } catch (error) {\n notify.error(\n 'Could not create share link',\n error instanceof Error ? error.message : 'Please try again.'\n );\n } finally {\n setSharingId(null);\n }\n };\n\n const total = data?.getFiles.total ?? 0;\n const hasFiles = files.length > 0;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-2 space-y-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Paperclip className=\"h-4 w-4\" />\n Attachments\n {total ? (\n <span className=\"text-sm font-normal text-text-secondary\">\n {total} file{total === 1 ? '' : 's'}\n </span>\n ) : null}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(event) => void handleFileSelected(event)}\n aria-label={`Upload a file for ${entityType} ${entityId}`}\n />\n\n {loading && !hasFiles ? (\n <div className=\"space-y-2\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div\n key={i}\n className=\"h-14 rounded-lg bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : !hasFiles ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No files yet\"\n description=\"Upload photos, receipts, warranties or documents to keep them with this record.\"\n />\n ) : (\n <ul className=\"space-y-1.5\">\n {files.map((file) => {\n const isSharing = sharingId === file.id;\n return (\n <li\n key={file.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-2.5\"\n >\n <FileBadge file={file} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\" title={file.name}>\n {file.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {formatSize(Number(file.size))} · {relativeTime(file.createdAt)}\n </p>\n </div>\n {/* Desktop: inline actions. Mobile: kebab menu to avoid crowding. */}\n <div className=\"hidden items-center gap-1 sm:flex\">\n {file.downloadUrl ? (\n <a\n href={file.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Download ${file.name}`}\n title=\"Download\"\n >\n <Download className=\"h-4 w-4\" />\n </a>\n ) : null}\n <button\n type=\"button\"\n disabled={isSharing}\n onClick={() => void handleShare(file.id)}\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary disabled:opacity-60\"\n aria-label={`Copy share link for ${file.name}`}\n title=\"Copy share link\"\n >\n {isSharing ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Link2 className=\"h-4 w-4\" />\n )}\n </button>\n </div>\n <div className=\"sm:hidden\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Actions for ${file.name}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {file.downloadUrl ? (\n <DropdownMenuItem asChild>\n <a href={file.downloadUrl} target=\"_blank\" rel=\"noreferrer\">\n <Download className=\"mr-2 h-4 w-4\" />\n Download\n </a>\n </DropdownMenuItem>\n ) : null}\n <DropdownMenuItem\n disabled={isSharing}\n onSelect={(e) => {\n e.preventDefault();\n void handleShare(file.id);\n }}\n >\n <Link2 className=\"mr-2 h-4 w-4\" />\n Copy share link\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n\n {/* Prominent full-width primary upload CTA (matches the reference concept). */}\n <Button\n className=\"w-full\"\n disabled={uploading}\n onClick={() => fileInputRef.current?.click()}\n >\n {uploading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Upload className=\"mr-2 h-4 w-4\" />\n )}\n {uploading ? 'Uploading…' : 'Upload files'}\n </Button>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AttachmentsPanel.js","names":[],"sources":["../../../src/assethandler/components/AttachmentsPanel.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n IllustratedEmptyState,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Paperclip,\n Upload,\n Link2,\n FileText,\n FileSpreadsheet,\n FileArchive,\n FileCode2,\n FileVideo,\n FileAudio,\n Image as ImageIcon,\n File as FileIcon,\n Loader2,\n Download,\n MoreHorizontal,\n} from 'lucide-react';\nimport { notify } from '../utils/toast';\n\n/**\n * Attach and reference files for an AssetHandler entity, backed by the\n * federated wspace-files-svc (same wspace gateway/Apollo client). Fully real (no fabricated data) —\n * lists workspace files via `getFiles`, uploads new files through the\n * `initiateUpload` → presigned PUT → `completeUpload` flow, and mints public\n * share links via `createFileShareLink`. All entries are live data.\n *\n * `entityType`/`entityId` scope the panel to the record it is mounted on and\n * are used for context labelling; uploaded files land in the workspace file\n * store so they can be shared and reused.\n */\nexport interface AttachmentsPanelProps {\n entityType: string;\n entityId: string;\n}\n\ninterface FileNode {\n id: string;\n name: string;\n extension: string;\n mimeType: string;\n size: number;\n downloadUrl?: string | null;\n createdAt: string;\n}\n\ninterface GetFilesResult {\n getFiles: {\n files: FileNode[];\n total: number;\n hasMore: boolean;\n };\n}\n\nconst GET_FILES_QUERY = gql`\n query AssetHandlerGetFiles($pagination: PaginationInput, $sort: FileSortInput) {\n getFiles(pagination: $pagination, sort: $sort) {\n total\n hasMore\n files {\n id\n name\n extension\n mimeType\n size\n downloadUrl\n createdAt\n }\n }\n }\n`;\n\nconst INITIATE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n }\n }\n }\n`;\n\nconst CREATE_SHARE_LINK_MUTATION = gql`\n mutation AssetHandlerCreateFileShareLink($input: CreateShareGrantLinkInput!) {\n createFileShareLink(input: $input) {\n id\n shareUrl\n }\n }\n`;\n\nfunction formatSize(bytes: number): string {\n if (!bytes) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB'];\n const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);\n const value = bytes / Math.pow(1024, exponent);\n return `${value.toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n}\n\nfunction extensionOf(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n}\n\n/** Compact relative time (\"2 hrs ago\"), with the full timestamp on hover. */\nfunction relativeTime(iso: string): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diff = Date.now() - then;\n const sec = Math.round(diff / 1000);\n if (sec < 45) return 'just now';\n const min = Math.round(sec / 60);\n if (min < 60) return `${min} min ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr} hr${hr === 1 ? '' : 's'} ago`;\n const day = Math.round(hr / 24);\n if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`;\n return new Date(iso).toLocaleDateString();\n}\n\ntype FileKind =\n 'image' | 'pdf' | 'sheet' | 'doc' | 'archive' | 'code' | 'video' | 'audio' | 'other';\n\n/** Map a file's extension/mime to a recognisable kind for icon + tint. */\nfunction kindOf(file: FileNode): FileKind {\n const mime = (file.mimeType || '').toLowerCase();\n const ext = (file.extension || extensionOf(file.name)).toLowerCase();\n if (\n mime.startsWith('image/') ||\n ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'bmp'].includes(ext)\n )\n return 'image';\n if (mime.startsWith('video/') || ['mp4', 'mov', 'webm', 'avi', 'mkv'].includes(ext))\n return 'video';\n if (mime.startsWith('audio/') || ['mp3', 'wav', 'ogg', 'm4a', 'flac'].includes(ext))\n return 'audio';\n if (mime === 'application/pdf' || ext === 'pdf') return 'pdf';\n if (['xls', 'xlsx', 'csv', 'tsv', 'ods'].includes(ext)) return 'sheet';\n if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(ext)) return 'archive';\n if (\n [\n 'js',\n 'ts',\n 'tsx',\n 'jsx',\n 'json',\n 'xml',\n 'yml',\n 'yaml',\n 'html',\n 'css',\n 'sh',\n 'py',\n 'sql',\n ].includes(ext)\n )\n return 'code';\n if (['doc', 'docx', 'txt', 'md', 'rtf', 'odt', 'pages'].includes(ext)) return 'doc';\n return 'other';\n}\n\nconst KIND_STYLE: Record<\n FileKind,\n { Icon: typeof FileText; iconClass: string; tileClass: string }\n> = {\n image: {\n Icon: ImageIcon,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n pdf: {\n Icon: FileText,\n iconClass: 'text-status-error-text',\n tileClass: 'bg-status-error-bg-subtle',\n },\n sheet: {\n Icon: FileSpreadsheet,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n doc: {\n Icon: FileText,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n archive: {\n Icon: FileArchive,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n code: {\n Icon: FileCode2,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n video: {\n Icon: FileVideo,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n audio: {\n Icon: FileAudio,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n other: { Icon: FileIcon, iconClass: 'text-text-secondary', tileClass: 'bg-bg-sunken' },\n};\n\n/** File-type badge tile — an inline thumbnail for images, a tinted glyph otherwise. */\nfunction FileBadge({ file }: { file: FileNode }) {\n const kind = kindOf(file);\n const { Icon, iconClass, tileClass } = KIND_STYLE[kind];\n if (kind === 'image' && file.downloadUrl) {\n return (\n <img\n src={file.downloadUrl}\n alt=\"\"\n loading=\"lazy\"\n className=\"h-10 w-10 shrink-0 rounded-lg border border-border-subtle object-cover\"\n />\n );\n }\n return (\n <span\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${tileClass}`}\n aria-hidden=\"true\"\n >\n <Icon className={`h-5 w-5 ${iconClass}`} />\n </span>\n );\n}\n\nexport function AttachmentsPanel({ entityType, entityId }: AttachmentsPanelProps) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [uploading, setUploading] = useState(false);\n const [sharingId, setSharingId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useQuery<GetFilesResult>(GET_FILES_QUERY, {\n variables: {\n pagination: { limit: 25, offset: 0 },\n sort: { field: 'CREATED_AT', order: 'DESC' },\n },\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n });\n\n const [initiateUpload] = useMutation<{\n initiateUpload: { uploadUrl?: string | null; session: { id: string } };\n }>(INITIATE_UPLOAD_MUTATION);\n const [completeUpload] = useMutation(COMPLETE_UPLOAD_MUTATION);\n const [createShareLink] = useMutation<{\n createFileShareLink: { id: string; shareUrl?: string | null };\n }>(CREATE_SHARE_LINK_MUTATION);\n\n const files = useMemo(() => data?.getFiles.files ?? [], [data]);\n\n const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers change.\n event.target.value = '';\n if (!file) return;\n\n setUploading(true);\n try {\n const initiated = await initiateUpload({\n variables: {\n input: {\n fileName: file.name,\n extension: extensionOf(file.name),\n fileSize: file.size,\n mimeType: file.type || 'application/octet-stream',\n visibility: 'WORKSPACE',\n },\n },\n });\n\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('Upload could not be initialised.');\n }\n\n const putResponse = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type || 'application/octet-stream' },\n });\n if (!putResponse.ok) {\n throw new Error(`Storage rejected the upload (HTTP ${putResponse.status}).`);\n }\n\n await completeUpload({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n\n notify.success('File uploaded', `${file.name} is now available on this record.`);\n await refetch();\n } catch (error) {\n notify.error(\n 'Upload failed',\n error instanceof Error ? error.message : 'The file could not be uploaded.'\n );\n } finally {\n setUploading(false);\n }\n };\n\n const handleShare = async (fileId: string) => {\n setSharingId(fileId);\n try {\n const result = await createShareLink({\n variables: { input: { resourceId: fileId, role: 'VIEWER' } },\n });\n const url = result.data?.createFileShareLink.shareUrl;\n if (!url) {\n throw new Error('No share link was returned.');\n }\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(url);\n notify.success('Share link copied', 'A view-only link is on your clipboard.');\n } else {\n notify.info('Share link ready', url);\n }\n } catch (error) {\n notify.error(\n 'Could not create share link',\n error instanceof Error ? error.message : 'Please try again.'\n );\n } finally {\n setSharingId(null);\n }\n };\n\n const total = data?.getFiles.total ?? 0;\n const hasFiles = files.length > 0;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-2 space-y-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Paperclip className=\"h-4 w-4\" />\n Attachments\n {total ? (\n <span className=\"text-sm font-normal text-text-secondary\">\n {total} file{total === 1 ? '' : 's'}\n </span>\n ) : null}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(event) => void handleFileSelected(event)}\n aria-label={`Upload a file for ${entityType} ${entityId}`}\n />\n\n {loading && !hasFiles ? (\n <div className=\"space-y-2\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div\n key={i}\n className=\"h-14 rounded-lg bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : !hasFiles ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No files yet\"\n description=\"Upload photos, receipts, warranties or documents to keep them with this record.\"\n />\n ) : (\n <ul className=\"space-y-1.5\">\n {files.map((file) => {\n const isSharing = sharingId === file.id;\n return (\n <li\n key={file.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-2.5\"\n >\n <FileBadge file={file} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\" title={file.name}>\n {file.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {formatSize(Number(file.size))} · {relativeTime(file.createdAt)}\n </p>\n </div>\n {/* Desktop: inline actions. Mobile: kebab menu to avoid crowding. */}\n <div className=\"hidden items-center gap-1 sm:flex\">\n {file.downloadUrl ? (\n <a\n href={file.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Download ${file.name}`}\n title=\"Download\"\n >\n <Download className=\"h-4 w-4\" />\n </a>\n ) : null}\n <button\n type=\"button\"\n disabled={isSharing}\n onClick={() => void handleShare(file.id)}\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary disabled:opacity-60\"\n aria-label={`Copy share link for ${file.name}`}\n title=\"Copy share link\"\n >\n {isSharing ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Link2 className=\"h-4 w-4\" />\n )}\n </button>\n </div>\n <div className=\"sm:hidden\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Actions for ${file.name}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {file.downloadUrl ? (\n <DropdownMenuItem asChild>\n <a href={file.downloadUrl} target=\"_blank\" rel=\"noreferrer\">\n <Download className=\"mr-2 h-4 w-4\" />\n Download\n </a>\n </DropdownMenuItem>\n ) : null}\n <DropdownMenuItem\n disabled={isSharing}\n onSelect={(e) => {\n e.preventDefault();\n void handleShare(file.id);\n }}\n >\n <Link2 className=\"mr-2 h-4 w-4\" />\n Copy share link\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n\n {/* Prominent full-width primary upload CTA (matches the reference concept). */}\n <Button\n className=\"w-full\"\n disabled={uploading}\n onClick={() => fileInputRef.current?.click()}\n >\n {uploading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Upload className=\"mr-2 h-4 w-4\" />\n )}\n {uploading ? 'Uploading…' : 'Upload files'}\n </Button>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,47 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
|
|
3
|
+
declare const ESG_CATEGORIES: readonly ["environment", "social", "governance"];
|
|
4
|
+
declare const ESG_TRENDS: readonly ["improving", "stable", "declining"];
|
|
5
|
+
interface MetricFormValues {
|
|
6
|
+
metricName: string;
|
|
7
|
+
category: (typeof ESG_CATEGORIES)[number];
|
|
8
|
+
unit: string;
|
|
9
|
+
value: number;
|
|
10
|
+
target: number | null;
|
|
11
|
+
trend: (typeof ESG_TRENDS)[number];
|
|
12
|
+
periodStart: string;
|
|
13
|
+
periodEnd: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildMetricMutationVariables(metricId: string | null, form: MetricFormValues): {
|
|
16
|
+
kind: "update";
|
|
17
|
+
variables: {
|
|
18
|
+
id: string;
|
|
19
|
+
input: {
|
|
20
|
+
metricName: string;
|
|
21
|
+
category: "environment" | "social" | "governance";
|
|
22
|
+
unit: string;
|
|
23
|
+
value: number;
|
|
24
|
+
target: number | null;
|
|
25
|
+
trend: "improving" | "stable" | "declining";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
kind: "create";
|
|
30
|
+
variables: {
|
|
31
|
+
input: {
|
|
32
|
+
metricKey: string;
|
|
33
|
+
periodStart: string;
|
|
34
|
+
periodEnd: string;
|
|
35
|
+
metricName: string;
|
|
36
|
+
category: "environment" | "social" | "governance";
|
|
37
|
+
unit: string;
|
|
38
|
+
value: number;
|
|
39
|
+
target: number | null;
|
|
40
|
+
trend: "improving" | "stable" | "declining";
|
|
41
|
+
};
|
|
42
|
+
id?: undefined;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
3
45
|
export declare function EsgDashboardPage(): React.JSX.Element;
|
|
46
|
+
export {};
|
|
4
47
|
//# sourceMappingURL=EsgDashboardPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EsgDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"EsgDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoG/B,QAAA,MAAM,cAAc,kDAAmD,CAAC;AACxE,QAAA,MAAM,UAAU,+CAAgD,CAAC;AA6BjE,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsB3F;AAwOD,wBAAgB,gBAAgB,sBAyT/B"}
|
|
@@ -9,13 +9,13 @@ import { ChartCard as l } from "../components/ChartCard.js";
|
|
|
9
9
|
import { SectionHeader as u } from "../components/SectionHeader.js";
|
|
10
10
|
import "../components/index.js";
|
|
11
11
|
import { useMemo as d, useState as f } from "react";
|
|
12
|
-
import {
|
|
13
|
-
import { gql as
|
|
14
|
-
import { useMutation as
|
|
15
|
-
import { Fragment as
|
|
16
|
-
import { Button as
|
|
12
|
+
import { Archive as p, Leaf as m, Minus as h, Pencil as g, Plus as _, TrendingDown as v, TrendingUp as y } from "lucide-react";
|
|
13
|
+
import { gql as b } from "@apollo/client";
|
|
14
|
+
import { useMutation as x, useQuery as S } from "@apollo/client/react";
|
|
15
|
+
import { Fragment as C, jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
16
|
+
import { Button as E, GlassCard as D, IllustratedEmptyState as O, Input as k, Label as A, PagePurpose as j, ResponsiveDialog as M, ResponsiveDialogContent as N, ResponsiveDialogDescription as P, ResponsiveDialogFooter as F, ResponsiveDialogHeader as I, ResponsiveDialogTitle as L, Select as R, SelectContent as z, SelectItem as B, SelectTrigger as V, SelectValue as H } from "@burdenoff/fe-libs/ui";
|
|
17
17
|
//#region src/assethandler/pages/EsgDashboardPage.tsx
|
|
18
|
-
var
|
|
18
|
+
var U = b`
|
|
19
19
|
query AssetHandlerEsgMetrics {
|
|
20
20
|
esgMetrics {
|
|
21
21
|
id
|
|
@@ -30,162 +30,202 @@ var V = v`
|
|
|
30
30
|
periodEnd
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
`,
|
|
34
|
-
mutation
|
|
35
|
-
|
|
33
|
+
`, W = b`
|
|
34
|
+
mutation AssetHandlerCreateEsgMetric($input: CreateEsgMetricInput!) {
|
|
35
|
+
createEsgMetric(input: $input) {
|
|
36
36
|
id
|
|
37
37
|
metricName
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
`,
|
|
40
|
+
`, G = b`
|
|
41
|
+
mutation AssetHandlerUpdateEsgMetric($id: ID!, $input: UpdateEsgMetricInput!) {
|
|
42
|
+
updateEsgMetric(id: $id, input: $input) {
|
|
43
|
+
id
|
|
44
|
+
metricName
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`, K = b`
|
|
48
|
+
mutation AssetHandlerArchiveEsgMetric($id: ID!) {
|
|
49
|
+
archiveEsgMetric(id: $id)
|
|
50
|
+
}
|
|
51
|
+
`, q = [
|
|
41
52
|
"environment",
|
|
42
53
|
"social",
|
|
43
54
|
"governance"
|
|
44
|
-
],
|
|
55
|
+
], J = [
|
|
45
56
|
"improving",
|
|
46
57
|
"stable",
|
|
47
58
|
"declining"
|
|
48
|
-
],
|
|
59
|
+
], Y = {
|
|
49
60
|
improving: {
|
|
50
61
|
label: "Improving",
|
|
51
|
-
icon:
|
|
62
|
+
icon: y,
|
|
52
63
|
tone: "positive",
|
|
53
64
|
dir: "up"
|
|
54
65
|
},
|
|
55
66
|
declining: {
|
|
56
67
|
label: "Declining",
|
|
57
|
-
icon:
|
|
68
|
+
icon: v,
|
|
58
69
|
tone: "negative",
|
|
59
70
|
dir: "down"
|
|
60
71
|
},
|
|
61
72
|
stable: {
|
|
62
73
|
label: "Stable",
|
|
63
|
-
icon:
|
|
74
|
+
icon: h,
|
|
64
75
|
tone: "neutral",
|
|
65
76
|
dir: "neutral"
|
|
66
77
|
}
|
|
67
|
-
},
|
|
78
|
+
}, X = {
|
|
68
79
|
positive: "bg-status-success-bg-subtle text-status-success-text",
|
|
69
80
|
negative: "bg-status-error-bg-subtle text-status-error-text",
|
|
70
81
|
neutral: "bg-bg-sunken text-text-secondary"
|
|
71
|
-
},
|
|
72
|
-
function
|
|
82
|
+
}, Z = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
83
|
+
function ee(e, t) {
|
|
84
|
+
let n = {
|
|
85
|
+
metricName: t.metricName.trim(),
|
|
86
|
+
category: t.category,
|
|
87
|
+
unit: t.unit.trim(),
|
|
88
|
+
value: t.value,
|
|
89
|
+
target: t.target,
|
|
90
|
+
trend: t.trend
|
|
91
|
+
};
|
|
92
|
+
return e ? {
|
|
93
|
+
kind: "update",
|
|
94
|
+
variables: {
|
|
95
|
+
id: e,
|
|
96
|
+
input: n
|
|
97
|
+
}
|
|
98
|
+
} : {
|
|
99
|
+
kind: "create",
|
|
100
|
+
variables: { input: {
|
|
101
|
+
...n,
|
|
102
|
+
metricKey: Z(t.metricName),
|
|
103
|
+
periodStart: new Date(t.periodStart).toISOString(),
|
|
104
|
+
periodEnd: new Date(t.periodEnd).toISOString()
|
|
105
|
+
} }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function Q() {
|
|
73
109
|
let e = /* @__PURE__ */ new Date();
|
|
74
110
|
return new Date(e.getFullYear(), e.getMonth(), 1).toISOString().slice(0, 10);
|
|
75
111
|
}
|
|
76
|
-
function
|
|
112
|
+
function te() {
|
|
77
113
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
78
114
|
}
|
|
79
|
-
function
|
|
80
|
-
let t =
|
|
81
|
-
return /* @__PURE__ */
|
|
82
|
-
className: n("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
|
83
|
-
children: [/* @__PURE__ */
|
|
115
|
+
function ne({ trend: e }) {
|
|
116
|
+
let t = Y[e ?? "stable"] ?? Y.stable, r = t.icon;
|
|
117
|
+
return /* @__PURE__ */ T("span", {
|
|
118
|
+
className: n("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", X[t.tone]),
|
|
119
|
+
children: [/* @__PURE__ */ w(r, { className: "h-3 w-3" }), t.label]
|
|
84
120
|
});
|
|
85
121
|
}
|
|
86
|
-
function
|
|
87
|
-
let [
|
|
122
|
+
function re({ open: e, metric: t, onOpenChange: n, onSaved: i }) {
|
|
123
|
+
let [a, o] = f(t?.metricName ?? ""), [s, c] = f(t?.category ?? "environment"), [l, u] = f(t?.unit ?? ""), [d, p] = f(t ? String(t.value) : ""), [m, h] = f(t?.target == null ? "" : String(t.target)), [g, _] = f(t?.trend ?? "stable"), [v, y] = f(t?.periodStart.slice(0, 10) ?? Q()), [b, S] = f(t?.periodEnd.slice(0, 10) ?? te()), C = { onError: (e) => r.error("Could not save metric", e.message) }, [D, { loading: O }] = x(W, {
|
|
124
|
+
...C,
|
|
88
125
|
onCompleted: (e) => {
|
|
89
|
-
r.success("Metric saved", `${e.
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
r.success("Metric saved", `${e.createEsgMetric.metricName} was recorded.`), n(!1), i();
|
|
127
|
+
}
|
|
128
|
+
}), [j, { loading: U }] = x(G, {
|
|
129
|
+
...C,
|
|
130
|
+
onCompleted: (e) => {
|
|
131
|
+
r.success("Metric updated", `${e.updateEsgMetric.metricName} was updated.`), n(!1), i();
|
|
132
|
+
}
|
|
133
|
+
}), K = O || U;
|
|
134
|
+
return /* @__PURE__ */ w(M, {
|
|
94
135
|
open: e,
|
|
95
|
-
onOpenChange:
|
|
96
|
-
children: /* @__PURE__ */
|
|
136
|
+
onOpenChange: n,
|
|
137
|
+
children: /* @__PURE__ */ T(N, {
|
|
97
138
|
className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl",
|
|
98
|
-
children: [/* @__PURE__ */
|
|
139
|
+
children: [/* @__PURE__ */ T(I, { children: [/* @__PURE__ */ w(L, { children: t ? "Edit Metric" : "Save Metric" }), /* @__PURE__ */ w(P, { children: t ? "Update this metric’s values. Its source identity and reporting period stay unchanged." : "Record an ESG metric to track progress toward your sustainability targets." })] }), /* @__PURE__ */ T("form", {
|
|
99
140
|
className: "space-y-3",
|
|
100
141
|
onSubmit: (e) => {
|
|
101
142
|
e.preventDefault();
|
|
102
|
-
let
|
|
103
|
-
if (!
|
|
143
|
+
let n = Number(d);
|
|
144
|
+
if (!a.trim() || !l.trim()) {
|
|
104
145
|
r.warning("Missing details", "Metric name and unit are required.");
|
|
105
146
|
return;
|
|
106
147
|
}
|
|
107
|
-
if (!Number.isFinite(
|
|
148
|
+
if (!Number.isFinite(n) || d.trim() === "") {
|
|
108
149
|
r.warning("Invalid value", "Enter a numeric metric value.");
|
|
109
150
|
return;
|
|
110
151
|
}
|
|
111
|
-
if (!
|
|
152
|
+
if (!v || !b) {
|
|
112
153
|
r.warning("Missing period", "Period start and end are required.");
|
|
113
154
|
return;
|
|
114
155
|
}
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} } });
|
|
156
|
+
let i = m.trim() === "" ? null : Number(m), o = ee(t?.id ?? null, {
|
|
157
|
+
metricName: a,
|
|
158
|
+
category: s,
|
|
159
|
+
unit: l,
|
|
160
|
+
value: n,
|
|
161
|
+
target: i != null && Number.isFinite(i) ? i : null,
|
|
162
|
+
trend: g,
|
|
163
|
+
periodStart: v,
|
|
164
|
+
periodEnd: b
|
|
165
|
+
});
|
|
166
|
+
o.kind === "update" ? j(o) : D(o);
|
|
127
167
|
},
|
|
128
168
|
children: [
|
|
129
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ T("div", {
|
|
130
170
|
className: "grid gap-3 md:grid-cols-3",
|
|
131
171
|
children: [
|
|
132
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ T("div", {
|
|
133
173
|
className: "space-y-2",
|
|
134
|
-
children: [/* @__PURE__ */
|
|
174
|
+
children: [/* @__PURE__ */ w(A, { children: "Metric Name *" }), /* @__PURE__ */ w(k, {
|
|
135
175
|
placeholder: "e.g., Scope 2 carbon emissions",
|
|
136
|
-
value:
|
|
137
|
-
onChange: (e) =>
|
|
176
|
+
value: a,
|
|
177
|
+
onChange: (e) => o(e.target.value)
|
|
138
178
|
})]
|
|
139
179
|
}),
|
|
140
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ T("div", {
|
|
141
181
|
className: "space-y-2",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
143
|
-
value:
|
|
144
|
-
onValueChange: (e) =>
|
|
145
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ w(A, { children: "Category" }), /* @__PURE__ */ T(R, {
|
|
183
|
+
value: s,
|
|
184
|
+
onValueChange: (e) => c(e),
|
|
185
|
+
children: [/* @__PURE__ */ w(V, { children: /* @__PURE__ */ w(H, {}) }), /* @__PURE__ */ w(z, { children: q.map((e) => /* @__PURE__ */ w(B, {
|
|
146
186
|
value: e,
|
|
147
187
|
className: "capitalize",
|
|
148
188
|
children: e
|
|
149
189
|
}, e)) })]
|
|
150
190
|
})]
|
|
151
191
|
}),
|
|
152
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ T("div", {
|
|
153
193
|
className: "space-y-2",
|
|
154
|
-
children: [/* @__PURE__ */
|
|
194
|
+
children: [/* @__PURE__ */ w(A, { children: "Unit *" }), /* @__PURE__ */ w(k, {
|
|
155
195
|
placeholder: "e.g., tCO2e",
|
|
156
|
-
value:
|
|
157
|
-
onChange: (e) =>
|
|
196
|
+
value: l,
|
|
197
|
+
onChange: (e) => u(e.target.value)
|
|
158
198
|
})]
|
|
159
199
|
})
|
|
160
200
|
]
|
|
161
201
|
}),
|
|
162
|
-
/* @__PURE__ */
|
|
202
|
+
/* @__PURE__ */ T("div", {
|
|
163
203
|
className: "grid gap-3 md:grid-cols-3",
|
|
164
204
|
children: [
|
|
165
|
-
/* @__PURE__ */
|
|
205
|
+
/* @__PURE__ */ T("div", {
|
|
166
206
|
className: "space-y-2",
|
|
167
|
-
children: [/* @__PURE__ */
|
|
207
|
+
children: [/* @__PURE__ */ w(A, { children: "Value *" }), /* @__PURE__ */ w(k, {
|
|
168
208
|
type: "number",
|
|
169
209
|
step: "any",
|
|
170
|
-
value:
|
|
171
|
-
onChange: (e) =>
|
|
210
|
+
value: d,
|
|
211
|
+
onChange: (e) => p(e.target.value)
|
|
172
212
|
})]
|
|
173
213
|
}),
|
|
174
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ T("div", {
|
|
175
215
|
className: "space-y-2",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
216
|
+
children: [/* @__PURE__ */ w(A, { children: "Target" }), /* @__PURE__ */ w(k, {
|
|
177
217
|
type: "number",
|
|
178
218
|
step: "any",
|
|
179
|
-
value:
|
|
180
|
-
onChange: (e) =>
|
|
219
|
+
value: m,
|
|
220
|
+
onChange: (e) => h(e.target.value)
|
|
181
221
|
})]
|
|
182
222
|
}),
|
|
183
|
-
/* @__PURE__ */
|
|
223
|
+
/* @__PURE__ */ T("div", {
|
|
184
224
|
className: "space-y-2",
|
|
185
|
-
children: [/* @__PURE__ */
|
|
186
|
-
value:
|
|
187
|
-
onValueChange: (e) =>
|
|
188
|
-
children: [/* @__PURE__ */
|
|
225
|
+
children: [/* @__PURE__ */ w(A, { children: "Trend" }), /* @__PURE__ */ T(R, {
|
|
226
|
+
value: g,
|
|
227
|
+
onValueChange: (e) => _(e),
|
|
228
|
+
children: [/* @__PURE__ */ w(V, { children: /* @__PURE__ */ w(H, {}) }), /* @__PURE__ */ w(z, { children: J.map((e) => /* @__PURE__ */ w(B, {
|
|
189
229
|
value: e,
|
|
190
230
|
className: "capitalize",
|
|
191
231
|
children: e
|
|
@@ -194,111 +234,146 @@ function Z({ open: e, onOpenChange: t, onSaved: n }) {
|
|
|
194
234
|
})
|
|
195
235
|
]
|
|
196
236
|
}),
|
|
197
|
-
/* @__PURE__ */
|
|
237
|
+
/* @__PURE__ */ T("div", {
|
|
198
238
|
className: "grid gap-3 md:grid-cols-2",
|
|
199
|
-
children: [/* @__PURE__ */
|
|
239
|
+
children: [/* @__PURE__ */ T("div", {
|
|
200
240
|
className: "space-y-2",
|
|
201
|
-
children: [/* @__PURE__ */
|
|
241
|
+
children: [/* @__PURE__ */ w(A, { children: "Period Start *" }), /* @__PURE__ */ w(k, {
|
|
202
242
|
type: "date",
|
|
203
|
-
value:
|
|
204
|
-
onChange: (e) =>
|
|
243
|
+
value: v,
|
|
244
|
+
onChange: (e) => y(e.target.value),
|
|
245
|
+
disabled: t != null
|
|
205
246
|
})]
|
|
206
|
-
}), /* @__PURE__ */
|
|
247
|
+
}), /* @__PURE__ */ T("div", {
|
|
207
248
|
className: "space-y-2",
|
|
208
|
-
children: [/* @__PURE__ */
|
|
249
|
+
children: [/* @__PURE__ */ w(A, { children: "Period End *" }), /* @__PURE__ */ w(k, {
|
|
209
250
|
type: "date",
|
|
210
251
|
value: b,
|
|
211
|
-
onChange: (e) =>
|
|
252
|
+
onChange: (e) => S(e.target.value),
|
|
253
|
+
disabled: t != null
|
|
212
254
|
})]
|
|
213
255
|
})]
|
|
214
256
|
}),
|
|
215
|
-
/* @__PURE__ */
|
|
257
|
+
/* @__PURE__ */ T(F, { children: [/* @__PURE__ */ w(E, {
|
|
216
258
|
type: "button",
|
|
217
259
|
variant: "outline",
|
|
218
|
-
onClick: () =>
|
|
219
|
-
disabled:
|
|
260
|
+
onClick: () => n(!1),
|
|
261
|
+
disabled: K,
|
|
220
262
|
children: "Cancel"
|
|
221
|
-
}), /* @__PURE__ */
|
|
263
|
+
}), /* @__PURE__ */ w(E, {
|
|
222
264
|
type: "submit",
|
|
223
|
-
disabled:
|
|
224
|
-
children:
|
|
265
|
+
disabled: K,
|
|
266
|
+
children: K ? "Saving…" : t ? "Update Metric" : "Save Metric"
|
|
225
267
|
})] })
|
|
226
268
|
]
|
|
227
269
|
})]
|
|
228
270
|
})
|
|
229
271
|
});
|
|
230
272
|
}
|
|
231
|
-
function
|
|
232
|
-
let [
|
|
233
|
-
|
|
273
|
+
function $() {
|
|
274
|
+
let [h, v] = f(!1), [y, b] = f(null), [k, A] = f(null), [R, z] = f("ALL"), { data: B, loading: V, refetch: H } = S(U, { fetchPolicy: "cache-and-network" }), W = d(() => B?.esgMetrics ?? [], [B?.esgMetrics]), [G, { loading: J }] = x(K, {
|
|
275
|
+
onCompleted: () => {
|
|
276
|
+
r.success("Metric archived", "The metric was removed from the active ESG dashboard."), A(null), H();
|
|
277
|
+
},
|
|
278
|
+
onError: (e) => r.error("Could not archive metric", e.message)
|
|
279
|
+
}), X = d(() => W.slice(0, 4), [W]), Z = d(() => R === "ALL" ? W : W.filter((e) => e.category === R), [W, R]), ee = d(() => {
|
|
280
|
+
let e = W.reduce((e, t) => (e[t.category] = (e[t.category] ?? 0) + 1, e), {});
|
|
234
281
|
return Object.entries(e).map(([e, n], r) => ({
|
|
235
282
|
name: e.charAt(0).toUpperCase() + e.slice(1),
|
|
236
283
|
value: n,
|
|
237
284
|
color: t(r)
|
|
238
285
|
}));
|
|
239
|
-
}, [
|
|
286
|
+
}, [W]), Q = d(() => W.filter((e) => e.target != null).slice(0, 8).map((e) => ({
|
|
240
287
|
name: e.metricName,
|
|
241
288
|
value: e.value,
|
|
242
289
|
target: e.target
|
|
243
|
-
})), [
|
|
244
|
-
let e = { ALL:
|
|
245
|
-
for (let t of
|
|
290
|
+
})), [W]), te = d(() => {
|
|
291
|
+
let e = { ALL: W.length };
|
|
292
|
+
for (let t of W) e[t.category] = (e[t.category] ?? 0) + 1;
|
|
246
293
|
return e;
|
|
247
|
-
}, [
|
|
294
|
+
}, [W]), $ = s(Z, {
|
|
248
295
|
pageSize: 8,
|
|
249
|
-
resetKey:
|
|
296
|
+
resetKey: R
|
|
250
297
|
});
|
|
251
|
-
return /* @__PURE__ */
|
|
298
|
+
return /* @__PURE__ */ T("div", {
|
|
252
299
|
className: "space-y-5 p-4 sm:p-6",
|
|
253
300
|
children: [
|
|
254
|
-
/* @__PURE__ */
|
|
301
|
+
/* @__PURE__ */ w(u, {
|
|
255
302
|
hero: !0,
|
|
256
|
-
icon:
|
|
303
|
+
icon: m,
|
|
257
304
|
title: "ESG Dashboard",
|
|
258
305
|
description: "Measure the environmental and governance impact of your estate against targets.",
|
|
259
|
-
actions: /* @__PURE__ */
|
|
306
|
+
actions: /* @__PURE__ */ T(E, {
|
|
260
307
|
className: "w-full sm:w-auto",
|
|
261
|
-
onClick: () =>
|
|
262
|
-
|
|
308
|
+
onClick: () => {
|
|
309
|
+
b(null), v(!0);
|
|
310
|
+
},
|
|
311
|
+
children: [/* @__PURE__ */ w(_, { className: "mr-2 h-4 w-4" }), "Save Metric"]
|
|
263
312
|
})
|
|
264
313
|
}),
|
|
265
|
-
/* @__PURE__ */
|
|
266
|
-
/* @__PURE__ */
|
|
267
|
-
open:
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
/* @__PURE__ */ w(j, { children: "Track energy use, carbon emissions and responsible disposal against targets — and produce the evidence ESG reporting and audits require." }),
|
|
315
|
+
h ? /* @__PURE__ */ w(re, {
|
|
316
|
+
open: !0,
|
|
317
|
+
metric: y,
|
|
318
|
+
onOpenChange: (e) => {
|
|
319
|
+
v(e), e || b(null);
|
|
320
|
+
},
|
|
321
|
+
onSaved: () => void H()
|
|
322
|
+
}) : null,
|
|
323
|
+
/* @__PURE__ */ w(M, {
|
|
324
|
+
open: k != null,
|
|
325
|
+
onOpenChange: (e) => {
|
|
326
|
+
e || A(null);
|
|
327
|
+
},
|
|
328
|
+
children: /* @__PURE__ */ T(N, { children: [/* @__PURE__ */ T(I, { children: [/* @__PURE__ */ w(L, { children: "Archive metric" }), /* @__PURE__ */ T(P, { children: [
|
|
329
|
+
"Archive ",
|
|
330
|
+
k?.metricName ?? "this metric",
|
|
331
|
+
"? It will leave the active ESG dashboard. Archiving does not refund monthly usage."
|
|
332
|
+
] })] }), /* @__PURE__ */ T(F, { children: [/* @__PURE__ */ w(E, {
|
|
333
|
+
variant: "outline",
|
|
334
|
+
onClick: () => A(null),
|
|
335
|
+
disabled: J,
|
|
336
|
+
children: "Cancel"
|
|
337
|
+
}), /* @__PURE__ */ w(E, {
|
|
338
|
+
variant: "destructive",
|
|
339
|
+
disabled: J || k == null,
|
|
340
|
+
onClick: () => {
|
|
341
|
+
k && G({ variables: { id: k.id } });
|
|
342
|
+
},
|
|
343
|
+
children: J ? "Archiving…" : "Archive Metric"
|
|
344
|
+
})] })] })
|
|
270
345
|
}),
|
|
271
|
-
|
|
346
|
+
V && W.length === 0 ? /* @__PURE__ */ w("div", {
|
|
272
347
|
className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
273
|
-
children: Array.from({ length: 4 }).map((e, t) => /* @__PURE__ */
|
|
348
|
+
children: Array.from({ length: 4 }).map((e, t) => /* @__PURE__ */ w("div", { className: "h-28 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer" }, t))
|
|
274
349
|
}) : null,
|
|
275
|
-
!
|
|
350
|
+
!V && W.length === 0 ? /* @__PURE__ */ w(O, {
|
|
276
351
|
illustration: "empty-data",
|
|
277
352
|
title: "No ESG metrics recorded yet",
|
|
278
353
|
description: "Save a metric to start tracking sustainability progress against your targets."
|
|
279
354
|
}) : null,
|
|
280
|
-
|
|
355
|
+
X.length > 0 ? /* @__PURE__ */ w("div", {
|
|
281
356
|
className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
282
|
-
children:
|
|
283
|
-
let n =
|
|
284
|
-
return /* @__PURE__ */
|
|
357
|
+
children: X.map((t) => {
|
|
358
|
+
let n = Y[t.trend ?? "stable"] ?? Y.stable;
|
|
359
|
+
return /* @__PURE__ */ w(c, {
|
|
285
360
|
label: `${t.metricName} (${t.unit})`,
|
|
286
361
|
value: t.value,
|
|
287
362
|
format: e,
|
|
288
363
|
delta: n.label,
|
|
289
364
|
deltaDirection: n.dir,
|
|
290
365
|
deltaTone: n.tone,
|
|
291
|
-
icon:
|
|
366
|
+
icon: m
|
|
292
367
|
}, t.id);
|
|
293
368
|
})
|
|
294
369
|
}) : null,
|
|
295
|
-
|
|
370
|
+
W.length > 0 ? /* @__PURE__ */ T("div", {
|
|
296
371
|
className: "grid gap-4 lg:grid-cols-2",
|
|
297
|
-
children: [/* @__PURE__ */
|
|
372
|
+
children: [/* @__PURE__ */ w(l, {
|
|
298
373
|
title: "Value vs. target",
|
|
299
374
|
subtitle: "Metrics measured against their targets",
|
|
300
|
-
children:
|
|
301
|
-
data:
|
|
375
|
+
children: Q.length > 0 ? /* @__PURE__ */ w(i, {
|
|
376
|
+
data: Q,
|
|
302
377
|
xKey: "name",
|
|
303
378
|
series: [{
|
|
304
379
|
key: "value",
|
|
@@ -311,86 +386,109 @@ function Q() {
|
|
|
311
386
|
height: 240,
|
|
312
387
|
categoryWidth: 150,
|
|
313
388
|
ariaLabel: "Metric value versus target"
|
|
314
|
-
}) : /* @__PURE__ */
|
|
389
|
+
}) : /* @__PURE__ */ w("div", {
|
|
315
390
|
className: "flex h-[240px] items-center justify-center text-sm text-text-secondary",
|
|
316
391
|
children: "Set a target on a metric to compare value against target."
|
|
317
392
|
})
|
|
318
|
-
}), /* @__PURE__ */
|
|
393
|
+
}), /* @__PURE__ */ w(l, {
|
|
319
394
|
title: "By category",
|
|
320
395
|
subtitle: "Metrics across E / S / G",
|
|
321
|
-
children: /* @__PURE__ */
|
|
322
|
-
data:
|
|
396
|
+
children: /* @__PURE__ */ w(a, {
|
|
397
|
+
data: ee,
|
|
323
398
|
height: 240,
|
|
324
399
|
centerLabel: "Metrics",
|
|
325
|
-
centerValue:
|
|
400
|
+
centerValue: W.length,
|
|
326
401
|
ariaLabel: "Metrics by category"
|
|
327
402
|
})
|
|
328
403
|
})]
|
|
329
404
|
}) : null,
|
|
330
|
-
|
|
331
|
-
/* @__PURE__ */
|
|
405
|
+
W.length > 0 ? /* @__PURE__ */ T(C, { children: [
|
|
406
|
+
/* @__PURE__ */ w("div", {
|
|
332
407
|
className: "flex flex-wrap items-center gap-2",
|
|
333
|
-
children: ["ALL", ...
|
|
408
|
+
children: ["ALL", ...q].map((e) => /* @__PURE__ */ T("button", {
|
|
334
409
|
type: "button",
|
|
335
|
-
onClick: () =>
|
|
336
|
-
className: n("rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors",
|
|
337
|
-
children: [e === "ALL" ? "All" : e, /* @__PURE__ */
|
|
410
|
+
onClick: () => z(e),
|
|
411
|
+
className: n("rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors", R === e ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
|
|
412
|
+
children: [e === "ALL" ? "All" : e, /* @__PURE__ */ w("span", {
|
|
338
413
|
className: "ml-1.5 text-xs opacity-70 tabular-nums",
|
|
339
|
-
children:
|
|
414
|
+
children: te[e] ?? 0
|
|
340
415
|
})]
|
|
341
416
|
}, e))
|
|
342
417
|
}),
|
|
343
|
-
/* @__PURE__ */
|
|
418
|
+
/* @__PURE__ */ w("div", {
|
|
344
419
|
className: "grid gap-4 sm:grid-cols-2",
|
|
345
|
-
children:
|
|
420
|
+
children: $.pageItems.map((t) => {
|
|
346
421
|
let n = t.target && t.target > 0 ? Math.min(100, Math.round(t.value / t.target * 100)) : null;
|
|
347
|
-
return /* @__PURE__ */
|
|
422
|
+
return /* @__PURE__ */ w(D, {
|
|
348
423
|
treatment: "glass",
|
|
349
424
|
className: "border-border-seam",
|
|
350
|
-
children: /* @__PURE__ */
|
|
425
|
+
children: /* @__PURE__ */ T("div", {
|
|
351
426
|
className: "space-y-3 p-4",
|
|
352
427
|
children: [
|
|
353
|
-
/* @__PURE__ */
|
|
428
|
+
/* @__PURE__ */ T("div", {
|
|
354
429
|
className: "flex items-start justify-between gap-2",
|
|
355
|
-
children: [/* @__PURE__ */
|
|
430
|
+
children: [/* @__PURE__ */ T("div", {
|
|
356
431
|
className: "min-w-0",
|
|
357
|
-
children: [/* @__PURE__ */
|
|
432
|
+
children: [/* @__PURE__ */ w("p", {
|
|
358
433
|
className: "text-xs uppercase tracking-wide text-text-secondary",
|
|
359
434
|
children: t.category
|
|
360
|
-
}), /* @__PURE__ */
|
|
435
|
+
}), /* @__PURE__ */ w("h2", {
|
|
361
436
|
className: "mt-0.5 text-base font-semibold text-text-primary",
|
|
362
437
|
children: t.metricName
|
|
363
438
|
})]
|
|
364
|
-
}), /* @__PURE__ */
|
|
439
|
+
}), /* @__PURE__ */ T("div", {
|
|
440
|
+
className: "flex items-center gap-1",
|
|
441
|
+
children: [
|
|
442
|
+
/* @__PURE__ */ w(ne, { trend: t.trend }),
|
|
443
|
+
/* @__PURE__ */ w(E, {
|
|
444
|
+
type: "button",
|
|
445
|
+
variant: "ghost",
|
|
446
|
+
size: "icon",
|
|
447
|
+
"aria-label": `Edit ${t.metricName}`,
|
|
448
|
+
onClick: () => {
|
|
449
|
+
b(t), v(!0);
|
|
450
|
+
},
|
|
451
|
+
children: /* @__PURE__ */ w(g, { className: "h-4 w-4" })
|
|
452
|
+
}),
|
|
453
|
+
/* @__PURE__ */ w(E, {
|
|
454
|
+
type: "button",
|
|
455
|
+
variant: "ghost",
|
|
456
|
+
size: "icon",
|
|
457
|
+
"aria-label": `Archive ${t.metricName}`,
|
|
458
|
+
onClick: () => A(t),
|
|
459
|
+
children: /* @__PURE__ */ w(p, { className: "h-4 w-4" })
|
|
460
|
+
})
|
|
461
|
+
]
|
|
462
|
+
})]
|
|
365
463
|
}),
|
|
366
|
-
/* @__PURE__ */
|
|
464
|
+
/* @__PURE__ */ T("div", {
|
|
367
465
|
className: "flex items-baseline gap-2",
|
|
368
466
|
children: [
|
|
369
|
-
/* @__PURE__ */
|
|
467
|
+
/* @__PURE__ */ w("span", {
|
|
370
468
|
className: "text-2xl font-bold tabular-nums text-text-primary",
|
|
371
469
|
children: e(t.value)
|
|
372
470
|
}),
|
|
373
|
-
/* @__PURE__ */
|
|
471
|
+
/* @__PURE__ */ w("span", {
|
|
374
472
|
className: "text-sm text-text-secondary",
|
|
375
473
|
children: t.unit
|
|
376
474
|
}),
|
|
377
|
-
t.target == null ? null : /* @__PURE__ */
|
|
475
|
+
t.target == null ? null : /* @__PURE__ */ T("span", {
|
|
378
476
|
className: "text-xs text-text-muted",
|
|
379
477
|
children: ["target ", e(t.target)]
|
|
380
478
|
})
|
|
381
479
|
]
|
|
382
480
|
}),
|
|
383
|
-
n == null ? null : /* @__PURE__ */
|
|
481
|
+
n == null ? null : /* @__PURE__ */ T("div", {
|
|
384
482
|
className: "space-y-1",
|
|
385
|
-
children: [/* @__PURE__ */
|
|
483
|
+
children: [/* @__PURE__ */ T("div", {
|
|
386
484
|
className: "flex items-center justify-between text-xs text-text-secondary",
|
|
387
|
-
children: [/* @__PURE__ */
|
|
485
|
+
children: [/* @__PURE__ */ w("span", { children: "Progress to target" }), /* @__PURE__ */ T("span", {
|
|
388
486
|
className: "font-medium tabular-nums",
|
|
389
487
|
children: [n, "%"]
|
|
390
488
|
})]
|
|
391
|
-
}), /* @__PURE__ */
|
|
489
|
+
}), /* @__PURE__ */ w("div", {
|
|
392
490
|
className: "h-2 overflow-hidden rounded-full bg-bg-sunken",
|
|
393
|
-
children: /* @__PURE__ */
|
|
491
|
+
children: /* @__PURE__ */ w("div", {
|
|
394
492
|
className: "h-full rounded-full bg-action-primary-bg",
|
|
395
493
|
style: { width: `${n}%` }
|
|
396
494
|
})
|
|
@@ -401,16 +499,16 @@ function Q() {
|
|
|
401
499
|
}, t.id);
|
|
402
500
|
})
|
|
403
501
|
}),
|
|
404
|
-
/* @__PURE__ */
|
|
405
|
-
page:
|
|
406
|
-
pageCount:
|
|
407
|
-
rangeStart:
|
|
408
|
-
rangeEnd:
|
|
409
|
-
totalItems:
|
|
410
|
-
canPrev:
|
|
411
|
-
canNext:
|
|
412
|
-
onPrev:
|
|
413
|
-
onNext:
|
|
502
|
+
/* @__PURE__ */ w(o, {
|
|
503
|
+
page: $.page,
|
|
504
|
+
pageCount: $.pageCount,
|
|
505
|
+
rangeStart: $.rangeStart,
|
|
506
|
+
rangeEnd: $.rangeEnd,
|
|
507
|
+
totalItems: $.totalItems,
|
|
508
|
+
canPrev: $.canPrev,
|
|
509
|
+
canNext: $.canNext,
|
|
510
|
+
onPrev: $.prevPage,
|
|
511
|
+
onNext: $.nextPage,
|
|
414
512
|
itemLabel: "metrics"
|
|
415
513
|
})
|
|
416
514
|
] }) : null
|
|
@@ -418,6 +516,6 @@ function Q() {
|
|
|
418
516
|
});
|
|
419
517
|
}
|
|
420
518
|
//#endregion
|
|
421
|
-
export {
|
|
519
|
+
export { $ as EsgDashboardPage };
|
|
422
520
|
|
|
423
521
|
//# sourceMappingURL=EsgDashboardPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EsgDashboardPage.js","names":[],"sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"sourcesContent":["import { Leaf, Plus, TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport type React from 'react';\nimport {\n Button,\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 { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n formatNumber,\n type DonutSlice,\n} from '../components';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst ESG_METRICS_QUERY = gql`\n query AssetHandlerEsgMetrics {\n esgMetrics {\n id\n metricKey\n metricName\n category\n unit\n value\n target\n trend\n periodStart\n periodEnd\n }\n }\n`;\n\nconst UPSERT_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerUpsertEsgMetric($input: UpsertEsgMetricInput!) {\n upsertEsgMetric(input: $input) {\n id\n metricName\n }\n }\n`;\n\ninterface EcoMetric {\n id: string;\n metricKey: string;\n metricName: string;\n category: string;\n unit: string;\n value: number;\n target: number | null;\n trend: string | null;\n periodStart: string;\n periodEnd: string;\n}\n\ninterface UpsertEsgMetricResult {\n upsertEsgMetric: { id: string; metricName: string };\n}\n\nconst ESG_CATEGORIES = ['environment', 'social', 'governance'] as const;\nconst ESG_TRENDS = ['improving', 'stable', 'declining'] as const;\n\nconst TREND_META: Record<\n string,\n {\n label: string;\n icon: typeof TrendingUp;\n tone: 'positive' | 'negative' | 'neutral';\n dir: 'up' | 'down' | 'neutral';\n }\n> = {\n improving: { label: 'Improving', icon: TrendingUp, tone: 'positive', dir: 'up' },\n declining: { label: 'Declining', icon: TrendingDown, tone: 'negative', dir: 'down' },\n stable: { label: 'Stable', icon: Minus, tone: 'neutral', dir: 'neutral' },\n};\n\nconst TREND_PILL: Record<'positive' | 'negative' | 'neutral', string> = {\n positive: 'bg-status-success-bg-subtle text-status-success-text',\n negative: 'bg-status-error-bg-subtle text-status-error-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nfunction firstOfMonthISO(): string {\n const now = new Date();\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);\n}\n\nfunction todayISO(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction TrendBadge({ trend }: { trend: string | null }) {\n const meta = TREND_META[trend ?? 'stable'] ?? TREND_META.stable;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',\n TREND_PILL[meta.tone]\n )}\n >\n <Icon className=\"h-3 w-3\" />\n {meta.label}\n </span>\n );\n}\n\nfunction RecordMetricDialog({\n open,\n onOpenChange,\n onSaved,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSaved: () => void;\n}) {\n const [metricName, setMetricName] = useState('');\n const [category, setCategory] = useState<(typeof ESG_CATEGORIES)[number]>('environment');\n const [unit, setUnit] = useState('');\n const [value, setValue] = useState('');\n const [target, setTarget] = useState('');\n const [trend, setTrend] = useState<(typeof ESG_TRENDS)[number]>('stable');\n const [periodStart, setPeriodStart] = useState(firstOfMonthISO());\n const [periodEnd, setPeriodEnd] = useState(todayISO());\n\n const [upsertEsgMetric, { loading: saving }] = useMutation<UpsertEsgMetricResult>(\n UPSERT_ESG_METRIC_MUTATION,\n {\n onCompleted: (result) => {\n notify.success('Metric saved', `${result.upsertEsgMetric.metricName} was recorded.`);\n setMetricName('');\n setUnit('');\n setValue('');\n setTarget('');\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not save metric', error.message),\n }\n );\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n const parsedValue = Number(value);\n if (!metricName.trim() || !unit.trim()) {\n notify.warning('Missing details', 'Metric name and unit are required.');\n return;\n }\n if (!Number.isFinite(parsedValue) || value.trim() === '') {\n notify.warning('Invalid value', 'Enter a numeric metric value.');\n return;\n }\n if (!periodStart || !periodEnd) {\n notify.warning('Missing period', 'Period start and end are required.');\n return;\n }\n const parsedTarget = target.trim() === '' ? null : Number(target);\n void upsertEsgMetric({\n variables: {\n input: {\n metricKey: slugify(metricName),\n metricName: metricName.trim(),\n category,\n unit: unit.trim(),\n value: parsedValue,\n target: parsedTarget != null && Number.isFinite(parsedTarget) ? parsedTarget : null,\n trend,\n periodStart: new Date(periodStart).toISOString(),\n periodEnd: new Date(periodEnd).toISOString(),\n },\n },\n });\n };\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>Save Metric</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Record an ESG metric to track progress toward your sustainability targets.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form className=\"space-y-3\" onSubmit={handleSubmit}>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Metric Name *</Label>\n <Input\n placeholder=\"e.g., Scope 2 carbon emissions\"\n value={metricName}\n onChange={(e) => setMetricName(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as typeof category)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_CATEGORIES.map((c) => (\n <SelectItem key={c} value={c} className=\"capitalize\">\n {c}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Unit *</Label>\n <Input\n placeholder=\"e.g., tCO2e\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Value *</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Target</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={target}\n onChange={(e) => setTarget(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Trend</Label>\n <Select value={trend} onValueChange={(v) => setTrend(v as typeof trend)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_TRENDS.map((t) => (\n <SelectItem key={t} value={t} className=\"capitalize\">\n {t}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Period Start *</Label>\n <Input\n type=\"date\"\n value={periodStart}\n onChange={(e) => setPeriodStart(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Period End *</Label>\n <Input type=\"date\" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} />\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={saving}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={saving}>\n {saving ? 'Saving…' : 'Save Metric'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function EsgDashboardPage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [category, setCategory] = useState<string>('ALL');\n\n const { data, loading, refetch } = useQuery<{ esgMetrics: EcoMetric[] }>(ESG_METRICS_QUERY, {\n fetchPolicy: 'cache-and-network',\n });\n\n const metrics = useMemo(() => data?.esgMetrics ?? [], [data?.esgMetrics]);\n\n const headline = useMemo(() => metrics.slice(0, 4), [metrics]);\n\n const filtered = useMemo(\n () => (category === 'ALL' ? metrics : metrics.filter((m) => m.category === category)),\n [metrics, category]\n );\n\n const categoryMix = useMemo<DonutSlice[]>(() => {\n const counts = metrics.reduce<Record<string, number>>((acc, m) => {\n acc[m.category] = (acc[m.category] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([cat, value], i) => ({\n name: cat.charAt(0).toUpperCase() + cat.slice(1),\n value,\n color: seriesColor(i),\n }));\n }, [metrics]);\n\n const valueVsTarget = useMemo(\n () =>\n metrics\n .filter((m) => m.target != null)\n .slice(0, 8)\n .map((m) => ({ name: m.metricName, value: m.value, target: m.target as number })),\n [metrics]\n );\n\n const categoryCounts = useMemo(() => {\n const counts: Record<string, number> = { ALL: metrics.length };\n for (const m of metrics) counts[m.category] = (counts[m.category] ?? 0) + 1;\n return counts;\n }, [metrics]);\n\n const pagination = usePagination(filtered, { pageSize: 8, resetKey: category });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={Leaf}\n title=\"ESG Dashboard\"\n description=\"Measure the environmental and governance impact of your estate against targets.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Save Metric\n </Button>\n }\n />\n <PagePurpose>\n Track energy use, carbon emissions and responsible disposal against targets — and produce\n the evidence ESG reporting and audits require.\n </PagePurpose>\n\n <RecordMetricDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n onSaved={() => void refetch()}\n />\n\n {loading && metrics.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {Array.from({ length: 4 }).map((_, i) => (\n <div key={i} className=\"h-28 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\" />\n ))}\n </div>\n ) : null}\n\n {!loading && metrics.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No ESG metrics recorded yet\"\n description=\"Save a metric to start tracking sustainability progress against your targets.\"\n />\n ) : null}\n\n {headline.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {headline.map((m) => {\n const meta = TREND_META[m.trend ?? 'stable'] ?? TREND_META.stable;\n return (\n <StatTile\n key={m.id}\n label={`${m.metricName} (${m.unit})`}\n value={m.value}\n format={formatNumber}\n delta={meta.label}\n deltaDirection={meta.dir}\n deltaTone={meta.tone}\n icon={Leaf}\n />\n );\n })}\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Value vs. target\" subtitle=\"Metrics measured against their targets\">\n {valueVsTarget.length > 0 ? (\n <BarsChart\n data={valueVsTarget}\n xKey=\"name\"\n series={[\n { key: 'value', label: 'Value' },\n { key: 'target', label: 'Target' },\n ]}\n orientation=\"horizontal\"\n height={240}\n categoryWidth={150}\n ariaLabel=\"Metric value versus target\"\n />\n ) : (\n <div className=\"flex h-[240px] items-center justify-center text-sm text-text-secondary\">\n Set a target on a metric to compare value against target.\n </div>\n )}\n </ChartCard>\n <ChartCard title=\"By category\" subtitle=\"Metrics across E / S / G\">\n <DonutChart\n data={categoryMix}\n height={240}\n centerLabel=\"Metrics\"\n centerValue={metrics.length}\n ariaLabel=\"Metrics by category\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n {(['ALL', ...ESG_CATEGORIES] as const).map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setCategory(c)}\n className={cn(\n 'rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors',\n category === c\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {c === 'ALL' ? 'All' : c}\n <span className=\"ml-1.5 text-xs opacity-70 tabular-nums\">\n {categoryCounts[c] ?? 0}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pagination.pageItems.map((metric) => {\n const pct =\n metric.target && metric.target > 0\n ? Math.min(100, Math.round((metric.value / metric.target) * 100))\n : null;\n return (\n <GlassCard key={metric.id} treatment=\"glass\" className=\"border-border-seam\">\n <div className=\"space-y-3 p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">\n {metric.category}\n </p>\n <h2 className=\"mt-0.5 text-base font-semibold text-text-primary\">\n {metric.metricName}\n </h2>\n </div>\n <TrendBadge trend={metric.trend} />\n </div>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatNumber(metric.value)}\n </span>\n <span className=\"text-sm text-text-secondary\">{metric.unit}</span>\n {metric.target != null ? (\n <span className=\"text-xs text-text-muted\">\n target {formatNumber(metric.target)}\n </span>\n ) : null}\n </div>\n {pct != null ? (\n <div className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>Progress to target</span>\n <span className=\"font-medium tabular-nums\">{pct}%</span>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n })}\n </div>\n\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=\"metrics\"\n />\n </>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EsgDashboardPage.js","names":[],"sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"sourcesContent":["import { Archive, Leaf, Minus, Pencil, Plus, TrendingDown, TrendingUp } from 'lucide-react';\nimport type React from 'react';\nimport {\n Button,\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 { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n formatNumber,\n type DonutSlice,\n} from '../components';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst ESG_METRICS_QUERY = gql`\n query AssetHandlerEsgMetrics {\n esgMetrics {\n id\n metricKey\n metricName\n category\n unit\n value\n target\n trend\n periodStart\n periodEnd\n }\n }\n`;\n\nconst CREATE_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerCreateEsgMetric($input: CreateEsgMetricInput!) {\n createEsgMetric(input: $input) {\n id\n metricName\n }\n }\n`;\n\nconst UPDATE_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerUpdateEsgMetric($id: ID!, $input: UpdateEsgMetricInput!) {\n updateEsgMetric(id: $id, input: $input) {\n id\n metricName\n }\n }\n`;\n\nconst ARCHIVE_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerArchiveEsgMetric($id: ID!) {\n archiveEsgMetric(id: $id)\n }\n`;\n\ninterface EcoMetric {\n id: string;\n metricKey: string;\n metricName: string;\n category: string;\n unit: string;\n value: number;\n target: number | null;\n trend: string | null;\n periodStart: string;\n periodEnd: string;\n}\n\ninterface CreateEsgMetricResult {\n createEsgMetric: { id: string; metricName: string };\n}\n\ninterface UpdateEsgMetricResult {\n updateEsgMetric: { id: string; metricName: string };\n}\n\nconst ESG_CATEGORIES = ['environment', 'social', 'governance'] as const;\nconst ESG_TRENDS = ['improving', 'stable', 'declining'] as const;\n\nconst TREND_META: Record<\n string,\n {\n label: string;\n icon: typeof TrendingUp;\n tone: 'positive' | 'negative' | 'neutral';\n dir: 'up' | 'down' | 'neutral';\n }\n> = {\n improving: { label: 'Improving', icon: TrendingUp, tone: 'positive', dir: 'up' },\n declining: { label: 'Declining', icon: TrendingDown, tone: 'negative', dir: 'down' },\n stable: { label: 'Stable', icon: Minus, tone: 'neutral', dir: 'neutral' },\n};\n\nconst TREND_PILL: Record<'positive' | 'negative' | 'neutral', string> = {\n positive: 'bg-status-success-bg-subtle text-status-success-text',\n negative: 'bg-status-error-bg-subtle text-status-error-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\ninterface MetricFormValues {\n metricName: string;\n category: (typeof ESG_CATEGORIES)[number];\n unit: string;\n value: number;\n target: number | null;\n trend: (typeof ESG_TRENDS)[number];\n periodStart: string;\n periodEnd: string;\n}\n\nexport function buildMetricMutationVariables(metricId: string | null, form: MetricFormValues) {\n const mutableInput = {\n metricName: form.metricName.trim(),\n category: form.category,\n unit: form.unit.trim(),\n value: form.value,\n target: form.target,\n trend: form.trend,\n };\n if (metricId)\n return { kind: 'update' as const, variables: { id: metricId, input: mutableInput } };\n return {\n kind: 'create' as const,\n variables: {\n input: {\n ...mutableInput,\n metricKey: slugify(form.metricName),\n periodStart: new Date(form.periodStart).toISOString(),\n periodEnd: new Date(form.periodEnd).toISOString(),\n },\n },\n };\n}\n\nfunction firstOfMonthISO(): string {\n const now = new Date();\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);\n}\n\nfunction todayISO(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction TrendBadge({ trend }: { trend: string | null }) {\n const meta = TREND_META[trend ?? 'stable'] ?? TREND_META.stable;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',\n TREND_PILL[meta.tone]\n )}\n >\n <Icon className=\"h-3 w-3\" />\n {meta.label}\n </span>\n );\n}\n\nfunction RecordMetricDialog({\n open,\n metric,\n onOpenChange,\n onSaved,\n}: {\n open: boolean;\n metric: EcoMetric | null;\n onOpenChange: (open: boolean) => void;\n onSaved: () => void;\n}) {\n const [metricName, setMetricName] = useState(metric?.metricName ?? '');\n const [category, setCategory] = useState<(typeof ESG_CATEGORIES)[number]>(\n (metric?.category as (typeof ESG_CATEGORIES)[number]) ?? 'environment'\n );\n const [unit, setUnit] = useState(metric?.unit ?? '');\n const [value, setValue] = useState(metric ? String(metric.value) : '');\n const [target, setTarget] = useState(metric?.target == null ? '' : String(metric.target));\n const [trend, setTrend] = useState<(typeof ESG_TRENDS)[number]>(\n (metric?.trend as (typeof ESG_TRENDS)[number]) ?? 'stable'\n );\n const [periodStart, setPeriodStart] = useState(\n metric?.periodStart.slice(0, 10) ?? firstOfMonthISO()\n );\n const [periodEnd, setPeriodEnd] = useState(metric?.periodEnd.slice(0, 10) ?? todayISO());\n\n const mutationOptions = {\n onError: (error: Error) => notify.error('Could not save metric', error.message),\n };\n const [createEsgMetric, { loading: creating }] = useMutation<CreateEsgMetricResult>(\n CREATE_ESG_METRIC_MUTATION,\n {\n ...mutationOptions,\n onCompleted: (result) => {\n notify.success('Metric saved', `${result.createEsgMetric.metricName} was recorded.`);\n onOpenChange(false);\n onSaved();\n },\n }\n );\n const [updateEsgMetric, { loading: updating }] = useMutation<UpdateEsgMetricResult>(\n UPDATE_ESG_METRIC_MUTATION,\n {\n ...mutationOptions,\n onCompleted: (result) => {\n notify.success('Metric updated', `${result.updateEsgMetric.metricName} was updated.`);\n onOpenChange(false);\n onSaved();\n },\n }\n );\n const saving = creating || updating;\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n const parsedValue = Number(value);\n if (!metricName.trim() || !unit.trim()) {\n notify.warning('Missing details', 'Metric name and unit are required.');\n return;\n }\n if (!Number.isFinite(parsedValue) || value.trim() === '') {\n notify.warning('Invalid value', 'Enter a numeric metric value.');\n return;\n }\n if (!periodStart || !periodEnd) {\n notify.warning('Missing period', 'Period start and end are required.');\n return;\n }\n const parsedTarget = target.trim() === '' ? null : Number(target);\n const mutation = buildMetricMutationVariables(metric?.id ?? null, {\n metricName,\n category,\n unit,\n value: parsedValue,\n target: parsedTarget != null && Number.isFinite(parsedTarget) ? parsedTarget : null,\n trend,\n periodStart,\n periodEnd,\n });\n if (mutation.kind === 'update') {\n void updateEsgMetric(mutation);\n } else {\n void createEsgMetric(mutation);\n }\n };\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>{metric ? 'Edit Metric' : 'Save Metric'}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {metric\n ? 'Update this metric’s values. Its source identity and reporting period stay unchanged.'\n : 'Record an ESG metric to track progress toward your sustainability targets.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form className=\"space-y-3\" onSubmit={handleSubmit}>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Metric Name *</Label>\n <Input\n placeholder=\"e.g., Scope 2 carbon emissions\"\n value={metricName}\n onChange={(e) => setMetricName(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as typeof category)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_CATEGORIES.map((c) => (\n <SelectItem key={c} value={c} className=\"capitalize\">\n {c}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Unit *</Label>\n <Input\n placeholder=\"e.g., tCO2e\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Value *</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Target</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={target}\n onChange={(e) => setTarget(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Trend</Label>\n <Select value={trend} onValueChange={(v) => setTrend(v as typeof trend)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_TRENDS.map((t) => (\n <SelectItem key={t} value={t} className=\"capitalize\">\n {t}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Period Start *</Label>\n <Input\n type=\"date\"\n value={periodStart}\n onChange={(e) => setPeriodStart(e.target.value)}\n disabled={metric != null}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Period End *</Label>\n <Input\n type=\"date\"\n value={periodEnd}\n onChange={(e) => setPeriodEnd(e.target.value)}\n disabled={metric != null}\n />\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={saving}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={saving}>\n {saving ? 'Saving…' : metric ? 'Update Metric' : 'Save Metric'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function EsgDashboardPage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [editingMetric, setEditingMetric] = useState<EcoMetric | null>(null);\n const [archivingMetric, setArchivingMetric] = useState<EcoMetric | null>(null);\n const [category, setCategory] = useState<string>('ALL');\n\n const { data, loading, refetch } = useQuery<{ esgMetrics: EcoMetric[] }>(ESG_METRICS_QUERY, {\n fetchPolicy: 'cache-and-network',\n });\n\n const metrics = useMemo(() => data?.esgMetrics ?? [], [data?.esgMetrics]);\n\n const [archiveEsgMetric, { loading: archiving }] = useMutation<{ archiveEsgMetric: boolean }>(\n ARCHIVE_ESG_METRIC_MUTATION,\n {\n onCompleted: () => {\n notify.success('Metric archived', 'The metric was removed from the active ESG dashboard.');\n setArchivingMetric(null);\n void refetch();\n },\n onError: (error) => notify.error('Could not archive metric', error.message),\n }\n );\n\n const headline = useMemo(() => metrics.slice(0, 4), [metrics]);\n\n const filtered = useMemo(\n () => (category === 'ALL' ? metrics : metrics.filter((m) => m.category === category)),\n [metrics, category]\n );\n\n const categoryMix = useMemo<DonutSlice[]>(() => {\n const counts = metrics.reduce<Record<string, number>>((acc, m) => {\n acc[m.category] = (acc[m.category] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([cat, value], i) => ({\n name: cat.charAt(0).toUpperCase() + cat.slice(1),\n value,\n color: seriesColor(i),\n }));\n }, [metrics]);\n\n const valueVsTarget = useMemo(\n () =>\n metrics\n .filter((m) => m.target != null)\n .slice(0, 8)\n .map((m) => ({ name: m.metricName, value: m.value, target: m.target as number })),\n [metrics]\n );\n\n const categoryCounts = useMemo(() => {\n const counts: Record<string, number> = { ALL: metrics.length };\n for (const m of metrics) counts[m.category] = (counts[m.category] ?? 0) + 1;\n return counts;\n }, [metrics]);\n\n const pagination = usePagination(filtered, { pageSize: 8, resetKey: category });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={Leaf}\n title=\"ESG Dashboard\"\n description=\"Measure the environmental and governance impact of your estate against targets.\"\n actions={\n <Button\n className=\"w-full sm:w-auto\"\n onClick={() => {\n setEditingMetric(null);\n setDialogOpen(true);\n }}\n >\n <Plus className=\"mr-2 h-4 w-4\" />\n Save Metric\n </Button>\n }\n />\n <PagePurpose>\n Track energy use, carbon emissions and responsible disposal against targets — and produce\n the evidence ESG reporting and audits require.\n </PagePurpose>\n\n {dialogOpen ? (\n <RecordMetricDialog\n open\n metric={editingMetric}\n onOpenChange={(nextOpen) => {\n setDialogOpen(nextOpen);\n if (!nextOpen) setEditingMetric(null);\n }}\n onSaved={() => void refetch()}\n />\n ) : null}\n\n <ResponsiveDialog\n open={archivingMetric != null}\n onOpenChange={(nextOpen) => {\n if (!nextOpen) setArchivingMetric(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Archive metric</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Archive {archivingMetric?.metricName ?? 'this metric'}? It will leave the active ESG\n dashboard. Archiving does not refund monthly usage.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setArchivingMetric(null)} disabled={archiving}>\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n disabled={archiving || archivingMetric == null}\n onClick={() => {\n if (archivingMetric) {\n void archiveEsgMetric({ variables: { id: archivingMetric.id } });\n }\n }}\n >\n {archiving ? 'Archiving…' : 'Archive Metric'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n\n {loading && metrics.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {Array.from({ length: 4 }).map((_, i) => (\n <div key={i} className=\"h-28 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\" />\n ))}\n </div>\n ) : null}\n\n {!loading && metrics.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No ESG metrics recorded yet\"\n description=\"Save a metric to start tracking sustainability progress against your targets.\"\n />\n ) : null}\n\n {headline.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {headline.map((m) => {\n const meta = TREND_META[m.trend ?? 'stable'] ?? TREND_META.stable;\n return (\n <StatTile\n key={m.id}\n label={`${m.metricName} (${m.unit})`}\n value={m.value}\n format={formatNumber}\n delta={meta.label}\n deltaDirection={meta.dir}\n deltaTone={meta.tone}\n icon={Leaf}\n />\n );\n })}\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Value vs. target\" subtitle=\"Metrics measured against their targets\">\n {valueVsTarget.length > 0 ? (\n <BarsChart\n data={valueVsTarget}\n xKey=\"name\"\n series={[\n { key: 'value', label: 'Value' },\n { key: 'target', label: 'Target' },\n ]}\n orientation=\"horizontal\"\n height={240}\n categoryWidth={150}\n ariaLabel=\"Metric value versus target\"\n />\n ) : (\n <div className=\"flex h-[240px] items-center justify-center text-sm text-text-secondary\">\n Set a target on a metric to compare value against target.\n </div>\n )}\n </ChartCard>\n <ChartCard title=\"By category\" subtitle=\"Metrics across E / S / G\">\n <DonutChart\n data={categoryMix}\n height={240}\n centerLabel=\"Metrics\"\n centerValue={metrics.length}\n ariaLabel=\"Metrics by category\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n {(['ALL', ...ESG_CATEGORIES] as const).map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setCategory(c)}\n className={cn(\n 'rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors',\n category === c\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {c === 'ALL' ? 'All' : c}\n <span className=\"ml-1.5 text-xs opacity-70 tabular-nums\">\n {categoryCounts[c] ?? 0}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pagination.pageItems.map((metric) => {\n const pct =\n metric.target && metric.target > 0\n ? Math.min(100, Math.round((metric.value / metric.target) * 100))\n : null;\n return (\n <GlassCard key={metric.id} treatment=\"glass\" className=\"border-border-seam\">\n <div className=\"space-y-3 p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">\n {metric.category}\n </p>\n <h2 className=\"mt-0.5 text-base font-semibold text-text-primary\">\n {metric.metricName}\n </h2>\n </div>\n <div className=\"flex items-center gap-1\">\n <TrendBadge trend={metric.trend} />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={`Edit ${metric.metricName}`}\n onClick={() => {\n setEditingMetric(metric);\n setDialogOpen(true);\n }}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={`Archive ${metric.metricName}`}\n onClick={() => setArchivingMetric(metric)}\n >\n <Archive className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatNumber(metric.value)}\n </span>\n <span className=\"text-sm text-text-secondary\">{metric.unit}</span>\n {metric.target != null ? (\n <span className=\"text-xs text-text-muted\">\n target {formatNumber(metric.target)}\n </span>\n ) : null}\n </div>\n {pct != null ? (\n <div className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>Progress to target</span>\n <span className=\"font-medium tabular-nums\">{pct}%</span>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n })}\n </div>\n\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=\"metrics\"\n />\n </>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant.d.ts","sourceRoot":"","sources":["../../src/types/assistant.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAElG,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAE/C,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,cAAc,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAEhG,MAAM,MAAM,2BAA2B,
|
|
1
|
+
{"version":3,"file":"assistant.d.ts","sourceRoot":"","sources":["../../src/types/assistant.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAElG,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAE/C,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,cAAc,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAEhG,MAAM,MAAM,2BAA2B,GACrC,MAAM,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;AAE9E,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,CAAC,EAAE,wBAAwB,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,2BAA2B,CAAC;IACpC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,eAAe,EAAE,mBAAmB,EAAkC,CAAC"}
|