@burdenoff/microfe-ecoimpacthub 2026.704.4 → 2026.704.5

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.
Files changed (47) hide show
  1. package/dist/ecoimpacthub/pages/DashboardPage.js +345 -189
  2. package/dist/ecoimpacthub/pages/DashboardPage.js.map +1 -1
  3. package/dist/ecoimpacthub/pages/_shared/ConservationBadge.js +58 -0
  4. package/dist/ecoimpacthub/pages/_shared/ConservationBadge.js.map +1 -0
  5. package/dist/ecoimpacthub/pages/_shared/EvidenceThumb.js +70 -0
  6. package/dist/ecoimpacthub/pages/_shared/EvidenceThumb.js.map +1 -0
  7. package/dist/ecoimpacthub/pages/_shared/MethodBadge.js +51 -0
  8. package/dist/ecoimpacthub/pages/_shared/MethodBadge.js.map +1 -0
  9. package/dist/ecoimpacthub/pages/_shared/TaxonChip.js +29 -0
  10. package/dist/ecoimpacthub/pages/_shared/TaxonChip.js.map +1 -0
  11. package/dist/ecoimpacthub/pages/badges.js +71 -0
  12. package/dist/ecoimpacthub/pages/badges.js.map +1 -0
  13. package/dist/ecoimpacthub/pages/carbon/CarbonPage.js +337 -300
  14. package/dist/ecoimpacthub/pages/carbon/CarbonPage.js.map +1 -1
  15. package/dist/ecoimpacthub/pages/carbonVisuals.js +89 -0
  16. package/dist/ecoimpacthub/pages/carbonVisuals.js.map +1 -0
  17. package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js +252 -218
  18. package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js.map +1 -1
  19. package/dist/ecoimpacthub/pages/habitat/HabitatProjectsPage.js +15 -12
  20. package/dist/ecoimpacthub/pages/habitat/HabitatProjectsPage.js.map +1 -1
  21. package/dist/ecoimpacthub/pages/habitat/LifecycleProgress.js +168 -0
  22. package/dist/ecoimpacthub/pages/habitat/LifecycleProgress.js.map +1 -0
  23. package/dist/ecoimpacthub/pages/monitoring/MonitoringPage.js +30 -17
  24. package/dist/ecoimpacthub/pages/monitoring/MonitoringPage.js.map +1 -1
  25. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +236 -219
  26. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
  27. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +248 -212
  28. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
  29. package/dist/ecoimpacthub/pages/reports/ReportsPage.js +119 -118
  30. package/dist/ecoimpacthub/pages/reports/ReportsPage.js.map +1 -1
  31. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +498 -370
  32. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
  33. package/dist/ecoimpacthub/pages/sites/SitesPage.js +109 -99
  34. package/dist/ecoimpacthub/pages/sites/SitesPage.js.map +1 -1
  35. package/dist/ecoimpacthub/pages/sites/siteTypeVisuals.js +64 -0
  36. package/dist/ecoimpacthub/pages/sites/siteTypeVisuals.js.map +1 -0
  37. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +275 -259
  38. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
  39. package/dist/ecoimpacthub/pages/species/SpeciesPage.js +235 -177
  40. package/dist/ecoimpacthub/pages/species/SpeciesPage.js.map +1 -1
  41. package/dist/node_modules/recharts/es6/cartesian/CartesianGrid.js +305 -0
  42. package/dist/node_modules/recharts/es6/cartesian/CartesianGrid.js.map +1 -0
  43. package/dist/node_modules/recharts/es6/context/chartLayoutContext.js +44 -33
  44. package/dist/node_modules/recharts/es6/context/chartLayoutContext.js.map +1 -1
  45. package/dist/node_modules/recharts/es6/util/ChartUtils.js +31 -23
  46. package/dist/node_modules/recharts/es6/util/ChartUtils.js.map +1 -1
  47. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"HabitatProjectDetailPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.tsx"],"sourcesContent":["/**\n * Habitat project detail — overview, lifecycle transitions & restoration activities.\n * @module ecoimpacthub/pages/habitat\n *\n * Reads `ecoHabitatProject` (+ nested activities); writes\n * `ecoTransitionHabitatProject`, `ecoLogRestorationActivity`.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Plus, Sprout } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataTable, type Column } from '../../components/DataTable';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { useEcoImpactHub } from '../../context';\nimport {\n useEcoHabitatProject,\n useEcoTransitionHabitatProject,\n useEcoLogRestorationActivity,\n} from '../../hooks';\nimport { formatDate, formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport type {\n EcoHabitatProjectStatus,\n EcoRestorationActivity,\n EcoRestorationActivityType,\n} from '../../types';\n\n/** Allowed forward lifecycle transitions per current status. */\nconst NEXT_STATUSES: Record<EcoHabitatProjectStatus, EcoHabitatProjectStatus[]> = {\n PLANNED: ['IN_PROGRESS', 'ON_HOLD', 'CANCELLED'],\n IN_PROGRESS: ['MONITORING', 'ON_HOLD', 'COMPLETED', 'CANCELLED'],\n MONITORING: ['COMPLETED', 'IN_PROGRESS'],\n ON_HOLD: ['IN_PROGRESS', 'CANCELLED'],\n COMPLETED: [],\n CANCELLED: [],\n};\n\nconst ACTIVITY_TYPES: EcoRestorationActivityType[] = [\n 'PLANTING',\n 'INVASIVE_REMOVAL',\n 'REWILDING',\n 'WATER_MANAGEMENT',\n 'SOIL_REMEDIATION',\n 'COMMUNITY_ENGAGEMENT',\n 'SURVEY',\n];\n\n/** Local `YYYY-MM-DDTHH:mm` string for a datetime-local default of \"now\". */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n};\n\ninterface ActivityFormState {\n type: EcoRestorationActivityType;\n summary: string;\n details: string;\n quantity: string;\n unit: string;\n performedAt: string;\n}\n\nconst emptyActivityForm = (): ActivityFormState => ({\n type: 'PLANTING',\n summary: '',\n details: '',\n quantity: '',\n unit: '',\n performedAt: nowLocal(),\n});\n\nexport function HabitatProjectDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useEcoImpactHub();\n const { data: project, isLoading, isError, refetch } = useEcoHabitatProject(id);\n\n const transitionProject = useEcoTransitionHabitatProject();\n const logActivity = useEcoLogRestorationActivity();\n\n const [activityOpen, setActivityOpen] = useState(false);\n const [activityForm, setActivityForm] = useState<ActivityFormState>(emptyActivityForm);\n const [error, setError] = useState<string | null>(null);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Habitat project\">\n <LoadingView />\n </PageLayout>\n );\n }\n if (isError || !project) {\n return (\n <PageLayout title=\"Habitat project\">\n <ErrorView message=\"This project could not be loaded.\" onRetry={() => refetch()} />\n </PageLayout>\n );\n }\n\n const activities = project.activities ?? [];\n const nextStatuses = NEXT_STATUSES[project.status];\n\n const handleTransition = async (status: EcoHabitatProjectStatus) => {\n if (!id) return;\n setError(null);\n try {\n await transitionProject.mutateAsync({ id, status });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update project status.');\n }\n };\n\n const handleLogActivity = async () => {\n if (!id || !activityForm.summary.trim()) {\n setError('Activity summary is required.');\n return;\n }\n setError(null);\n try {\n await logActivity.mutateAsync({\n projectId: id,\n type: activityForm.type,\n summary: activityForm.summary.trim(),\n details: activityForm.details.trim() || null,\n quantity: activityForm.quantity ? Number(activityForm.quantity) : null,\n unit: activityForm.unit.trim() || null,\n performedAt: activityForm.performedAt\n ? new Date(activityForm.performedAt).toISOString()\n : new Date().toISOString(),\n });\n setActivityOpen(false);\n setActivityForm(emptyActivityForm());\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log activity.');\n }\n };\n\n const activityColumns: Column<EcoRestorationActivity>[] = [\n {\n key: 'type',\n header: 'Type',\n render: (a) => (\n <span className=\"font-medium text-text-primary\">{formatEnumLabel(a.type)}</span>\n ),\n },\n {\n key: 'summary',\n header: 'Summary',\n render: (a) => <span className=\"text-text-secondary\">{a.summary}</span>,\n },\n {\n key: 'quantity',\n header: 'Quantity',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.quantity ?? 0,\n render: (a) =>\n a.quantity != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(a.quantity)}\n {a.unit ? ` ${a.unit}` : ''}\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'performedAt',\n header: 'Performed',\n align: 'right',\n hideOnMobile: true,\n render: (a) => <span className=\"text-text-tertiary\">{formatDateTime(a.performedAt)}</span>,\n },\n ];\n\n return (\n <PageLayout\n title={project.name}\n description={formatEnumLabel(project.status)}\n actions={\n <>\n <StatusBadge status={project.status} />\n <Button variant=\"ghost\" onClick={() => navigateTo('habitat')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n </>\n }\n >\n {/* Overview + actions */}\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <GlassCard treatment=\"glass\" className=\"p-6 lg:col-span-2\">\n <dl className=\"grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3\">\n <div>\n <dt className=\"text-xs text-text-tertiary\">Status</dt>\n <dd className=\"mt-1\">\n <StatusBadge status={project.status} />\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-tertiary\">Target area</dt>\n <dd className=\"mt-0.5 text-sm text-text-primary tabular-nums\">\n {project.targetHectares != null\n ? `${formatNumber(project.targetHectares)} ha`\n : '—'}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-tertiary\">Budget</dt>\n <dd className=\"mt-0.5 text-sm text-text-primary tabular-nums\">\n {formatNumber(project.budgetAmount)} {project.currency}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-tertiary\">Start</dt>\n <dd className=\"mt-0.5 text-sm text-text-primary\">{formatDate(project.startDate)}</dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-tertiary\">End</dt>\n <dd className=\"mt-0.5 text-sm text-text-primary\">{formatDate(project.endDate)}</dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-tertiary\">Completed</dt>\n <dd className=\"mt-0.5 text-sm text-text-primary\">\n {formatDate(project.completedAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-tertiary\">Site</dt>\n <dd className=\"mt-0.5 text-sm text-text-primary\">{project.siteId ?? '—'}</dd>\n </div>\n </dl>\n {project.description && (\n <p className=\"mt-5 rounded-lg bg-bg-sunken/60 p-3 text-sm text-text-secondary\">\n {project.description}\n </p>\n )}\n </GlassCard>\n\n <div className=\"flex flex-col gap-4\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-6\">\n <p className=\"text-xs text-text-tertiary\">Quick actions</p>\n <Button onClick={() => setActivityOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Log activity\n </Button>\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-6\">\n <p className=\"text-xs text-text-tertiary\">Advance project</p>\n {nextStatuses.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n This project is {formatEnumLabel(project.status).toLowerCase()} — no further\n transitions.\n </p>\n ) : (\n nextStatuses.map((next) => (\n <Button\n key={next}\n variant=\"outline\"\n onClick={() => handleTransition(next)}\n disabled={transitionProject.isPending}\n >\n {formatEnumLabel(next)}\n </Button>\n ))\n )}\n </GlassCard>\n </div>\n </div>\n\n {error && <p className=\"mt-4 text-sm text-status-error-text\">{error}</p>}\n\n {/* Restoration activities */}\n <Section\n title=\"Restoration activities\"\n description=\"Work logged against this project — planting, removals, surveys & engagement.\"\n action={\n <Button size=\"sm\" onClick={() => setActivityOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Log activity\n </Button>\n }\n >\n {activities.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Sprout className=\"h-10 w-10\" />}\n title=\"No activities yet\"\n description=\"Log restoration work to build this project's field record.\"\n />\n ) : (\n <DataTable columns={activityColumns} rows={activities} rowKey={(a) => a.id} />\n )}\n </Section>\n\n {/* Log activity dialog */}\n <ResponsiveDialog open={activityOpen} onOpenChange={setActivityOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Log restoration activity</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Record a piece of work performed for {project.name}.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actType\">Type</Label>\n <Select\n value={activityForm.type}\n onValueChange={(v) =>\n setActivityForm({ ...activityForm, type: v as EcoRestorationActivityType })\n }\n >\n <SelectTrigger id=\"actType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ACTIVITY_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actPerformedAt\">Performed at</Label>\n <Input\n id=\"actPerformedAt\"\n type=\"datetime-local\"\n value={activityForm.performedAt}\n onChange={(e) =>\n setActivityForm({ ...activityForm, performedAt: e.target.value })\n }\n />\n </div>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actSummary\">Summary *</Label>\n <Input\n id=\"actSummary\"\n value={activityForm.summary}\n onChange={(e) => setActivityForm({ ...activityForm, summary: e.target.value })}\n placeholder=\"Planted 500 mangrove saplings\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actDetails\">Details</Label>\n <Textarea\n id=\"actDetails\"\n value={activityForm.details}\n onChange={(e) => setActivityForm({ ...activityForm, details: e.target.value })}\n placeholder=\"Optional notes about the work.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actQuantity\">Quantity</Label>\n <Input\n id=\"actQuantity\"\n type=\"number\"\n step=\"any\"\n value={activityForm.quantity}\n onChange={(e) => setActivityForm({ ...activityForm, quantity: e.target.value })}\n placeholder=\"500\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actUnit\">Unit</Label>\n <Input\n id=\"actUnit\"\n value={activityForm.unit}\n onChange={(e) => setActivityForm({ ...activityForm, unit: e.target.value })}\n placeholder=\"saplings\"\n />\n </div>\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setActivityOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleLogActivity} disabled={logActivity.isPending}>\n {logActivity.isPending ? 'Logging…' : 'Log activity'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HabitatProjectDetailPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.tsx"],"sourcesContent":["/**\n * Habitat project detail — overview, lifecycle-progress, transitions & activities.\n * @module ecoimpacthub/pages/habitat\n *\n * Reads `ecoHabitatProject` (+ nested activities); writes\n * `ecoTransitionHabitatProject`, `ecoLogRestorationActivity`. The hero is\n * balanced into three tidy cards — overview, lifecycle-progress (a status-derived\n * radial ring + stepper, since the schema has no numeric progress) and\n * advance-project. Tags are driven via the platform tag store (no eco-svc change).\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, ArrowRight, Plus, Sprout } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { DataTable, type Column } from '../../components/DataTable';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { EcoTags } from '../../components/EcoTags';\nimport { useEcoImpactHub } from '../../context';\nimport {\n useEcoHabitatProject,\n useEcoTransitionHabitatProject,\n useEcoLogRestorationActivity,\n useEcoSites,\n} from '../../hooks';\nimport { formatDate, formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport type {\n EcoHabitatProjectStatus,\n EcoRestorationActivity,\n EcoRestorationActivityType,\n} from '../../types';\nimport { ActivityTypeBadge, LifecycleBadge } from '../badges';\nimport { LifecycleProgress } from './LifecycleProgress';\n\n/** Allowed forward lifecycle transitions per current status. */\nconst NEXT_STATUSES: Record<EcoHabitatProjectStatus, EcoHabitatProjectStatus[]> = {\n PLANNED: ['IN_PROGRESS', 'ON_HOLD', 'CANCELLED'],\n IN_PROGRESS: ['MONITORING', 'ON_HOLD', 'COMPLETED', 'CANCELLED'],\n MONITORING: ['COMPLETED', 'IN_PROGRESS'],\n ON_HOLD: ['IN_PROGRESS', 'CANCELLED'],\n COMPLETED: [],\n CANCELLED: [],\n};\n\nconst ACTIVITY_TYPES: EcoRestorationActivityType[] = [\n 'PLANTING',\n 'INVASIVE_REMOVAL',\n 'REWILDING',\n 'WATER_MANAGEMENT',\n 'SOIL_REMEDIATION',\n 'COMMUNITY_ENGAGEMENT',\n 'SURVEY',\n];\n\n/** Local `YYYY-MM-DDTHH:mm` string for a datetime-local default of \"now\". */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n};\n\ninterface ActivityFormState {\n type: EcoRestorationActivityType;\n summary: string;\n details: string;\n quantity: string;\n unit: string;\n performedAt: string;\n}\n\nconst emptyActivityForm = (): ActivityFormState => ({\n type: 'PLANTING',\n summary: '',\n details: '',\n quantity: '',\n unit: '',\n performedAt: nowLocal(),\n});\n\n/** A labelled key/value cell for the overview grid. */\nfunction Fact({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div>\n <dt className=\"text-xs text-text-tertiary\">{label}</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">{children}</dd>\n </div>\n );\n}\n\nexport function HabitatProjectDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useEcoImpactHub();\n const { data: project, isLoading, isError, refetch } = useEcoHabitatProject(id);\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n\n const transitionProject = useEcoTransitionHabitatProject();\n const logActivity = useEcoLogRestorationActivity();\n\n const [activityOpen, setActivityOpen] = useState(false);\n const [activityForm, setActivityForm] = useState<ActivityFormState>(emptyActivityForm);\n const [error, setError] = useState<string | null>(null);\n\n const projectSiteId = project?.siteId ?? null;\n const siteItems = sites.data?.items;\n const siteName = useMemo(\n () => (projectSiteId ? (siteItems?.find((s) => s.id === projectSiteId)?.name ?? null) : null),\n [projectSiteId, siteItems]\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Habitat project\">\n <LoadingView />\n </PageLayout>\n );\n }\n if (isError || !project) {\n return (\n <PageLayout title=\"Habitat project\">\n <ErrorView message=\"This project could not be loaded.\" onRetry={() => refetch()} />\n </PageLayout>\n );\n }\n\n const activities = project.activities ?? [];\n const nextStatuses = NEXT_STATUSES[project.status];\n const timeline = [formatDate(project.startDate), formatDate(project.endDate)].join(' – ');\n const subtitle = [siteName, timeline].filter((v) => v && v !== '— – —').join(' · ');\n\n const handleTransition = async (status: EcoHabitatProjectStatus) => {\n if (!id) return;\n setError(null);\n try {\n await transitionProject.mutateAsync({ id, status });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update project status.');\n }\n };\n\n const handleLogActivity = async () => {\n if (!id || !activityForm.summary.trim()) {\n setError('Activity summary is required.');\n return;\n }\n setError(null);\n try {\n await logActivity.mutateAsync({\n projectId: id,\n type: activityForm.type,\n summary: activityForm.summary.trim(),\n details: activityForm.details.trim() || null,\n quantity: activityForm.quantity ? Number(activityForm.quantity) : null,\n unit: activityForm.unit.trim() || null,\n performedAt: activityForm.performedAt\n ? new Date(activityForm.performedAt).toISOString()\n : new Date().toISOString(),\n });\n setActivityOpen(false);\n setActivityForm(emptyActivityForm());\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log activity.');\n }\n };\n\n const activityColumns: Column<EcoRestorationActivity>[] = [\n {\n key: 'type',\n header: 'Type',\n render: (a) => <ActivityTypeBadge type={a.type} />,\n },\n {\n key: 'summary',\n header: 'Summary',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"text-text-primary\">{a.summary}</p>\n {a.details && (\n <p className=\"mt-0.5 max-w-md truncate text-xs text-text-tertiary\">{a.details}</p>\n )}\n </div>\n ),\n },\n {\n key: 'quantity',\n header: 'Quantity',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.quantity ?? 0,\n render: (a) =>\n a.quantity != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(a.quantity)}\n {a.unit ? ` ${a.unit}` : ''}\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'performedAt',\n header: 'Performed',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.performedAt,\n render: (a) => <span className=\"text-text-tertiary\">{formatDateTime(a.performedAt)}</span>,\n },\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex flex-wrap items-center gap-x-3 gap-y-2\">\n <span className=\"text-text-primary\">{project.name}</span>\n <LifecycleBadge status={project.status} />\n </span>\n }\n description={subtitle || undefined}\n actions={\n <Button variant=\"ghost\" onClick={() => navigateTo('habitat')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n }\n >\n {/* Hero — overview · lifecycle-progress · advance-project */}\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-4\">\n {/* Overview */}\n <GlassCard treatment=\"glass\" className=\"p-6 lg:col-span-2\">\n <dl className=\"grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3\">\n <Fact label=\"Status\">\n <LifecycleBadge status={project.status} />\n </Fact>\n <Fact label=\"Target area\">\n <span className=\"tabular-nums\">\n {project.targetHectares != null\n ? `${formatNumber(project.targetHectares)} ha`\n : '—'}\n </span>\n </Fact>\n <Fact label=\"Budget\">\n <span className=\"tabular-nums\">\n {project.budgetAmount > 0\n ? `${formatNumber(project.budgetAmount)} ${project.currency}`\n : '—'}\n </span>\n </Fact>\n <Fact label=\"Start\">{formatDate(project.startDate)}</Fact>\n <Fact label=\"End\">{formatDate(project.endDate)}</Fact>\n <Fact label={project.completedAt ? 'Completed' : 'Site'}>\n {project.completedAt ? formatDate(project.completedAt) : (siteName ?? '—')}\n </Fact>\n {project.completedAt && <Fact label=\"Site\">{siteName ?? '—'}</Fact>}\n </dl>\n\n {project.description && (\n <p className=\"mt-5 rounded-lg bg-bg-sunken/60 p-3 text-sm text-text-secondary\">\n {project.description}\n </p>\n )}\n\n <div className=\"mt-5 border-t border-border-seam pt-4\">\n <EcoTags entityType=\"ecoimpacthub-habitat\" entityId={project.id} />\n </div>\n </GlassCard>\n\n {/* Lifecycle progress */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-6\">\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Lifecycle progress\n </p>\n <div className=\"flex flex-1 items-center justify-center\">\n <LifecycleProgress status={project.status} />\n </div>\n </GlassCard>\n\n {/* Advance project */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-6\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Advance project\n </p>\n {nextStatuses.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n This project is {formatEnumLabel(project.status).toLowerCase()} — no further\n transitions.\n </p>\n ) : (\n nextStatuses.map((next) => (\n <Button\n key={next}\n variant=\"outline\"\n onClick={() => handleTransition(next)}\n disabled={transitionProject.isPending}\n className=\"justify-between\"\n >\n {formatEnumLabel(next)}\n <ArrowRight className=\"h-4 w-4 opacity-60\" />\n </Button>\n ))\n )}\n <Button className=\"mt-auto\" onClick={() => setActivityOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Log activity\n </Button>\n </GlassCard>\n </div>\n\n {error && <p className=\"mt-4 text-sm text-status-error-text\">{error}</p>}\n\n {/* Restoration activities */}\n <Section\n title=\"Restoration activities\"\n description=\"Work logged against this project — planting, removals, surveys & engagement.\"\n action={\n <Button size=\"sm\" onClick={() => setActivityOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Log activity\n </Button>\n }\n >\n {activities.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Sprout className=\"h-10 w-10\" />}\n title=\"No activities yet\"\n description=\"Log restoration work to build this project's field record.\"\n action={\n <Button size=\"sm\" onClick={() => setActivityOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Log activity\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={activityColumns}\n rows={activities}\n rowKey={(a) => a.id}\n defaultSort={{ key: 'performedAt', dir: 'desc' }}\n />\n )}\n </Section>\n\n {/* Log activity dialog */}\n <ResponsiveDialog open={activityOpen} onOpenChange={setActivityOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Log restoration activity</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Record a piece of work performed for {project.name}.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actType\">Type</Label>\n <Select\n value={activityForm.type}\n onValueChange={(v) =>\n setActivityForm({ ...activityForm, type: v as EcoRestorationActivityType })\n }\n >\n <SelectTrigger id=\"actType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ACTIVITY_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actPerformedAt\">Performed at</Label>\n <Input\n id=\"actPerformedAt\"\n type=\"datetime-local\"\n value={activityForm.performedAt}\n onChange={(e) =>\n setActivityForm({ ...activityForm, performedAt: e.target.value })\n }\n />\n </div>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actSummary\">Summary *</Label>\n <Input\n id=\"actSummary\"\n value={activityForm.summary}\n onChange={(e) => setActivityForm({ ...activityForm, summary: e.target.value })}\n placeholder=\"Planted 500 mangrove saplings\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actDetails\">Details</Label>\n <Textarea\n id=\"actDetails\"\n value={activityForm.details}\n onChange={(e) => setActivityForm({ ...activityForm, details: e.target.value })}\n placeholder=\"Optional notes about the work.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actQuantity\">Quantity</Label>\n <Input\n id=\"actQuantity\"\n type=\"number\"\n step=\"any\"\n value={activityForm.quantity}\n onChange={(e) => setActivityForm({ ...activityForm, quantity: e.target.value })}\n placeholder=\"500\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"actUnit\">Unit</Label>\n <Input\n id=\"actUnit\"\n value={activityForm.unit}\n onChange={(e) => setActivityForm({ ...activityForm, unit: e.target.value })}\n placeholder=\"saplings\"\n />\n </div>\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setActivityOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleLogActivity} disabled={logActivity.isPending}>\n {logActivity.isPending ? 'Logging…' : 'Log activity'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import { useEcoImpactHub as e } from "../../context/EcoImpactHubContext.js";
2
2
  import { PageLayout as t } from "../../components/PageLayout.js";
3
3
  import { formatDate as n, formatNumber as r, formatRelativeTime as i } from "../../utils/formatters.js";
4
- import { StatusBadge as a } from "../../components/StatusBadge.js";
5
- import { ErrorView as o, LoadingView as s } from "../../components/StateViews.js";
6
- import { EmptyState as c } from "../../components/EmptyState.js";
4
+ import { ErrorView as a, LoadingView as o } from "../../components/StateViews.js";
5
+ import { EmptyState as s } from "../../components/EmptyState.js";
7
6
  import "../../context/index.js";
8
- import { useEcoCreateHabitatProject as l, useEcoHabitatProjects as u, useEcoSites as d } from "../../hooks/useEcoImpactHubApi.js";
7
+ import { useEcoCreateHabitatProject as c, useEcoHabitatProjects as l, useEcoSites as u } from "../../hooks/useEcoImpactHubApi.js";
9
8
  import "../../hooks/index.js";
10
- import { DataTable as f } from "../../components/DataTable.js";
9
+ import { DataTable as d } from "../../components/DataTable.js";
10
+ import { LifecycleBadge as f } from "../badges.js";
11
11
  import { FilterTabs as p } from "../../components/FilterTabs.js";
12
12
  import { useMemo as m, useState as h } from "react";
13
13
  import { Plus as g, Trees as _ } from "lucide-react";
@@ -54,10 +54,10 @@ var N = [
54
54
  endDate: ""
55
55
  }, I = (e) => e ? new Date(e).toISOString() : null;
56
56
  function L() {
57
- let { navigateTo: L } = e(), [R, z] = h("ALL"), [B, V] = h(!1), [H, U] = h(F), [W, G] = h(null), { data: K, isLoading: q, isError: J, refetch: Y } = u(R === "ALL" ? void 0 : R), X = d({ pagination: {
57
+ let { navigateTo: L } = e(), [R, z] = h("ALL"), [B, V] = h(!1), [H, U] = h(F), [W, G] = h(null), { data: K, isLoading: q, isError: J, refetch: Y } = l(R === "ALL" ? void 0 : R), X = u({ pagination: {
58
58
  limit: 100,
59
59
  offset: 0
60
- } }), Z = l(), Q = m(() => K ?? [], [K]), ne = X.data?.items ?? [], re = [
60
+ } }), Z = c(), Q = m(() => K ?? [], [K]), ne = X.data?.items ?? [], re = [
61
61
  {
62
62
  key: "name",
63
63
  header: "Project",
@@ -72,7 +72,7 @@ function L() {
72
72
  {
73
73
  key: "status",
74
74
  header: "Status",
75
- render: (e) => /* @__PURE__ */ v(a, { status: e.status })
75
+ render: (e) => /* @__PURE__ */ v(f, { status: e.status })
76
76
  },
77
77
  {
78
78
  key: "target",
@@ -94,13 +94,16 @@ function L() {
94
94
  align: "right",
95
95
  hideOnMobile: !0,
96
96
  sortValue: (e) => e.budgetAmount,
97
- render: (e) => /* @__PURE__ */ y("span", {
97
+ render: (e) => e.budgetAmount > 0 ? /* @__PURE__ */ y("span", {
98
98
  className: "tabular-nums text-text-secondary",
99
99
  children: [
100
100
  r(e.budgetAmount),
101
101
  " ",
102
102
  e.currency
103
103
  ]
104
+ }) : /* @__PURE__ */ v("span", {
105
+ className: "text-text-tertiary",
106
+ children: "—"
104
107
  })
105
108
  },
106
109
  {
@@ -145,10 +148,10 @@ function L() {
145
148
  onChange: z
146
149
  })
147
150
  }),
148
- q ? /* @__PURE__ */ v(s, { label: "Loading habitat projects…" }) : J ? /* @__PURE__ */ v(o, {
151
+ q ? /* @__PURE__ */ v(o, { label: "Loading habitat projects…" }) : J ? /* @__PURE__ */ v(a, {
149
152
  message: "Couldn't load habitat projects.",
150
153
  onRetry: () => Y()
151
- }) : Q.length === 0 ? /* @__PURE__ */ v(c, {
154
+ }) : Q.length === 0 ? /* @__PURE__ */ v(s, {
152
155
  illustration: "empty-data",
153
156
  icon: /* @__PURE__ */ v(_, { className: "h-10 w-10" }),
154
157
  title: "No habitat projects yet",
@@ -157,7 +160,7 @@ function L() {
157
160
  onClick: () => V(!0),
158
161
  children: [/* @__PURE__ */ v(g, { className: "mr-1.5 h-4 w-4" }), " New project"]
159
162
  })
160
- }) : /* @__PURE__ */ v(f, {
163
+ }) : /* @__PURE__ */ v(d, {
161
164
  columns: re,
162
165
  rows: Q,
163
166
  rowKey: (e) => e.id,
@@ -1 +1 @@
1
- {"version":3,"file":"HabitatProjectsPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/habitat/HabitatProjectsPage.tsx"],"sourcesContent":["/**\n * Habitat projects — list, filter, and create restoration & conservation projects.\n * @module ecoimpacthub/pages/habitat\n *\n * Reads `ecoListHabitatProjects` (plain array); writes `ecoCreateHabitatProject`.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Trees } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataTable, type Column } from '../../components/DataTable';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { useEcoImpactHub } from '../../context';\nimport { useEcoHabitatProjects, useEcoCreateHabitatProject, useEcoSites } from '../../hooks';\nimport { formatDate, formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport type { EcoHabitatProject, EcoHabitatProjectStatus } from '../../types';\n\nconst STATUS_FILTERS: FilterOption<EcoHabitatProjectStatus | 'ALL'>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'PLANNED', label: 'Planned' },\n { value: 'IN_PROGRESS', label: 'In progress' },\n { value: 'MONITORING', label: 'Monitoring' },\n { value: 'COMPLETED', label: 'Completed' },\n { value: 'ON_HOLD', label: 'On hold' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst NO_SITE = 'NONE';\n\ninterface CreateFormState {\n name: string;\n description: string;\n siteId: string;\n targetHectares: string;\n budgetAmount: string;\n currency: string;\n startDate: string;\n endDate: string;\n}\n\nconst EMPTY_FORM: CreateFormState = {\n name: '',\n description: '',\n siteId: NO_SITE,\n targetHectares: '',\n budgetAmount: '',\n currency: 'USD',\n startDate: '',\n endDate: '',\n};\n\nconst toIso = (value: string): string | null => (value ? new Date(value).toISOString() : null);\n\nexport function HabitatProjectsPage() {\n const { navigateTo } = useEcoImpactHub();\n const [status, setStatus] = useState<EcoHabitatProjectStatus | 'ALL'>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<CreateFormState>(EMPTY_FORM);\n const [formError, setFormError] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch } = useEcoHabitatProjects(\n status === 'ALL' ? undefined : status\n );\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const createProject = useEcoCreateHabitatProject();\n\n const rows = useMemo(() => data ?? [], [data]);\n const siteOptions = sites.data?.items ?? [];\n\n const columns: Column<EcoHabitatProject>[] = [\n {\n key: 'name',\n header: 'Project',\n render: (p) => (\n <div>\n <p className=\"font-semibold text-text-primary\">{p.name}</p>\n {p.description && (\n <p className=\"max-w-xs truncate text-xs text-text-tertiary\">{p.description}</p>\n )}\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (p) => <StatusBadge status={p.status} />,\n },\n {\n key: 'target',\n header: 'Target',\n align: 'right',\n hideOnMobile: true,\n sortValue: (p) => p.targetHectares ?? 0,\n render: (p) =>\n p.targetHectares != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(p.targetHectares)} ha\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'budget',\n header: 'Budget',\n align: 'right',\n hideOnMobile: true,\n sortValue: (p) => p.budgetAmount,\n render: (p) => (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(p.budgetAmount)} {p.currency}\n </span>\n ),\n },\n {\n key: 'timeline',\n header: 'Timeline',\n hideOnMobile: true,\n render: (p) => (\n <span className=\"text-text-secondary\">\n {formatDate(p.startDate)} – {formatDate(p.endDate)}\n </span>\n ),\n },\n {\n key: 'updated',\n header: 'Updated',\n align: 'right',\n hideOnMobile: true,\n render: (p) => <span className=\"text-text-tertiary\">{formatRelativeTime(p.updatedAt)}</span>,\n },\n ];\n\n const resetForm = () => {\n setForm(EMPTY_FORM);\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!form.name.trim()) {\n setFormError('Project name is required.');\n return;\n }\n try {\n const created = await createProject.mutateAsync({\n name: form.name.trim(),\n description: form.description.trim() || null,\n siteId: form.siteId && form.siteId !== NO_SITE ? form.siteId : null,\n targetHectares: form.targetHectares ? Number(form.targetHectares) : null,\n budgetAmount: form.budgetAmount ? Number(form.budgetAmount) : null,\n currency: form.currency.trim() || null,\n startDate: toIso(form.startDate),\n endDate: toIso(form.endDate),\n });\n setDialogOpen(false);\n resetForm();\n navigateTo(`habitat/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create project.');\n }\n };\n\n return (\n <PageLayout\n title=\"Habitat projects\"\n description=\"Restoration & conservation projects — planting, rewilding, invasive removal & monitoring.\"\n actions={\n <Button onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New project\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={STATUS_FILTERS} value={status} onChange={setStatus} />\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading habitat projects…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load habitat projects.\" onRetry={() => refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Trees className=\"h-10 w-10\" />}\n title=\"No habitat projects yet\"\n description={\n status === 'ALL'\n ? 'Create your first restoration project to plan work, budget and track activities.'\n : 'No projects match this filter.'\n }\n action={\n <Button onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New project\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(p) => p.id}\n onRowClick={(p) => navigateTo(`habitat/${p.id}`)}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n setDialogOpen(open);\n if (!open) resetForm();\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New habitat project</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Plan a restoration or conservation project and track its activities over time.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"name\">Name *</Label>\n <Input\n id=\"name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n placeholder=\"Palar mangrove restoration\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"Optional context about the project's goals.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"site\">Site</Label>\n <Select value={form.siteId} onValueChange={(v) => setForm({ ...form, siteId: v })}>\n <SelectTrigger id=\"site\">\n <SelectValue placeholder=\"No site\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={NO_SITE}>No site</SelectItem>\n {siteOptions.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"target\">Target (hectares)</Label>\n <Input\n id=\"target\"\n type=\"number\"\n min=\"0\"\n step=\"any\"\n value={form.targetHectares}\n onChange={(e) => setForm({ ...form, targetHectares: e.target.value })}\n placeholder=\"12.5\"\n />\n </div>\n <div className=\"grid gap-2 sm:grid-cols-[1fr_auto]\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"budget\">Budget</Label>\n <Input\n id=\"budget\"\n type=\"number\"\n min=\"0\"\n step=\"any\"\n value={form.budgetAmount}\n onChange={(e) => setForm({ ...form, budgetAmount: e.target.value })}\n placeholder=\"50000\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"currency\">Currency</Label>\n <Input\n id=\"currency\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n placeholder=\"USD\"\n className=\"w-24\"\n />\n </div>\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"startDate\">Start date</Label>\n <Input\n id=\"startDate\"\n type=\"date\"\n value={form.startDate}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"endDate\">End date</Label>\n <Input\n id=\"endDate\"\n type=\"date\"\n value={form.endDate}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setDialogOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createProject.isPending}>\n {createProject.isPending ? 'Creating…' : 'Create project'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HabitatProjectsPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/habitat/HabitatProjectsPage.tsx"],"sourcesContent":["/**\n * Habitat projects — list, filter, and create restoration & conservation projects.\n * @module ecoimpacthub/pages/habitat\n *\n * Reads `ecoListHabitatProjects` (plain array); writes `ecoCreateHabitatProject`.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Trees } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { DataTable, type Column } from '../../components/DataTable';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { LifecycleBadge } from '../badges';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { useEcoImpactHub } from '../../context';\nimport { useEcoHabitatProjects, useEcoCreateHabitatProject, useEcoSites } from '../../hooks';\nimport { formatDate, formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport type { EcoHabitatProject, EcoHabitatProjectStatus } from '../../types';\n\nconst STATUS_FILTERS: FilterOption<EcoHabitatProjectStatus | 'ALL'>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'PLANNED', label: 'Planned' },\n { value: 'IN_PROGRESS', label: 'In progress' },\n { value: 'MONITORING', label: 'Monitoring' },\n { value: 'COMPLETED', label: 'Completed' },\n { value: 'ON_HOLD', label: 'On hold' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst NO_SITE = 'NONE';\n\ninterface CreateFormState {\n name: string;\n description: string;\n siteId: string;\n targetHectares: string;\n budgetAmount: string;\n currency: string;\n startDate: string;\n endDate: string;\n}\n\nconst EMPTY_FORM: CreateFormState = {\n name: '',\n description: '',\n siteId: NO_SITE,\n targetHectares: '',\n budgetAmount: '',\n currency: 'USD',\n startDate: '',\n endDate: '',\n};\n\nconst toIso = (value: string): string | null => (value ? new Date(value).toISOString() : null);\n\nexport function HabitatProjectsPage() {\n const { navigateTo } = useEcoImpactHub();\n const [status, setStatus] = useState<EcoHabitatProjectStatus | 'ALL'>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<CreateFormState>(EMPTY_FORM);\n const [formError, setFormError] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch } = useEcoHabitatProjects(\n status === 'ALL' ? undefined : status\n );\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const createProject = useEcoCreateHabitatProject();\n\n const rows = useMemo(() => data ?? [], [data]);\n const siteOptions = sites.data?.items ?? [];\n\n const columns: Column<EcoHabitatProject>[] = [\n {\n key: 'name',\n header: 'Project',\n render: (p) => (\n <div>\n <p className=\"font-semibold text-text-primary\">{p.name}</p>\n {p.description && (\n <p className=\"max-w-xs truncate text-xs text-text-tertiary\">{p.description}</p>\n )}\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (p) => <LifecycleBadge status={p.status} />,\n },\n {\n key: 'target',\n header: 'Target',\n align: 'right',\n hideOnMobile: true,\n sortValue: (p) => p.targetHectares ?? 0,\n render: (p) =>\n p.targetHectares != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(p.targetHectares)} ha\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'budget',\n header: 'Budget',\n align: 'right',\n hideOnMobile: true,\n sortValue: (p) => p.budgetAmount,\n render: (p) =>\n p.budgetAmount > 0 ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(p.budgetAmount)} {p.currency}\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'timeline',\n header: 'Timeline',\n hideOnMobile: true,\n render: (p) => (\n <span className=\"text-text-secondary\">\n {formatDate(p.startDate)} – {formatDate(p.endDate)}\n </span>\n ),\n },\n {\n key: 'updated',\n header: 'Updated',\n align: 'right',\n hideOnMobile: true,\n render: (p) => <span className=\"text-text-tertiary\">{formatRelativeTime(p.updatedAt)}</span>,\n },\n ];\n\n const resetForm = () => {\n setForm(EMPTY_FORM);\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!form.name.trim()) {\n setFormError('Project name is required.');\n return;\n }\n try {\n const created = await createProject.mutateAsync({\n name: form.name.trim(),\n description: form.description.trim() || null,\n siteId: form.siteId && form.siteId !== NO_SITE ? form.siteId : null,\n targetHectares: form.targetHectares ? Number(form.targetHectares) : null,\n budgetAmount: form.budgetAmount ? Number(form.budgetAmount) : null,\n currency: form.currency.trim() || null,\n startDate: toIso(form.startDate),\n endDate: toIso(form.endDate),\n });\n setDialogOpen(false);\n resetForm();\n navigateTo(`habitat/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create project.');\n }\n };\n\n return (\n <PageLayout\n title=\"Habitat projects\"\n description=\"Restoration & conservation projects — planting, rewilding, invasive removal & monitoring.\"\n actions={\n <Button onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New project\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={STATUS_FILTERS} value={status} onChange={setStatus} />\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading habitat projects…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load habitat projects.\" onRetry={() => refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Trees className=\"h-10 w-10\" />}\n title=\"No habitat projects yet\"\n description={\n status === 'ALL'\n ? 'Create your first restoration project to plan work, budget and track activities.'\n : 'No projects match this filter.'\n }\n action={\n <Button onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New project\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(p) => p.id}\n onRowClick={(p) => navigateTo(`habitat/${p.id}`)}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n setDialogOpen(open);\n if (!open) resetForm();\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New habitat project</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Plan a restoration or conservation project and track its activities over time.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"name\">Name *</Label>\n <Input\n id=\"name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n placeholder=\"Palar mangrove restoration\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"Optional context about the project's goals.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"site\">Site</Label>\n <Select value={form.siteId} onValueChange={(v) => setForm({ ...form, siteId: v })}>\n <SelectTrigger id=\"site\">\n <SelectValue placeholder=\"No site\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={NO_SITE}>No site</SelectItem>\n {siteOptions.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"target\">Target (hectares)</Label>\n <Input\n id=\"target\"\n type=\"number\"\n min=\"0\"\n step=\"any\"\n value={form.targetHectares}\n onChange={(e) => setForm({ ...form, targetHectares: e.target.value })}\n placeholder=\"12.5\"\n />\n </div>\n <div className=\"grid gap-2 sm:grid-cols-[1fr_auto]\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"budget\">Budget</Label>\n <Input\n id=\"budget\"\n type=\"number\"\n min=\"0\"\n step=\"any\"\n value={form.budgetAmount}\n onChange={(e) => setForm({ ...form, budgetAmount: e.target.value })}\n placeholder=\"50000\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"currency\">Currency</Label>\n <Input\n id=\"currency\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n placeholder=\"USD\"\n className=\"w-24\"\n />\n </div>\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"startDate\">Start date</Label>\n <Input\n id=\"startDate\"\n type=\"date\"\n value={form.startDate}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"endDate\">End date</Label>\n <Input\n id=\"endDate\"\n type=\"date\"\n value={form.endDate}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setDialogOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createProject.isPending}>\n {createProject.isPending ? 'Creating…' : 'Create project'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { formatEnumLabel as t } from "../../utils/formatters.js";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ //#region src/ecoimpacthub/pages/habitat/LifecycleProgress.tsx
5
+ var i = [
6
+ {
7
+ status: "PLANNED",
8
+ label: "Planned"
9
+ },
10
+ {
11
+ status: "IN_PROGRESS",
12
+ label: "In progress"
13
+ },
14
+ {
15
+ status: "MONITORING",
16
+ label: "Monitoring"
17
+ },
18
+ {
19
+ status: "COMPLETED",
20
+ label: "Completed"
21
+ }
22
+ ], a = {
23
+ accent: "text-accent",
24
+ warning: "text-status-warning-text",
25
+ error: "text-status-error-text",
26
+ success: "text-status-success-text"
27
+ }, o = {
28
+ accent: "bg-accent text-action-primary-text",
29
+ warning: "bg-status-warning-bg-subtle text-status-warning-text",
30
+ error: "bg-status-error-bg-subtle text-status-error-text",
31
+ success: "bg-status-success-bg-subtle text-status-success-text"
32
+ };
33
+ function s(e) {
34
+ switch (e) {
35
+ case "PLANNED": return {
36
+ percent: 15,
37
+ tone: "accent",
38
+ stageIndex: 0
39
+ };
40
+ case "IN_PROGRESS": return {
41
+ percent: 45,
42
+ tone: "accent",
43
+ stageIndex: 1
44
+ };
45
+ case "MONITORING": return {
46
+ percent: 75,
47
+ tone: "accent",
48
+ stageIndex: 2
49
+ };
50
+ case "COMPLETED": return {
51
+ percent: 100,
52
+ tone: "success",
53
+ stageIndex: 3
54
+ };
55
+ case "ON_HOLD": return {
56
+ percent: 45,
57
+ tone: "warning",
58
+ stageIndex: 1
59
+ };
60
+ case "CANCELLED": return {
61
+ percent: 100,
62
+ tone: "error",
63
+ stageIndex: -1
64
+ };
65
+ }
66
+ }
67
+ var c = 132, l = 11, u = (c - l) / 2, d = 2 * Math.PI * u;
68
+ function f({ percent: t, tone: i, centerLabel: o }) {
69
+ let s = Math.max(0, Math.min(100, t)), f = d * (1 - s / 100);
70
+ return /* @__PURE__ */ r("div", {
71
+ className: "relative mx-auto",
72
+ style: {
73
+ width: c,
74
+ height: c
75
+ },
76
+ children: [/* @__PURE__ */ r("svg", {
77
+ width: c,
78
+ height: c,
79
+ viewBox: `0 0 ${c} ${c}`,
80
+ className: "-rotate-90",
81
+ role: "img",
82
+ "aria-label": `Lifecycle progress ${Math.round(s)} percent`,
83
+ children: [/* @__PURE__ */ n("circle", {
84
+ cx: c / 2,
85
+ cy: c / 2,
86
+ r: u,
87
+ fill: "none",
88
+ strokeWidth: l,
89
+ stroke: "currentColor",
90
+ className: "text-bg-sunken"
91
+ }), /* @__PURE__ */ n("circle", {
92
+ cx: c / 2,
93
+ cy: c / 2,
94
+ r: u,
95
+ fill: "none",
96
+ strokeWidth: l,
97
+ strokeLinecap: "round",
98
+ stroke: "currentColor",
99
+ strokeDasharray: d,
100
+ strokeDashoffset: f,
101
+ className: e("transition-[stroke-dashoffset] duration-700 ease-out", a[i])
102
+ })]
103
+ }), /* @__PURE__ */ r("div", {
104
+ className: "absolute inset-0 flex flex-col items-center justify-center",
105
+ children: [/* @__PURE__ */ n("span", {
106
+ className: e("text-2xl font-bold tabular-nums", a[i]),
107
+ children: o
108
+ }), /* @__PURE__ */ n("span", {
109
+ className: "mt-0.5 text-[10px] font-medium uppercase tracking-wide text-text-tertiary",
110
+ children: "Lifecycle"
111
+ })]
112
+ })]
113
+ });
114
+ }
115
+ function p({ stageIndex: t, tone: a }) {
116
+ return /* @__PURE__ */ n("ol", {
117
+ className: "mt-5 flex flex-wrap items-center justify-center gap-x-1.5 gap-y-2",
118
+ children: i.map((s, c) => {
119
+ let l = t >= 0 && c <= t, u = c === t, d = t >= 0 && c < t;
120
+ return /* @__PURE__ */ r("li", {
121
+ className: "flex items-center gap-1.5",
122
+ children: [
123
+ /* @__PURE__ */ n("span", {
124
+ className: e("flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold", l ? o[a] : "border border-border-seam text-text-tertiary"),
125
+ children: c + 1
126
+ }),
127
+ /* @__PURE__ */ n("span", {
128
+ className: e("text-xs", u ? "font-semibold text-text-primary" : l ? "text-text-secondary" : "text-text-tertiary"),
129
+ children: s.label
130
+ }),
131
+ c < i.length - 1 && /* @__PURE__ */ n("span", {
132
+ "aria-hidden": "true",
133
+ className: e("h-px w-4", d ? "bg-accent" : "bg-border-seam")
134
+ })
135
+ ]
136
+ }, s.status);
137
+ })
138
+ });
139
+ }
140
+ function m({ status: e }) {
141
+ let i = s(e), a = e === "CANCELLED" ? "—" : `${i.percent}%`;
142
+ return /* @__PURE__ */ r("div", {
143
+ className: "flex flex-col items-center",
144
+ children: [
145
+ /* @__PURE__ */ n(f, {
146
+ percent: i.percent,
147
+ tone: i.tone,
148
+ centerLabel: a
149
+ }),
150
+ /* @__PURE__ */ n("p", {
151
+ className: "mt-3 text-center text-sm font-semibold text-text-primary",
152
+ children: t(e)
153
+ }),
154
+ /* @__PURE__ */ n("p", {
155
+ className: "mt-0.5 text-center text-xs text-text-tertiary",
156
+ children: "Derived from lifecycle stage"
157
+ }),
158
+ /* @__PURE__ */ n(p, {
159
+ stageIndex: i.stageIndex,
160
+ tone: i.tone
161
+ })
162
+ ]
163
+ });
164
+ }
165
+ //#endregion
166
+ export { m as LifecycleProgress };
167
+
168
+ //# sourceMappingURL=LifecycleProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LifecycleProgress.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/habitat/LifecycleProgress.tsx"],"sourcesContent":["/**\n * LifecycleProgress — a lifecycle-derived progress visualization for a habitat\n * project (Boff UI 2.0).\n * @module ecoimpacthub/pages/habitat/LifecycleProgress\n *\n * The eco schema has NO numeric progress field, so this derives a progress\n * reading purely from the project's lifecycle status: a radial ring whose fill\n * maps to the stage (Planned 15% → In progress 45% → Monitoring 75% →\n * Completed 100%), paired with a compact stage stepper. On Hold and Cancelled\n * are handled off the linear path (paused-amber / closed-red). Semantic tokens\n * only, reduced-motion-safe, and it wraps cleanly at 375px.\n */\n\nimport type { EcoHabitatProjectStatus } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\n\ninterface StageDef {\n status: Extract<EcoHabitatProjectStatus, 'PLANNED' | 'IN_PROGRESS' | 'MONITORING' | 'COMPLETED'>;\n label: string;\n}\n\n/** The linear lifecycle path shown by the stepper. */\nconst STAGES: StageDef[] = [\n { status: 'PLANNED', label: 'Planned' },\n { status: 'IN_PROGRESS', label: 'In progress' },\n { status: 'MONITORING', label: 'Monitoring' },\n { status: 'COMPLETED', label: 'Completed' },\n];\n\ntype Tone = 'accent' | 'warning' | 'error' | 'success';\n\nconst toneText: Record<Tone, string> = {\n accent: 'text-accent',\n warning: 'text-status-warning-text',\n error: 'text-status-error-text',\n success: 'text-status-success-text',\n};\n\nconst toneDot: Record<Tone, string> = {\n accent: 'bg-accent text-action-primary-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n success: 'bg-status-success-bg-subtle text-status-success-text',\n};\n\ninterface LifecycleView {\n percent: number;\n tone: Tone;\n /** Index into STAGES of the active stage, or -1 when off-path (cancelled). */\n stageIndex: number;\n}\n\nfunction viewFor(status: EcoHabitatProjectStatus): LifecycleView {\n switch (status) {\n case 'PLANNED':\n return { percent: 15, tone: 'accent', stageIndex: 0 };\n case 'IN_PROGRESS':\n return { percent: 45, tone: 'accent', stageIndex: 1 };\n case 'MONITORING':\n return { percent: 75, tone: 'accent', stageIndex: 2 };\n case 'COMPLETED':\n return { percent: 100, tone: 'success', stageIndex: 3 };\n case 'ON_HOLD':\n return { percent: 45, tone: 'warning', stageIndex: 1 };\n case 'CANCELLED':\n return { percent: 100, tone: 'error', stageIndex: -1 };\n }\n}\n\nconst SIZE = 132;\nconst STROKE = 11;\nconst RADIUS = (SIZE - STROKE) / 2;\nconst CIRC = 2 * Math.PI * RADIUS;\n\nfunction Ring({\n percent,\n tone,\n centerLabel,\n}: {\n percent: number;\n tone: Tone;\n centerLabel: string;\n}) {\n const clamped = Math.max(0, Math.min(100, percent));\n const offset = CIRC * (1 - clamped / 100);\n return (\n <div className=\"relative mx-auto\" style={{ width: SIZE, height: SIZE }}>\n <svg\n width={SIZE}\n height={SIZE}\n viewBox={`0 0 ${SIZE} ${SIZE}`}\n className=\"-rotate-90\"\n role=\"img\"\n aria-label={`Lifecycle progress ${Math.round(clamped)} percent`}\n >\n <circle\n cx={SIZE / 2}\n cy={SIZE / 2}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n stroke=\"currentColor\"\n className=\"text-bg-sunken\"\n />\n <circle\n cx={SIZE / 2}\n cy={SIZE / 2}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n strokeDasharray={CIRC}\n strokeDashoffset={offset}\n className={cn('transition-[stroke-dashoffset] duration-700 ease-out', toneText[tone])}\n />\n </svg>\n <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n <span className={cn('text-2xl font-bold tabular-nums', toneText[tone])}>{centerLabel}</span>\n <span className=\"mt-0.5 text-[10px] font-medium uppercase tracking-wide text-text-tertiary\">\n Lifecycle\n </span>\n </div>\n </div>\n );\n}\n\nfunction Stepper({ stageIndex, tone }: { stageIndex: number; tone: Tone }) {\n return (\n <ol className=\"mt-5 flex flex-wrap items-center justify-center gap-x-1.5 gap-y-2\">\n {STAGES.map((stage, i) => {\n const done = stageIndex >= 0 && i <= stageIndex;\n const current = i === stageIndex;\n const connectorDone = stageIndex >= 0 && i < stageIndex;\n return (\n <li key={stage.status} className=\"flex items-center gap-1.5\">\n <span\n className={cn(\n 'flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold',\n done ? toneDot[tone] : 'border border-border-seam text-text-tertiary'\n )}\n >\n {i + 1}\n </span>\n <span\n className={cn(\n 'text-xs',\n current\n ? 'font-semibold text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-tertiary'\n )}\n >\n {stage.label}\n </span>\n {i < STAGES.length - 1 && (\n <span\n aria-hidden=\"true\"\n className={cn('h-px w-4', connectorDone ? 'bg-accent' : 'bg-border-seam')}\n />\n )}\n </li>\n );\n })}\n </ol>\n );\n}\n\nexport function LifecycleProgress({ status }: { status: EcoHabitatProjectStatus }) {\n const view = viewFor(status);\n const centerLabel = status === 'CANCELLED' ? '—' : `${view.percent}%`;\n return (\n <div className=\"flex flex-col items-center\">\n <Ring percent={view.percent} tone={view.tone} centerLabel={centerLabel} />\n <p className=\"mt-3 text-center text-sm font-semibold text-text-primary\">\n {formatEnumLabel(status)}\n </p>\n <p className=\"mt-0.5 text-center text-xs text-text-tertiary\">Derived from lifecycle stage</p>\n <Stepper stageIndex={view.stageIndex} tone={view.tone} />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -211,7 +211,13 @@ function D() {
211
211
  {
212
212
  key: "breach",
213
213
  header: "Status",
214
- render: (e) => e.isBreach ? /* @__PURE__ */ u(i, { status: "BREACH" }) : /* @__PURE__ */ u(i, { status: "OK" })
214
+ render: (e) => e.isBreach ? /* @__PURE__ */ u(i, {
215
+ status: "BREACH",
216
+ className: "uppercase"
217
+ }) : /* @__PURE__ */ u(i, {
218
+ status: "OK",
219
+ className: "uppercase"
220
+ })
215
221
  },
216
222
  {
217
223
  key: "site",
@@ -566,23 +572,30 @@ function D() {
566
572
  }),
567
573
  I.siteId && je.length > 0 && /* @__PURE__ */ d("div", {
568
574
  className: "grid gap-1.5",
569
- children: [/* @__PURE__ */ u(m, {
570
- htmlFor: "mSensor",
571
- children: "Sensor (optional)"
572
- }), /* @__PURE__ */ d(x, {
573
- value: I.sensorId,
574
- onValueChange: (e) => L({
575
- ...I,
576
- sensorId: e
575
+ children: [
576
+ /* @__PURE__ */ u(m, {
577
+ htmlFor: "mSensor",
578
+ children: "Sensor (optional)"
577
579
  }),
578
- children: [/* @__PURE__ */ u(w, {
579
- id: "mSensor",
580
- children: /* @__PURE__ */ u(T, { placeholder: "Manual entry" })
581
- }), /* @__PURE__ */ u(S, { children: je.map((e) => /* @__PURE__ */ u(C, {
582
- value: e.id,
583
- children: e.name
584
- }, e.id)) })]
585
- })]
580
+ /* @__PURE__ */ d(x, {
581
+ value: I.sensorId,
582
+ onValueChange: (e) => L({
583
+ ...I,
584
+ sensorId: e
585
+ }),
586
+ children: [/* @__PURE__ */ u(w, {
587
+ id: "mSensor",
588
+ children: /* @__PURE__ */ u(T, { placeholder: "Manual entry" })
589
+ }), /* @__PURE__ */ u(S, { children: je.map((e) => /* @__PURE__ */ u(C, {
590
+ value: e.id,
591
+ children: e.name
592
+ }, e.id)) })]
593
+ }),
594
+ /* @__PURE__ */ u("p", {
595
+ className: "text-xs text-text-tertiary",
596
+ children: "Select a specific sensor for context, or leave blank for a site-level reading."
597
+ })
598
+ ]
586
599
  }),
587
600
  Z && /* @__PURE__ */ u("p", {
588
601
  className: "text-sm text-status-error-text",