@burdenoff/microfe-ecoimpacthub 2026.720.3 → 2026.720.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.
- package/dist/_virtual/_rolldown/runtime.js +1 -1
- package/dist/ecoimpacthub/EcoImpactHubRoot.js.map +1 -1
- package/dist/ecoimpacthub/EcoImpactHubRoutes.js.map +1 -1
- package/dist/ecoimpacthub/components/DashboardCustomizerDialog.d.ts +11 -0
- package/dist/ecoimpacthub/components/DashboardCustomizerDialog.d.ts.map +1 -0
- package/dist/ecoimpacthub/components/DashboardCustomizerDialog.js +182 -0
- package/dist/ecoimpacthub/components/DashboardCustomizerDialog.js.map +1 -0
- package/dist/ecoimpacthub/components/DataTable.js.map +1 -1
- package/dist/ecoimpacthub/components/EcoDiscussion.js.map +1 -1
- package/dist/ecoimpacthub/components/EcoTags.js.map +1 -1
- package/dist/ecoimpacthub/components/EmptyState.js.map +1 -1
- package/dist/ecoimpacthub/components/EvidenceUpload.js.map +1 -1
- package/dist/ecoimpacthub/components/FeatureFlagGate.js.map +1 -1
- package/dist/ecoimpacthub/components/FileLink.js.map +1 -1
- package/dist/ecoimpacthub/components/FilterTabs.js.map +1 -1
- package/dist/ecoimpacthub/components/MetricsCard.js.map +1 -1
- package/dist/ecoimpacthub/components/MiniBar.js.map +1 -1
- package/dist/ecoimpacthub/components/PageLayout.js.map +1 -1
- package/dist/ecoimpacthub/components/Pagination.js.map +1 -1
- package/dist/ecoimpacthub/components/StateViews.js.map +1 -1
- package/dist/ecoimpacthub/components/StatusBadge.js.map +1 -1
- package/dist/ecoimpacthub/components/Tabs.js.map +1 -1
- package/dist/ecoimpacthub/context/EcoImpactHubContext.js.map +1 -1
- package/dist/ecoimpacthub/hooks/index.d.ts +1 -0
- package/dist/ecoimpacthub/hooks/index.d.ts.map +1 -1
- package/dist/ecoimpacthub/hooks/index.js +1 -0
- package/dist/ecoimpacthub/hooks/useCursorPager.js.map +1 -1
- package/dist/ecoimpacthub/hooks/useDashboardLayout.d.ts +29 -0
- package/dist/ecoimpacthub/hooks/useDashboardLayout.d.ts.map +1 -0
- package/dist/ecoimpacthub/hooks/useDashboardLayout.js +95 -0
- package/dist/ecoimpacthub/hooks/useDashboardLayout.js.map +1 -0
- package/dist/ecoimpacthub/hooks/useEcoComments.js.map +1 -1
- package/dist/ecoimpacthub/hooks/useEcoFiles.js.map +1 -1
- package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js.map +1 -1
- package/dist/ecoimpacthub/hooks/useEcoTags.js.map +1 -1
- package/dist/ecoimpacthub/pages/DashboardPage.d.ts +7 -0
- package/dist/ecoimpacthub/pages/DashboardPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/DashboardPage.js +505 -447
- package/dist/ecoimpacthub/pages/DashboardPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/_shared/ConservationBadge.js.map +1 -1
- package/dist/ecoimpacthub/pages/_shared/EvidenceThumb.js.map +1 -1
- package/dist/ecoimpacthub/pages/_shared/MethodBadge.js.map +1 -1
- package/dist/ecoimpacthub/pages/_shared/TaxonChip.js.map +1 -1
- package/dist/ecoimpacthub/pages/badges.js.map +1 -1
- package/dist/ecoimpacthub/pages/carbon/CarbonPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/carbonVisuals.js.map +1 -1
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectsPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/habitat/LifecycleProgress.js.map +1 -1
- package/dist/ecoimpacthub/pages/monitoring/MonitoringPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportsPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SitesPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/sites/siteTypeVisuals.js.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesPage.js.map +1 -1
- package/dist/ecoimpacthub/services/createSandboxAssistantTransport.d.ts.map +1 -1
- package/dist/ecoimpacthub/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/ecoimpacthub/types/index.d.ts.map +1 -1
- package/dist/ecoimpacthub/utils/cn.js.map +1 -1
- package/dist/ecoimpacthub/utils/formatters.js.map +1 -1
- package/dist/ecoimpacthub/utils/navigation.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/d3-array/src/ascending.js.map +1 -1
- package/dist/node_modules/d3-array/src/bisect.js.map +1 -1
- package/dist/node_modules/d3-array/src/bisector.js.map +1 -1
- package/dist/node_modules/d3-array/src/descending.js.map +1 -1
- package/dist/node_modules/d3-array/src/max.js.map +1 -1
- package/dist/node_modules/d3-array/src/min.js.map +1 -1
- package/dist/node_modules/d3-array/src/number.js.map +1 -1
- package/dist/node_modules/d3-array/src/quantile.js.map +1 -1
- package/dist/node_modules/d3-array/src/quickselect.js.map +1 -1
- package/dist/node_modules/d3-array/src/range.js.map +1 -1
- package/dist/node_modules/d3-array/src/sort.js.map +1 -1
- package/dist/node_modules/d3-array/src/ticks.js.map +1 -1
- package/dist/node_modules/d3-color/src/color.js.map +1 -1
- package/dist/node_modules/d3-color/src/define.js.map +1 -1
- package/dist/node_modules/d3-format/src/defaultLocale.js.map +1 -1
- package/dist/node_modules/d3-format/src/exponent.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatDecimal.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatGroup.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatNumerals.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatPrefixAuto.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatRounded.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatSpecifier.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatTrim.js.map +1 -1
- package/dist/node_modules/d3-format/src/formatTypes.js.map +1 -1
- package/dist/node_modules/d3-format/src/identity.js.map +1 -1
- package/dist/node_modules/d3-format/src/locale.js.map +1 -1
- package/dist/node_modules/d3-format/src/precisionFixed.js.map +1 -1
- package/dist/node_modules/d3-format/src/precisionPrefix.js.map +1 -1
- package/dist/node_modules/d3-format/src/precisionRound.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/array.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/basis.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/basisClosed.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/color.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/date.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/number.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/numberArray.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/object.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/piecewise.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/rgb.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/round.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/string.js.map +1 -1
- package/dist/node_modules/d3-interpolate/src/value.js.map +1 -1
- package/dist/node_modules/d3-path/src/path.js.map +1 -1
- package/dist/node_modules/d3-scale/src/band.js.map +1 -1
- package/dist/node_modules/d3-scale/src/constant.js.map +1 -1
- package/dist/node_modules/d3-scale/src/continuous.js.map +1 -1
- package/dist/node_modules/d3-scale/src/diverging.js.map +1 -1
- package/dist/node_modules/d3-scale/src/identity.js.map +1 -1
- package/dist/node_modules/d3-scale/src/init.js.map +1 -1
- package/dist/node_modules/d3-scale/src/linear.js.map +1 -1
- package/dist/node_modules/d3-scale/src/log.js.map +1 -1
- package/dist/node_modules/d3-scale/src/nice.js.map +1 -1
- package/dist/node_modules/d3-scale/src/number.js.map +1 -1
- package/dist/node_modules/d3-scale/src/ordinal.js.map +1 -1
- package/dist/node_modules/d3-scale/src/pow.js.map +1 -1
- package/dist/node_modules/d3-scale/src/quantile.js.map +1 -1
- package/dist/node_modules/d3-scale/src/quantize.js.map +1 -1
- package/dist/node_modules/d3-scale/src/radial.js.map +1 -1
- package/dist/node_modules/d3-scale/src/sequential.js.map +1 -1
- package/dist/node_modules/d3-scale/src/sequentialQuantile.js.map +1 -1
- package/dist/node_modules/d3-scale/src/symlog.js.map +1 -1
- package/dist/node_modules/d3-scale/src/threshold.js.map +1 -1
- package/dist/node_modules/d3-scale/src/tickFormat.js.map +1 -1
- package/dist/node_modules/d3-scale/src/time.js.map +1 -1
- package/dist/node_modules/d3-scale/src/utcTime.js.map +1 -1
- package/dist/node_modules/d3-shape/src/area.js.map +1 -1
- package/dist/node_modules/d3-shape/src/array.js.map +1 -1
- package/dist/node_modules/d3-shape/src/constant.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/basis.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/basisClosed.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/basisOpen.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/bump.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/linear.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/linearClosed.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/monotone.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/natural.js.map +1 -1
- package/dist/node_modules/d3-shape/src/curve/step.js.map +1 -1
- package/dist/node_modules/d3-shape/src/line.js.map +1 -1
- package/dist/node_modules/d3-shape/src/noop.js.map +1 -1
- package/dist/node_modules/d3-shape/src/offset/expand.js.map +1 -1
- package/dist/node_modules/d3-shape/src/offset/none.js.map +1 -1
- package/dist/node_modules/d3-shape/src/offset/silhouette.js.map +1 -1
- package/dist/node_modules/d3-shape/src/offset/wiggle.js.map +1 -1
- package/dist/node_modules/d3-shape/src/order/none.js.map +1 -1
- package/dist/node_modules/d3-shape/src/path.js.map +1 -1
- package/dist/node_modules/d3-shape/src/point.js.map +1 -1
- package/dist/node_modules/d3-shape/src/stack.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/circle.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/cross.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/diamond.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/square.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/star.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/triangle.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol/wye.js.map +1 -1
- package/dist/node_modules/d3-shape/src/symbol.js.map +1 -1
- package/dist/node_modules/d3-time/src/day.js.map +1 -1
- package/dist/node_modules/d3-time/src/hour.js.map +1 -1
- package/dist/node_modules/d3-time/src/interval.js.map +1 -1
- package/dist/node_modules/d3-time/src/millisecond.js.map +1 -1
- package/dist/node_modules/d3-time/src/minute.js.map +1 -1
- package/dist/node_modules/d3-time/src/month.js.map +1 -1
- package/dist/node_modules/d3-time/src/second.js.map +1 -1
- package/dist/node_modules/d3-time/src/ticks.js.map +1 -1
- package/dist/node_modules/d3-time/src/week.js.map +1 -1
- package/dist/node_modules/d3-time/src/year.js.map +1 -1
- package/dist/node_modules/d3-time-format/src/defaultLocale.js.map +1 -1
- package/dist/node_modules/d3-time-format/src/locale.js.map +1 -1
- package/dist/node_modules/decimal.js-light/decimal.js.map +1 -1
- package/dist/node_modules/eventemitter3/index.js.map +1 -1
- package/dist/node_modules/fast-equals/dist/es/index.js.map +1 -1
- package/dist/node_modules/internmap/src/index.js.map +1 -1
- package/dist/node_modules/lodash/_DataView.js.map +1 -1
- package/dist/node_modules/lodash/_Hash.js.map +1 -1
- package/dist/node_modules/lodash/_ListCache.js.map +1 -1
- package/dist/node_modules/lodash/_Map.js.map +1 -1
- package/dist/node_modules/lodash/_MapCache.js.map +1 -1
- package/dist/node_modules/lodash/_Promise.js.map +1 -1
- package/dist/node_modules/lodash/_Set.js.map +1 -1
- package/dist/node_modules/lodash/_SetCache.js.map +1 -1
- package/dist/node_modules/lodash/_Stack.js +2 -1
- package/dist/node_modules/lodash/_Stack.js.map +1 -1
- package/dist/node_modules/lodash/_Symbol.js.map +1 -1
- package/dist/node_modules/lodash/_Uint8Array.js.map +1 -1
- package/dist/node_modules/lodash/_WeakMap.js.map +1 -1
- package/dist/node_modules/lodash/_apply.js.map +1 -1
- package/dist/node_modules/lodash/_arrayEvery.js.map +1 -1
- package/dist/node_modules/lodash/_arrayFilter.js.map +1 -1
- package/dist/node_modules/lodash/_arrayIncludes.js.map +1 -1
- package/dist/node_modules/lodash/_arrayIncludesWith.js.map +1 -1
- package/dist/node_modules/lodash/_arrayLikeKeys.js.map +1 -1
- package/dist/node_modules/lodash/_arrayMap.js.map +1 -1
- package/dist/node_modules/lodash/_arrayPush.js.map +1 -1
- package/dist/node_modules/lodash/_arraySome.js.map +1 -1
- package/dist/node_modules/lodash/_asciiToArray.js.map +1 -1
- package/dist/node_modules/lodash/_assocIndexOf.js.map +1 -1
- package/dist/node_modules/lodash/_baseAssignValue.js.map +1 -1
- package/dist/node_modules/lodash/_baseEach.js.map +1 -1
- package/dist/node_modules/lodash/_baseEvery.js.map +1 -1
- package/dist/node_modules/lodash/_baseExtremum.js.map +1 -1
- package/dist/node_modules/lodash/_baseFindIndex.js.map +1 -1
- package/dist/node_modules/lodash/_baseFlatten.js.map +1 -1
- package/dist/node_modules/lodash/_baseFor.js.map +1 -1
- package/dist/node_modules/lodash/_baseForOwn.js.map +1 -1
- package/dist/node_modules/lodash/_baseGet.js.map +1 -1
- package/dist/node_modules/lodash/_baseGetAllKeys.js.map +1 -1
- package/dist/node_modules/lodash/_baseGetTag.js.map +1 -1
- package/dist/node_modules/lodash/_baseGt.js.map +1 -1
- package/dist/node_modules/lodash/_baseHasIn.js.map +1 -1
- package/dist/node_modules/lodash/_baseIndexOf.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsArguments.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsEqual.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsEqualDeep.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsMatch.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsNaN.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsNative.js.map +1 -1
- package/dist/node_modules/lodash/_baseIsTypedArray.js.map +1 -1
- package/dist/node_modules/lodash/_baseIteratee.js.map +1 -1
- package/dist/node_modules/lodash/_baseKeys.js.map +1 -1
- package/dist/node_modules/lodash/_baseLt.js.map +1 -1
- package/dist/node_modules/lodash/_baseMap.js.map +1 -1
- package/dist/node_modules/lodash/_baseMatches.js.map +1 -1
- package/dist/node_modules/lodash/_baseMatchesProperty.js.map +1 -1
- package/dist/node_modules/lodash/_baseOrderBy.js.map +1 -1
- package/dist/node_modules/lodash/_baseProperty.js.map +1 -1
- package/dist/node_modules/lodash/_basePropertyDeep.js.map +1 -1
- package/dist/node_modules/lodash/_baseRange.js.map +1 -1
- package/dist/node_modules/lodash/_baseRest.js.map +1 -1
- package/dist/node_modules/lodash/_baseSetToString.js.map +1 -1
- package/dist/node_modules/lodash/_baseSlice.js.map +1 -1
- package/dist/node_modules/lodash/_baseSome.js.map +1 -1
- package/dist/node_modules/lodash/_baseSortBy.js.map +1 -1
- package/dist/node_modules/lodash/_baseTimes.js.map +1 -1
- package/dist/node_modules/lodash/_baseToString.js.map +1 -1
- package/dist/node_modules/lodash/_baseTrim.js.map +1 -1
- package/dist/node_modules/lodash/_baseUnary.js.map +1 -1
- package/dist/node_modules/lodash/_baseUniq.js.map +1 -1
- package/dist/node_modules/lodash/_cacheHas.js.map +1 -1
- package/dist/node_modules/lodash/_castPath.js.map +1 -1
- package/dist/node_modules/lodash/_castSlice.js.map +1 -1
- package/dist/node_modules/lodash/_compareAscending.js.map +1 -1
- package/dist/node_modules/lodash/_compareMultiple.js.map +1 -1
- package/dist/node_modules/lodash/_coreJsData.js.map +1 -1
- package/dist/node_modules/lodash/_createBaseEach.js.map +1 -1
- package/dist/node_modules/lodash/_createBaseFor.js.map +1 -1
- package/dist/node_modules/lodash/_createCaseFirst.js.map +1 -1
- package/dist/node_modules/lodash/_createFind.js.map +1 -1
- package/dist/node_modules/lodash/_createRange.js.map +1 -1
- package/dist/node_modules/lodash/_createSet.js.map +1 -1
- package/dist/node_modules/lodash/_defineProperty.js.map +1 -1
- package/dist/node_modules/lodash/_equalArrays.js.map +1 -1
- package/dist/node_modules/lodash/_equalByTag.js.map +1 -1
- package/dist/node_modules/lodash/_equalObjects.js.map +1 -1
- package/dist/node_modules/lodash/_freeGlobal.js.map +1 -1
- package/dist/node_modules/lodash/_getAllKeys.js.map +1 -1
- package/dist/node_modules/lodash/_getMapData.js.map +1 -1
- package/dist/node_modules/lodash/_getMatchData.js.map +1 -1
- package/dist/node_modules/lodash/_getNative.js.map +1 -1
- package/dist/node_modules/lodash/_getPrototype.js.map +1 -1
- package/dist/node_modules/lodash/_getRawTag.js.map +1 -1
- package/dist/node_modules/lodash/_getSymbols.js.map +1 -1
- package/dist/node_modules/lodash/_getTag.js.map +1 -1
- package/dist/node_modules/lodash/_getValue.js.map +1 -1
- package/dist/node_modules/lodash/_hasPath.js.map +1 -1
- package/dist/node_modules/lodash/_hasUnicode.js.map +1 -1
- package/dist/node_modules/lodash/_hashClear.js.map +1 -1
- package/dist/node_modules/lodash/_hashDelete.js.map +1 -1
- package/dist/node_modules/lodash/_hashGet.js.map +1 -1
- package/dist/node_modules/lodash/_hashHas.js.map +1 -1
- package/dist/node_modules/lodash/_hashSet.js.map +1 -1
- package/dist/node_modules/lodash/_isFlattenable.js.map +1 -1
- package/dist/node_modules/lodash/_isIndex.js.map +1 -1
- package/dist/node_modules/lodash/_isIterateeCall.js.map +1 -1
- package/dist/node_modules/lodash/_isKey.js.map +1 -1
- package/dist/node_modules/lodash/_isKeyable.js.map +1 -1
- package/dist/node_modules/lodash/_isMasked.js.map +1 -1
- package/dist/node_modules/lodash/_isPrototype.js.map +1 -1
- package/dist/node_modules/lodash/_isStrictComparable.js.map +1 -1
- package/dist/node_modules/lodash/_listCacheClear.js.map +1 -1
- package/dist/node_modules/lodash/_listCacheDelete.js.map +1 -1
- package/dist/node_modules/lodash/_listCacheGet.js.map +1 -1
- package/dist/node_modules/lodash/_listCacheHas.js.map +1 -1
- package/dist/node_modules/lodash/_listCacheSet.js.map +1 -1
- package/dist/node_modules/lodash/_mapCacheClear.js.map +1 -1
- package/dist/node_modules/lodash/_mapCacheDelete.js.map +1 -1
- package/dist/node_modules/lodash/_mapCacheGet.js.map +1 -1
- package/dist/node_modules/lodash/_mapCacheHas.js.map +1 -1
- package/dist/node_modules/lodash/_mapCacheSet.js.map +1 -1
- package/dist/node_modules/lodash/_mapToArray.js.map +1 -1
- package/dist/node_modules/lodash/_matchesStrictComparable.js.map +1 -1
- package/dist/node_modules/lodash/_memoizeCapped.js.map +1 -1
- package/dist/node_modules/lodash/_nativeCreate.js.map +1 -1
- package/dist/node_modules/lodash/_nativeKeys.js.map +1 -1
- package/dist/node_modules/lodash/_nodeUtil.js.map +1 -1
- package/dist/node_modules/lodash/_objectToString.js.map +1 -1
- package/dist/node_modules/lodash/_overArg.js.map +1 -1
- package/dist/node_modules/lodash/_overRest.js.map +1 -1
- package/dist/node_modules/lodash/_root.js.map +1 -1
- package/dist/node_modules/lodash/_setCacheAdd.js.map +1 -1
- package/dist/node_modules/lodash/_setCacheHas.js.map +1 -1
- package/dist/node_modules/lodash/_setToArray.js.map +1 -1
- package/dist/node_modules/lodash/_setToString.js.map +1 -1
- package/dist/node_modules/lodash/_shortOut.js.map +1 -1
- package/dist/node_modules/lodash/_stackClear.js.map +1 -1
- package/dist/node_modules/lodash/_stackDelete.js.map +1 -1
- package/dist/node_modules/lodash/_stackGet.js.map +1 -1
- package/dist/node_modules/lodash/_stackHas.js.map +1 -1
- package/dist/node_modules/lodash/_stackSet.js.map +1 -1
- package/dist/node_modules/lodash/_strictIndexOf.js.map +1 -1
- package/dist/node_modules/lodash/_stringToArray.js.map +1 -1
- package/dist/node_modules/lodash/_stringToPath.js.map +1 -1
- package/dist/node_modules/lodash/_toKey.js.map +1 -1
- package/dist/node_modules/lodash/_toSource.js.map +1 -1
- package/dist/node_modules/lodash/_trimmedEndIndex.js.map +1 -1
- package/dist/node_modules/lodash/_unicodeToArray.js.map +1 -1
- package/dist/node_modules/lodash/constant.js.map +1 -1
- package/dist/node_modules/lodash/debounce.js.map +1 -1
- package/dist/node_modules/lodash/eq.js.map +1 -1
- package/dist/node_modules/lodash/every.js.map +1 -1
- package/dist/node_modules/lodash/find.js.map +1 -1
- package/dist/node_modules/lodash/findIndex.js.map +1 -1
- package/dist/node_modules/lodash/flatMap.js.map +1 -1
- package/dist/node_modules/lodash/get.js.map +1 -1
- package/dist/node_modules/lodash/hasIn.js.map +1 -1
- package/dist/node_modules/lodash/identity.js.map +1 -1
- package/dist/node_modules/lodash/isArguments.js.map +1 -1
- package/dist/node_modules/lodash/isArray.js.map +1 -1
- package/dist/node_modules/lodash/isArrayLike.js.map +1 -1
- package/dist/node_modules/lodash/isBoolean.js.map +1 -1
- package/dist/node_modules/lodash/isBuffer.js.map +1 -1
- package/dist/node_modules/lodash/isEqual.js.map +1 -1
- package/dist/node_modules/lodash/isFunction.js.map +1 -1
- package/dist/node_modules/lodash/isLength.js.map +1 -1
- package/dist/node_modules/lodash/isNaN.js.map +1 -1
- package/dist/node_modules/lodash/isNil.js.map +1 -1
- package/dist/node_modules/lodash/isNumber.js.map +1 -1
- package/dist/node_modules/lodash/isObject.js.map +1 -1
- package/dist/node_modules/lodash/isObjectLike.js.map +1 -1
- package/dist/node_modules/lodash/isPlainObject.js.map +1 -1
- package/dist/node_modules/lodash/isString.js.map +1 -1
- package/dist/node_modules/lodash/isSymbol.js.map +1 -1
- package/dist/node_modules/lodash/isTypedArray.js.map +1 -1
- package/dist/node_modules/lodash/keys.js.map +1 -1
- package/dist/node_modules/lodash/last.js.map +1 -1
- package/dist/node_modules/lodash/map.js.map +1 -1
- package/dist/node_modules/lodash/mapValues.js.map +1 -1
- package/dist/node_modules/lodash/max.js.map +1 -1
- package/dist/node_modules/lodash/memoize.js.map +1 -1
- package/dist/node_modules/lodash/min.js.map +1 -1
- package/dist/node_modules/lodash/noop.js.map +1 -1
- package/dist/node_modules/lodash/now.js.map +1 -1
- package/dist/node_modules/lodash/property.js.map +1 -1
- package/dist/node_modules/lodash/range.js.map +1 -1
- package/dist/node_modules/lodash/some.js.map +1 -1
- package/dist/node_modules/lodash/sortBy.js.map +1 -1
- package/dist/node_modules/lodash/stubArray.js.map +1 -1
- package/dist/node_modules/lodash/stubFalse.js.map +1 -1
- package/dist/node_modules/lodash/throttle.js.map +1 -1
- package/dist/node_modules/lodash/toFinite.js.map +1 -1
- package/dist/node_modules/lodash/toInteger.js.map +1 -1
- package/dist/node_modules/lodash/toNumber.js.map +1 -1
- package/dist/node_modules/lodash/toString.js.map +1 -1
- package/dist/node_modules/lodash/uniqBy.js.map +1 -1
- package/dist/node_modules/lodash/upperFirst.js.map +1 -1
- package/dist/node_modules/object-assign/index.js.map +1 -1
- package/dist/node_modules/prop-types/checkPropTypes.js.map +1 -1
- package/dist/node_modules/prop-types/factoryWithThrowingShims.js.map +1 -1
- package/dist/node_modules/prop-types/factoryWithTypeCheckers.js.map +1 -1
- package/dist/node_modules/prop-types/index.js.map +1 -1
- package/dist/node_modules/prop-types/lib/ReactPropTypesSecret.js.map +1 -1
- package/dist/node_modules/prop-types/lib/has.js.map +1 -1
- package/dist/node_modules/prop-types/node_modules/react-is/cjs/react-is.development.js.map +1 -1
- package/dist/node_modules/prop-types/node_modules/react-is/cjs/react-is.production.min.js.map +1 -1
- package/dist/node_modules/prop-types/node_modules/react-is/index.js.map +1 -1
- package/dist/node_modules/react-is/cjs/react-is.development.js.map +1 -1
- package/dist/node_modules/react-is/cjs/react-is.production.min.js.map +1 -1
- package/dist/node_modules/react-is/index.js.map +1 -1
- package/dist/node_modules/react-smooth/es6/Animate.js +2 -2
- package/dist/node_modules/react-smooth/es6/Animate.js.map +1 -1
- package/dist/node_modules/react-smooth/es6/AnimateManager.js.map +1 -1
- package/dist/node_modules/react-smooth/es6/configUpdate.js.map +1 -1
- package/dist/node_modules/react-smooth/es6/easing.js.map +1 -1
- package/dist/node_modules/react-smooth/es6/setRafTimeout.js.map +1 -1
- package/dist/node_modules/react-smooth/es6/util.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/Bar.js +9 -9
- package/dist/node_modules/recharts/es6/cartesian/Bar.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/Brush.js +12 -12
- package/dist/node_modules/recharts/es6/cartesian/Brush.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/CartesianAxis.js +7 -7
- package/dist/node_modules/recharts/es6/cartesian/CartesianAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/CartesianGrid.js +13 -13
- package/dist/node_modules/recharts/es6/cartesian/CartesianGrid.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ErrorBar.js +4 -4
- package/dist/node_modules/recharts/es6/cartesian/ErrorBar.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceArea.js +3 -3
- package/dist/node_modules/recharts/es6/cartesian/ReferenceArea.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceDot.js +3 -3
- package/dist/node_modules/recharts/es6/cartesian/ReferenceDot.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceLine.js +4 -4
- package/dist/node_modules/recharts/es6/cartesian/ReferenceLine.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/XAxis.js +3 -3
- package/dist/node_modules/recharts/es6/cartesian/XAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/YAxis.js +3 -3
- package/dist/node_modules/recharts/es6/cartesian/YAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/getEquidistantTicks.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/getTicks.js.map +1 -1
- package/dist/node_modules/recharts/es6/chart/AccessibilityManager.js +1 -1
- package/dist/node_modules/recharts/es6/chart/AccessibilityManager.js.map +1 -1
- package/dist/node_modules/recharts/es6/chart/BarChart.js.map +1 -1
- package/dist/node_modules/recharts/es6/chart/generateCategoricalChart.js +22 -22
- package/dist/node_modules/recharts/es6/chart/generateCategoricalChart.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/Cell.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/Cursor.js +1 -1
- package/dist/node_modules/recharts/es6/component/Cursor.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/DefaultLegendContent.js +11 -11
- package/dist/node_modules/recharts/es6/component/DefaultLegendContent.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/DefaultTooltipContent.js +6 -6
- package/dist/node_modules/recharts/es6/component/DefaultTooltipContent.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/Label.js +14 -14
- package/dist/node_modules/recharts/es6/component/Label.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/LabelList.js +6 -6
- package/dist/node_modules/recharts/es6/component/LabelList.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/Legend.js +5 -5
- package/dist/node_modules/recharts/es6/component/Legend.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/ResponsiveContainer.js +3 -3
- package/dist/node_modules/recharts/es6/component/ResponsiveContainer.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/Text.js +2 -2
- package/dist/node_modules/recharts/es6/component/Text.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/Tooltip.js +3 -3
- package/dist/node_modules/recharts/es6/component/Tooltip.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/TooltipBoundingBox.js +2 -2
- package/dist/node_modules/recharts/es6/component/TooltipBoundingBox.js.map +1 -1
- package/dist/node_modules/recharts/es6/container/Layer.js +2 -2
- package/dist/node_modules/recharts/es6/container/Layer.js.map +1 -1
- package/dist/node_modules/recharts/es6/container/Surface.js +2 -2
- package/dist/node_modules/recharts/es6/container/Surface.js.map +1 -1
- package/dist/node_modules/recharts/es6/context/chartLayoutContext.js +2 -2
- package/dist/node_modules/recharts/es6/context/chartLayoutContext.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Cross.js +1 -1
- package/dist/node_modules/recharts/es6/shape/Cross.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Curve.js +1 -1
- package/dist/node_modules/recharts/es6/shape/Curve.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Dot.js +1 -1
- package/dist/node_modules/recharts/es6/shape/Dot.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Rectangle.js +4 -4
- package/dist/node_modules/recharts/es6/shape/Rectangle.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Sector.js +1 -1
- package/dist/node_modules/recharts/es6/shape/Sector.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Symbols.js +1 -1
- package/dist/node_modules/recharts/es6/shape/Symbols.js.map +1 -1
- package/dist/node_modules/recharts/es6/shape/Trapezoid.js +4 -4
- package/dist/node_modules/recharts/es6/shape/Trapezoid.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/ActiveShapeUtils.js +9 -9
- package/dist/node_modules/recharts/es6/util/ActiveShapeUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/BarUtils.js +1 -1
- package/dist/node_modules/recharts/es6/util/BarUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/CartesianUtils.js +1 -1
- package/dist/node_modules/recharts/es6/util/CartesianUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/ChartUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/CssPrefixUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/DOMUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/DataUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/DetectReferenceElementsDomain.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/Events.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/Global.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/IfOverflowMatches.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/LogUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/PolarUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/ReactUtils.js +1 -1
- package/dist/node_modules/recharts/es6/util/ReactUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/ReduceCSSCalc.js +1 -1
- package/dist/node_modules/recharts/es6/util/ReduceCSSCalc.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/ShallowEqual.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/TickUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/calculateViewBox.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/cursor/getCursorPoints.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/cursor/getCursorRectangle.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/cursor/getRadialCursorPoints.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/getEveryNthWithCondition.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/getLegendProps.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/isDomainSpecifiedByUser.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/payload/getUniqPayload.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/tooltip/translate.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/types.js +1 -1
- package/dist/node_modules/recharts/es6/util/types.js.map +1 -1
- package/dist/node_modules/recharts-scale/es6/getNiceTickValues.js.map +1 -1
- package/dist/node_modules/recharts-scale/es6/util/arithmetic.js.map +1 -1
- package/dist/node_modules/recharts-scale/es6/util/utils.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +1 -1
- package/package.json +2 -2
|
@@ -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` (Connection: items + pageInfo); 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?.items ?? [], [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":";;;;;;;;;;;;;;;;AAqCA,IAAM,IAAkE;CACtE;EAAE,OAAO;EAAO,OAAO;EAAO;CAC9B;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAe,OAAO;EAAe;CAC9C;EAAE,OAAO;EAAc,OAAO;EAAc;CAC5C;EAAE,OAAO;EAAa,OAAO;EAAa;CAC1C;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAa,OAAO;EAAa;CAC3C,EAEK,IAAU,QAaV,IAA8B;CAClC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,gBAAgB;CAChB,cAAc;CACd,UAAU;CACV,WAAW;CACX,SAAS;CACV,EAEK,KAAS,MAAkC,IAAQ,IAAI,KAAK,EAAM,CAAC,aAAa,GAAG;AAEzF,SAAgB,IAAsB;CACpC,IAAM,EAAE,kBAAe,GAAiB,EAClC,CAAC,GAAQ,KAAa,EAA0C,MAAM,EACtE,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAM,KAAW,EAA0B,EAAW,EACvD,CAAC,GAAW,KAAgB,EAAwB,KAAK,EAEzD,EAAE,SAAM,cAAW,YAAS,eAAY,EAC5C,MAAW,QAAQ,KAAA,IAAY,EAChC,EACK,IAAQ,EAAY,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAC9D,IAAgB,GAA4B,EAE5C,IAAO,QAAc,GAAM,SAAS,EAAE,EAAE,CAAC,EAAK,CAAC,EAC/C,KAAc,EAAM,MAAM,SAAS,EAAE,EAErC,KAAuC;EAC3C;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MACP,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;IAAG,WAAU;cAAmC,EAAE;IAAS,CAAA,EAC1D,EAAE,eACD,kBAAC,KAAD;IAAG,WAAU;cAAgD,EAAE;IAAgB,CAAA,CAE7E,EAAA,CAAA;GAET;EACD;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MAAM,kBAAC,GAAD,EAAgB,QAAQ,EAAE,QAAU,CAAA;GACpD;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,cAAc;GACd,YAAY,MAAM,EAAE,kBAAkB;GACtC,SAAS,MACP,EAAE,kBAAkB,OAKlB,kBAAC,QAAD;IAAM,WAAU;cAAqB;IAAQ,CAAA,GAJ7C,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACG,EAAa,EAAE,eAAe,EAAC,MAC3B;;GAIZ;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,cAAc;GACd,YAAY,MAAM,EAAE;GACpB,SAAS,MACP,EAAE,eAAe,IACf,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACG,EAAa,EAAE,aAAa;KAAC;KAAE,EAAE;KAC7B;QAEP,kBAAC,QAAD;IAAM,WAAU;cAAqB;IAAQ,CAAA;GAElD;EACD;GACE,KAAK;GACL,QAAQ;GACR,cAAc;GACd,SAAS,MACP,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACG,EAAW,EAAE,UAAU;KAAC;KAAI,EAAW,EAAE,QAAQ;KAC7C;;GAEV;EACD;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,cAAc;GACd,SAAS,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAsB,EAAmB,EAAE,UAAU;IAAQ,CAAA;GAC7F;EACF,EAEK,UAAkB;AAEtB,EADA,EAAQ,EAAW,EACnB,EAAa,KAAK;;AA2BpB,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD;GAAQ,eAAe,EAAc,GAAK;aAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,eAC5B;;YANb;GASE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAY,SAAS;KAAgB,OAAO;KAAQ,UAAU;KAAa,CAAA;IACvE,CAAA;GAEL,IACC,kBAAC,GAAD,EAAa,OAAM,6BAA8B,CAAA,GAC/C,IACF,kBAAC,GAAD;IAAW,SAAQ;IAAkC,eAAe,GAAS;IAAI,CAAA,GAC/E,EAAK,WAAW,IAClB,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAO,WAAU,aAAc,CAAA;IACrC,OAAM;IACN,aACE,MAAW,QACP,qFACA;IAEN,QACE,kBAAC,GAAD;KAAQ,eAAe,EAAc,GAAK;eAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,eAC5B;;IAEX,CAAA,GAEF,kBAAC,GAAD;IACW;IACH;IACN,SAAS,MAAM,EAAE;IACjB,aAAa,MAAM,EAAW,WAAW,EAAE,KAAK;IAChD,CAAA;GAGJ,kBAAC,GAAD;IACE,MAAM;IACN,eAAe,MAAS;AAEtB,KADA,EAAc,EAAK,EACd,KAAM,GAAW;;cAGxB,kBAAC,GAAD,EAAA,UAAA;KACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,uBAA2C,CAAA,EAClE,kBAAC,IAAD,EAAA,UAA6B,kFAEC,CAAA,CACP,EAAA,CAAA;KAEzB,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAO;SAAc,CAAA,EACpC,kBAAC,GAAD;SACE,IAAG;SACH,OAAO,EAAK;SACZ,WAAW,MAAM,EAAQ;UAAE,GAAG;UAAM,MAAM,EAAE,OAAO;UAAO,CAAC;SAC3D,aAAY;SACZ,CAAA,CACE;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAc;SAAmB,CAAA,EAChD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO,EAAK;SACZ,WAAW,MAAM,EAAQ;UAAE,GAAG;UAAM,aAAa,EAAE,OAAO;UAAO,CAAC;SAClE,aAAY;SACZ,MAAM;SACN,CAAA,CACE;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAO;SAAY,CAAA,EAClC,kBAAC,GAAD;SAAQ,OAAO,EAAK;SAAQ,gBAAgB,MAAM,EAAQ;UAAE,GAAG;UAAM,QAAQ;UAAG,CAAC;mBAAjF,CACE,kBAAC,GAAD;UAAe,IAAG;oBAChB,kBAAC,GAAD,EAAa,aAAY,WAAY,CAAA;UACvB,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;UAAY,OAAO;oBAAS;UAAoB,CAAA,EAC/C,GAAY,KAAK,MAChB,kBAAC,GAAD;UAAuB,OAAO,EAAE;oBAC7B,EAAE;UACQ,EAFI,EAAE,GAEN,CACb,CACY,EAAA,CAAA,CACT;WACL;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAS;UAAyB,CAAA,EACjD,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,KAAI;UACJ,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAQ;WAAE,GAAG;WAAM,gBAAgB,EAAE,OAAO;WAAO,CAAC;UACrE,aAAY;UACZ,CAAA,CACE;YACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WAAO,SAAQ;qBAAS;WAAc,CAAA,EACtC,kBAAC,GAAD;WACE,IAAG;WACH,MAAK;WACL,KAAI;WACJ,MAAK;WACL,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,cAAc,EAAE,OAAO;YAAO,CAAC;WACnE,aAAY;WACZ,CAAA,CACE;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WAAO,SAAQ;qBAAW;WAAgB,CAAA,EAC1C,kBAAC,GAAD;WACE,IAAG;WACH,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,UAAU,EAAE,OAAO;YAAO,CAAC;WAC/D,aAAY;WACZ,WAAU;WACV,CAAA,CACE;YACF;WACF;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAY;UAAkB,CAAA,EAC7C,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAQ;WAAE,GAAG;WAAM,WAAW,EAAE,OAAO;WAAO,CAAC;UAChE,CAAA,CACE;YACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAU;UAAgB,CAAA,EACzC,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAQ;WAAE,GAAG;WAAM,SAAS,EAAE,OAAO;WAAO,CAAC;UAC9D,CAAA,CACE;WACF;;OAEL,KAAa,kBAAC,KAAD;QAAG,WAAU;kBAAkC;QAAc,CAAA;OACvE;;KAEN,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAQ,SAAQ;MAAU,eAAe,EAAc,GAAM;gBAAE;MAEtD,CAAA,EACT,kBAAC,GAAD;MAAQ,SA5LG,YAAY;AAC/B,WAAI,CAAC,EAAK,KAAK,MAAM,EAAE;AACrB,UAAa,4BAA4B;AACzC;;AAEF,WAAI;QACF,IAAM,IAAU,MAAM,EAAc,YAAY;SAC9C,MAAM,EAAK,KAAK,MAAM;SACtB,aAAa,EAAK,YAAY,MAAM,IAAI;SACxC,QAAQ,EAAK,UAAU,EAAK,WAAW,IAAU,EAAK,SAAS;SAC/D,gBAAgB,EAAK,iBAAiB,OAAO,EAAK,eAAe,GAAG;SACpE,cAAc,EAAK,eAAe,OAAO,EAAK,aAAa,GAAG;SAC9D,UAAU,EAAK,SAAS,MAAM,IAAI;SAClC,WAAW,EAAM,EAAK,UAAU;SAChC,SAAS,EAAM,EAAK,QAAQ;SAC7B,CAAC;AAGF,QAFA,EAAc,GAAM,EACpB,GAAW,EACX,EAAW,WAAW,EAAQ,KAAK;gBAC5B,GAAK;AACZ,UAAa,aAAe,QAAQ,EAAI,UAAU,4BAA4B;;;MAwKzC,UAAU,EAAc;gBACpD,EAAc,YAAY,cAAc;MAClC,CAAA,CACc,EAAA,CAAA;KACD,EAAA,CAAA;IACT,CAAA;GACR"}
|
|
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` (Connection: items + pageInfo); 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?.items ?? [], [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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"}
|
|
@@ -1 +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":";;;;AAuBA,IAAM,IAAqB;CACzB;EAAE,QAAQ;EAAW,OAAO;
|
|
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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MonitoringPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/monitoring/MonitoringPage.tsx"],"sourcesContent":["/**\n * Monitoring — a cross-site hub for environmental measurements, sensors &\n * alert thresholds.\n * @module ecoimpacthub/pages/monitoring\n *\n * Reads `ecoListMeasurements`, `ecoListSensors`, `ecoListThresholds` (and\n * `ecoListSites` for name lookups); writes `ecoRecordMeasurement`,\n * `ecoCreateSensor`, `ecoSetSensorStatus`, `ecoCreateThreshold`,\n * `ecoSetThresholdActive`.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Activity, Radio, Gauge } from 'lucide-react';\nimport {\n Button,\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 { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataTable, type Column } from '../../components/DataTable';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useEcoSites,\n useEcoMeasurements,\n useEcoRecordMeasurement,\n useEcoSensors,\n useEcoCreateSensor,\n useEcoSetSensorStatus,\n useEcoThresholds,\n useEcoCreateThreshold,\n useEcoSetThresholdActive,\n useCursorPager,\n} from '../../hooks';\nimport { formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport type {\n EcoMeasurement,\n EcoSensor,\n EcoSensorKind,\n EcoSensorStatus,\n EcoAlertThreshold,\n EcoThresholdDirection,\n EcoThresholdSeverity,\n} from '../../types';\n\ntype MonitoringTab = 'measurements' | 'sensors' | 'thresholds';\n\nconst SENSOR_KINDS: EcoSensorKind[] = [\n 'AIR_QUALITY',\n 'WATER_QUALITY',\n 'WEATHER',\n 'SOIL',\n 'ENERGY_METER',\n 'FLOW',\n 'ACOUSTIC',\n 'CAMERA_TRAP',\n 'MANUAL',\n];\nconst SENSOR_STATUSES: EcoSensorStatus[] = [\n 'ONLINE',\n 'OFFLINE',\n 'DEGRADED',\n 'MAINTENANCE',\n 'RETIRED',\n];\nconst THRESHOLD_DIRECTIONS: EcoThresholdDirection[] = ['ABOVE', 'BELOW', 'OUTSIDE_RANGE'];\nconst THRESHOLD_SEVERITIES: EcoThresholdSeverity[] = ['INFO', 'WARNING', 'CRITICAL'];\n\nconst BREACH_FILTERS: FilterOption<'ALL' | 'BREACHES'>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'BREACHES', label: 'Breaches only' },\n];\n\nconst ALL = 'ALL';\nconst PAGE_SIZE = 25;\n\nexport function MonitoringPage() {\n const [tab, setTab] = useState<MonitoringTab>('measurements');\n\n // Site directory — powers name lookups + the per-tab site filters/selects.\n const sitesQuery = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const allSites = useMemo(() => sitesQuery.data?.items ?? [], [sitesQuery.data]);\n const siteNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const s of allSites) map.set(s.id, s.name);\n return map;\n }, [allSites]);\n const siteName = (id?: string | null): string => (id ? (siteNameById.get(id) ?? '—') : '—');\n\n // ── Measurements tab state ────────────────────────────────────────────────\n const [measSiteId, setMeasSiteId] = useState<string>(ALL);\n const [breachFilter, setBreachFilter] = useState<'ALL' | 'BREACHES'>('ALL');\n const measPager = useCursorPager();\n\n const measurementsQuery = useEcoMeasurements({\n siteId: measSiteId === ALL ? undefined : measSiteId,\n breachesOnly: breachFilter === 'BREACHES' ? true : undefined,\n pagination: { limit: PAGE_SIZE, cursor: measPager.cursor },\n });\n const recordMeasurement = useEcoRecordMeasurement();\n\n const [measureOpen, setMeasureOpen] = useState(false);\n const [measureForm, setMeasureForm] = useState({\n siteId: '',\n parameter: '',\n value: '',\n unit: '',\n sensorId: '',\n });\n const dialogSensors = useEcoSensors({ siteId: measureForm.siteId || undefined });\n const dialogSensorList = dialogSensors.data?.items ?? [];\n\n // ── Sensors tab state ─────────────────────────────────────────────────────\n const [sensorSiteId, setSensorSiteId] = useState<string>(ALL);\n const [sensorStatusFilter, setSensorStatusFilter] = useState<EcoSensorStatus | 'ALL'>(ALL);\n\n const sensorsQuery = useEcoSensors({\n siteId: sensorSiteId === ALL ? undefined : sensorSiteId,\n status: sensorStatusFilter === ALL ? undefined : sensorStatusFilter,\n });\n const sensors = sensorsQuery.data?.items ?? [];\n const createSensor = useEcoCreateSensor();\n const setSensorStatus = useEcoSetSensorStatus();\n\n const [sensorOpen, setSensorOpen] = useState(false);\n const [sensorForm, setSensorForm] = useState({\n siteId: '',\n name: '',\n kind: 'AIR_QUALITY' as EcoSensorKind,\n unit: '',\n });\n\n // ── Thresholds tab state ──────────────────────────────────────────────────\n const [thresholdSiteId, setThresholdSiteId] = useState<string>(ALL);\n\n const thresholdsQuery = useEcoThresholds(thresholdSiteId === ALL ? undefined : thresholdSiteId);\n const thresholds = thresholdsQuery.data?.items ?? [];\n const createThreshold = useEcoCreateThreshold();\n const setThresholdActive = useEcoSetThresholdActive();\n\n const [thresholdOpen, setThresholdOpen] = useState(false);\n const [thresholdForm, setThresholdForm] = useState({\n siteId: '',\n parameter: '',\n unit: '',\n direction: 'ABOVE' as EcoThresholdDirection,\n severity: 'WARNING' as EcoThresholdSeverity,\n limitValue: '',\n minValue: '',\n maxValue: '',\n });\n\n const [error, setError] = useState<string | null>(null);\n\n const tabItems: TabItem<MonitoringTab>[] = [\n { value: 'measurements', label: 'Measurements', icon: <Activity className=\"h-4 w-4\" /> },\n {\n value: 'sensors',\n label: 'Sensors',\n icon: <Radio className=\"h-4 w-4\" />,\n count: sensorsQuery.data?.items?.length,\n },\n {\n value: 'thresholds',\n label: 'Thresholds',\n icon: <Gauge className=\"h-4 w-4\" />,\n count: thresholdsQuery.data?.items?.length,\n },\n ];\n\n // ── Mutations ─────────────────────────────────────────────────────────────\n const handleRecordMeasurement = async () => {\n if (\n !measureForm.siteId ||\n !measureForm.parameter.trim() ||\n !measureForm.unit.trim() ||\n !measureForm.value\n ) {\n setError('Site, parameter, value and unit are required.');\n return;\n }\n setError(null);\n try {\n await recordMeasurement.mutateAsync({\n siteId: measureForm.siteId,\n parameter: measureForm.parameter.trim(),\n value: Number(measureForm.value),\n unit: measureForm.unit.trim(),\n recordedAt: new Date().toISOString(),\n sensorId: measureForm.sensorId || null,\n source: 'MANUAL',\n });\n setMeasureOpen(false);\n setMeasureForm({ siteId: '', parameter: '', value: '', unit: '', sensorId: '' });\n measurementsQuery.refetch();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record measurement.');\n }\n };\n\n const handleAddSensor = async () => {\n if (!sensorForm.siteId || !sensorForm.name.trim()) {\n setError('Site and sensor name are required.');\n return;\n }\n setError(null);\n try {\n await createSensor.mutateAsync({\n name: sensorForm.name.trim(),\n siteId: sensorForm.siteId,\n kind: sensorForm.kind,\n unit: sensorForm.unit.trim() || null,\n });\n setSensorOpen(false);\n setSensorForm({ siteId: '', name: '', kind: 'AIR_QUALITY', unit: '' });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add sensor.');\n }\n };\n\n const handleAddThreshold = async () => {\n if (!thresholdForm.siteId || !thresholdForm.parameter.trim() || !thresholdForm.unit.trim()) {\n setError('Site, parameter and unit are required.');\n return;\n }\n setError(null);\n try {\n await createThreshold.mutateAsync({\n siteId: thresholdForm.siteId,\n parameter: thresholdForm.parameter.trim(),\n unit: thresholdForm.unit.trim(),\n direction: thresholdForm.direction,\n severity: thresholdForm.severity,\n limitValue: thresholdForm.limitValue ? Number(thresholdForm.limitValue) : null,\n minValue: thresholdForm.minValue ? Number(thresholdForm.minValue) : null,\n maxValue: thresholdForm.maxValue ? Number(thresholdForm.maxValue) : null,\n });\n setThresholdOpen(false);\n setThresholdForm({\n siteId: '',\n parameter: '',\n unit: '',\n direction: 'ABOVE',\n severity: 'WARNING',\n limitValue: '',\n minValue: '',\n maxValue: '',\n });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add threshold.');\n }\n };\n\n // ── Columns ───────────────────────────────────────────────────────────────\n const measurementColumns: Column<EcoMeasurement>[] = [\n {\n key: 'parameter',\n header: 'Parameter',\n render: (m) => <span className=\"font-medium text-text-primary\">{m.parameter}</span>,\n },\n {\n key: 'value',\n header: 'Value',\n align: 'right',\n render: (m) => (\n <span className=\"tabular-nums text-text-primary\">\n {formatNumber(m.value)} {m.unit}\n </span>\n ),\n },\n {\n key: 'source',\n header: 'Source',\n hideOnMobile: true,\n render: (m) => <span className=\"text-text-secondary\">{formatEnumLabel(m.source)}</span>,\n },\n {\n key: 'breach',\n header: 'Status',\n render: (m) =>\n m.isBreach ? (\n <StatusBadge status=\"BREACH\" className=\"uppercase\" />\n ) : (\n <StatusBadge status=\"OK\" className=\"uppercase\" />\n ),\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (m) => <span className=\"text-text-secondary\">{siteName(m.siteId)}</span>,\n },\n {\n key: 'recorded',\n header: 'Recorded',\n align: 'right',\n hideOnMobile: true,\n render: (m) => <span className=\"text-text-tertiary\">{formatDateTime(m.recordedAt)}</span>,\n },\n ];\n\n const sensorColumns: Column<EcoSensor>[] = [\n {\n key: 'name',\n header: 'Sensor',\n render: (s) => (\n <div>\n <p className=\"font-medium text-text-primary\">{s.name}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(s.kind)}</p>\n </div>\n ),\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (s) => <span className=\"text-text-secondary\">{siteName(s.siteId)}</span>,\n },\n {\n key: 'unit',\n header: 'Unit',\n hideOnMobile: true,\n render: (s) => <span className=\"text-text-secondary\">{s.unit ?? '—'}</span>,\n },\n { key: 'status', header: 'Status', render: (s) => <StatusBadge status={s.status} /> },\n {\n key: 'action',\n header: '',\n align: 'right',\n render: (s) => (\n <Select\n value={s.status}\n onValueChange={(v) => setSensorStatus.mutate({ id: s.id, status: v as EcoSensorStatus })}\n >\n <SelectTrigger className=\"h-8 w-36\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SENSOR_STATUSES.map((st) => (\n <SelectItem key={st} value={st}>\n {formatEnumLabel(st)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ),\n },\n ];\n\n const thresholdColumns: Column<EcoAlertThreshold>[] = [\n {\n key: 'parameter',\n header: 'Parameter',\n render: (t) => <span className=\"font-medium text-text-primary\">{t.parameter}</span>,\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (t) => <span className=\"text-text-secondary\">{siteName(t.siteId)}</span>,\n },\n {\n key: 'rule',\n header: 'Rule',\n render: (t) => (\n <span className=\"text-text-secondary\">\n {t.direction === 'OUTSIDE_RANGE'\n ? `${formatNumber(t.minValue)}–${formatNumber(t.maxValue)} ${t.unit}`\n : `${formatEnumLabel(t.direction)} ${formatNumber(t.limitValue)} ${t.unit}`}\n </span>\n ),\n },\n { key: 'severity', header: 'Severity', render: (t) => <StatusBadge status={t.severity} /> },\n {\n key: 'active',\n header: 'Active',\n align: 'right',\n render: (t) => (\n <Button\n size=\"sm\"\n variant={t.isActive ? 'outline' : 'ghost'}\n onClick={() => setThresholdActive.mutate({ id: t.id, isActive: !t.isActive })}\n >\n {t.isActive ? 'Active' : 'Inactive'}\n </Button>\n ),\n },\n ];\n\n // ── Reusable site <Select> options ────────────────────────────────────────\n const siteOptions = allSites.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ));\n\n const measurementRows = measurementsQuery.data?.items ?? [];\n\n return (\n <PageLayout\n title=\"Monitoring\"\n description=\"Environmental sensors, readings and alert thresholds across every monitored site.\"\n actions={\n tab === 'measurements' ? (\n <Button\n onClick={() => {\n setError(null);\n setMeasureOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record measurement\n </Button>\n ) : tab === 'sensors' ? (\n <Button\n onClick={() => {\n setError(null);\n setSensorOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add sensor\n </Button>\n ) : (\n <Button\n onClick={() => {\n setError(null);\n setThresholdOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add threshold\n </Button>\n )\n }\n >\n <Tabs items={tabItems} value={tab} onChange={setTab} className=\"mb-5\" />\n\n {/* ── Measurements ─────────────────────────────────────────────────── */}\n {tab === 'measurements' && (\n <>\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select\n value={measSiteId}\n onValueChange={(v) => {\n setMeasSiteId(v);\n measPager.reset();\n }}\n >\n <SelectTrigger className=\"w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All sites</SelectItem>\n {siteOptions}\n </SelectContent>\n </Select>\n <FilterTabs\n options={BREACH_FILTERS}\n value={breachFilter}\n onChange={(v) => {\n setBreachFilter(v);\n measPager.reset();\n }}\n />\n </div>\n\n {measurementsQuery.isLoading ? (\n <LoadingView label=\"Loading measurements…\" />\n ) : measurementsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load measurements.\"\n onRetry={() => measurementsQuery.refetch()}\n />\n ) : measurementRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No measurements\"\n description={\n measSiteId === ALL && breachFilter === 'ALL'\n ? 'Record a measurement to start building a reading history.'\n : 'No measurements match these filters.'\n }\n action={\n <Button\n onClick={() => {\n setError(null);\n setMeasureOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record measurement\n </Button>\n }\n />\n ) : (\n <>\n <DataTable columns={measurementColumns} rows={measurementRows} rowKey={(m) => m.id} />\n <Pagination\n page={measPager.page}\n pageSize={PAGE_SIZE}\n itemCount={measurementRows.length}\n hasMore={measurementsQuery.data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) =>\n measPager.onPageChange(target, measurementsQuery.data?.pageInfo?.cursor)\n }\n />\n </>\n )}\n </>\n )}\n\n {/* ── Sensors ──────────────────────────────────────────────────────── */}\n {tab === 'sensors' && (\n <>\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select value={sensorSiteId} onValueChange={setSensorSiteId}>\n <SelectTrigger className=\"w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All sites</SelectItem>\n {siteOptions}\n </SelectContent>\n </Select>\n <Select\n value={sensorStatusFilter}\n onValueChange={(v) => setSensorStatusFilter(v as EcoSensorStatus | 'ALL')}\n >\n <SelectTrigger className=\"w-44\">\n <SelectValue placeholder=\"All statuses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\n {SENSOR_STATUSES.map((st) => (\n <SelectItem key={st} value={st}>\n {formatEnumLabel(st)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {sensorsQuery.isLoading ? (\n <LoadingView label=\"Loading sensors…\" />\n ) : sensorsQuery.isError ? (\n <ErrorView message=\"Couldn't load sensors.\" onRetry={() => sensorsQuery.refetch()} />\n ) : sensors.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Radio className=\"h-10 w-10\" />}\n title=\"No sensors\"\n description={\n sensorSiteId === ALL && sensorStatusFilter === ALL\n ? 'Add a sensor to begin streaming or logging readings.'\n : 'No sensors match these filters.'\n }\n action={\n <Button\n onClick={() => {\n setError(null);\n setSensorOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add sensor\n </Button>\n }\n />\n ) : (\n <DataTable columns={sensorColumns} rows={sensors} rowKey={(s) => s.id} />\n )}\n </>\n )}\n\n {/* ── Thresholds ───────────────────────────────────────────────────── */}\n {tab === 'thresholds' && (\n <>\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select value={thresholdSiteId} onValueChange={setThresholdSiteId}>\n <SelectTrigger className=\"w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All sites</SelectItem>\n {siteOptions}\n </SelectContent>\n </Select>\n </div>\n\n {thresholdsQuery.isLoading ? (\n <LoadingView label=\"Loading thresholds…\" />\n ) : thresholdsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load thresholds.\"\n onRetry={() => thresholdsQuery.refetch()}\n />\n ) : thresholds.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No thresholds\"\n description={\n thresholdSiteId === ALL\n ? 'Define a threshold to flag readings that drift out of range.'\n : 'This site has no thresholds yet.'\n }\n action={\n <Button\n onClick={() => {\n setError(null);\n setThresholdOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add threshold\n </Button>\n }\n />\n ) : (\n <DataTable columns={thresholdColumns} rows={thresholds} rowKey={(t) => t.id} />\n )}\n </>\n )}\n\n {/* ── Record measurement dialog ────────────────────────────────────── */}\n <ResponsiveDialog\n open={measureOpen}\n onOpenChange={(open) => {\n setMeasureOpen(open);\n if (!open) setError(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Record measurement</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Log a reading. It is checked against the site's active thresholds and flagged if\n it breaches.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mSite\">Site *</Label>\n <Select\n value={measureForm.siteId}\n onValueChange={(v) => setMeasureForm({ ...measureForm, siteId: v, sensorId: '' })}\n >\n <SelectTrigger id=\"mSite\">\n <SelectValue placeholder=\"Select a site\" />\n </SelectTrigger>\n <SelectContent>{siteOptions}</SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-3\">\n <div className=\"grid gap-1.5 sm:col-span-1\">\n <Label htmlFor=\"mParam\">Parameter *</Label>\n <Input\n id=\"mParam\"\n value={measureForm.parameter}\n onChange={(e) => setMeasureForm({ ...measureForm, parameter: e.target.value })}\n placeholder=\"PM2.5\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mValue\">Value *</Label>\n <Input\n id=\"mValue\"\n type=\"number\"\n step=\"any\"\n value={measureForm.value}\n onChange={(e) => setMeasureForm({ ...measureForm, value: e.target.value })}\n placeholder=\"42\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mUnit\">Unit *</Label>\n <Input\n id=\"mUnit\"\n value={measureForm.unit}\n onChange={(e) => setMeasureForm({ ...measureForm, unit: e.target.value })}\n placeholder=\"µg/m³\"\n />\n </div>\n </div>\n {measureForm.siteId && dialogSensorList.length > 0 && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mSensor\">Sensor (optional)</Label>\n <Select\n value={measureForm.sensorId}\n onValueChange={(v) => setMeasureForm({ ...measureForm, sensorId: v })}\n >\n <SelectTrigger id=\"mSensor\">\n <SelectValue placeholder=\"Manual entry\" />\n </SelectTrigger>\n <SelectContent>\n {dialogSensorList.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-text-tertiary\">\n Select a specific sensor for context, or leave blank for a site-level reading.\n </p>\n </div>\n )}\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setMeasureOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleRecordMeasurement} disabled={recordMeasurement.isPending}>\n {recordMeasurement.isPending ? 'Recording…' : 'Record'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n\n {/* ── Add sensor dialog ────────────────────────────────────────────── */}\n <ResponsiveDialog\n open={sensorOpen}\n onOpenChange={(open) => {\n setSensorOpen(open);\n if (!open) setError(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add sensor</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Register a sensor and attribute it to a monitored site.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sSite\">Site *</Label>\n <Select\n value={sensorForm.siteId}\n onValueChange={(v) => setSensorForm({ ...sensorForm, siteId: v })}\n >\n <SelectTrigger id=\"sSite\">\n <SelectValue placeholder=\"Select a site\" />\n </SelectTrigger>\n <SelectContent>{siteOptions}</SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sName\">Name *</Label>\n <Input\n id=\"sName\"\n value={sensorForm.name}\n onChange={(e) => setSensorForm({ ...sensorForm, name: e.target.value })}\n placeholder=\"North gate PM2.5\"\n />\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sKind\">Kind</Label>\n <Select\n value={sensorForm.kind}\n onValueChange={(v) => setSensorForm({ ...sensorForm, kind: v as EcoSensorKind })}\n >\n <SelectTrigger id=\"sKind\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SENSOR_KINDS.map((k) => (\n <SelectItem key={k} value={k}>\n {formatEnumLabel(k)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sUnit\">Unit</Label>\n <Input\n id=\"sUnit\"\n value={sensorForm.unit}\n onChange={(e) => setSensorForm({ ...sensorForm, unit: e.target.value })}\n placeholder=\"µg/m³\"\n />\n </div>\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setSensorOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleAddSensor} disabled={createSensor.isPending}>\n {createSensor.isPending ? 'Adding…' : 'Add sensor'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n\n {/* ── Add threshold dialog ─────────────────────────────────────────── */}\n <ResponsiveDialog\n open={thresholdOpen}\n onOpenChange={(open) => {\n setThresholdOpen(open);\n if (!open) setError(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add alert threshold</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Flag readings that drift out of an acceptable range.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tSite\">Site *</Label>\n <Select\n value={thresholdForm.siteId}\n onValueChange={(v) => setThresholdForm({ ...thresholdForm, siteId: v })}\n >\n <SelectTrigger id=\"tSite\">\n <SelectValue placeholder=\"Select a site\" />\n </SelectTrigger>\n <SelectContent>{siteOptions}</SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tParam\">Parameter *</Label>\n <Input\n id=\"tParam\"\n value={thresholdForm.parameter}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, parameter: e.target.value })\n }\n placeholder=\"PM2.5\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tUnit\">Unit *</Label>\n <Input\n id=\"tUnit\"\n value={thresholdForm.unit}\n onChange={(e) => setThresholdForm({ ...thresholdForm, unit: e.target.value })}\n placeholder=\"µg/m³\"\n />\n </div>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tDir\">Direction</Label>\n <Select\n value={thresholdForm.direction}\n onValueChange={(v) =>\n setThresholdForm({ ...thresholdForm, direction: v as EcoThresholdDirection })\n }\n >\n <SelectTrigger id=\"tDir\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {THRESHOLD_DIRECTIONS.map((d) => (\n <SelectItem key={d} value={d}>\n {formatEnumLabel(d)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tSev\">Severity</Label>\n <Select\n value={thresholdForm.severity}\n onValueChange={(v) =>\n setThresholdForm({ ...thresholdForm, severity: v as EcoThresholdSeverity })\n }\n >\n <SelectTrigger id=\"tSev\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {THRESHOLD_SEVERITIES.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n {thresholdForm.direction === 'OUTSIDE_RANGE' ? (\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tMin\">Min value</Label>\n <Input\n id=\"tMin\"\n type=\"number\"\n step=\"any\"\n value={thresholdForm.minValue}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, minValue: e.target.value })\n }\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tMax\">Max value</Label>\n <Input\n id=\"tMax\"\n type=\"number\"\n step=\"any\"\n value={thresholdForm.maxValue}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, maxValue: e.target.value })\n }\n />\n </div>\n </div>\n ) : (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tLimit\">Limit value</Label>\n <Input\n id=\"tLimit\"\n type=\"number\"\n step=\"any\"\n value={thresholdForm.limitValue}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, limitValue: e.target.value })\n }\n placeholder=\"35\"\n />\n </div>\n )}\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setThresholdOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleAddThreshold} disabled={createThreshold.isPending}>\n {createThreshold.isPending ? 'Adding…' : 'Add threshold'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MonitoringPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/monitoring/MonitoringPage.tsx"],"sourcesContent":["/**\n * Monitoring — a cross-site hub for environmental measurements, sensors &\n * alert thresholds.\n * @module ecoimpacthub/pages/monitoring\n *\n * Reads `ecoListMeasurements`, `ecoListSensors`, `ecoListThresholds` (and\n * `ecoListSites` for name lookups); writes `ecoRecordMeasurement`,\n * `ecoCreateSensor`, `ecoSetSensorStatus`, `ecoCreateThreshold`,\n * `ecoSetThresholdActive`.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Activity, Radio, Gauge } from 'lucide-react';\nimport {\n Button,\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 { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { DataTable, type Column } from '../../components/DataTable';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { Tabs, type TabItem } from '../../components/Tabs';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useEcoSites,\n useEcoMeasurements,\n useEcoRecordMeasurement,\n useEcoSensors,\n useEcoCreateSensor,\n useEcoSetSensorStatus,\n useEcoThresholds,\n useEcoCreateThreshold,\n useEcoSetThresholdActive,\n useCursorPager,\n} from '../../hooks';\nimport { formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport type {\n EcoMeasurement,\n EcoSensor,\n EcoSensorKind,\n EcoSensorStatus,\n EcoAlertThreshold,\n EcoThresholdDirection,\n EcoThresholdSeverity,\n} from '../../types';\n\ntype MonitoringTab = 'measurements' | 'sensors' | 'thresholds';\n\nconst SENSOR_KINDS: EcoSensorKind[] = [\n 'AIR_QUALITY',\n 'WATER_QUALITY',\n 'WEATHER',\n 'SOIL',\n 'ENERGY_METER',\n 'FLOW',\n 'ACOUSTIC',\n 'CAMERA_TRAP',\n 'MANUAL',\n];\nconst SENSOR_STATUSES: EcoSensorStatus[] = [\n 'ONLINE',\n 'OFFLINE',\n 'DEGRADED',\n 'MAINTENANCE',\n 'RETIRED',\n];\nconst THRESHOLD_DIRECTIONS: EcoThresholdDirection[] = ['ABOVE', 'BELOW', 'OUTSIDE_RANGE'];\nconst THRESHOLD_SEVERITIES: EcoThresholdSeverity[] = ['INFO', 'WARNING', 'CRITICAL'];\n\nconst BREACH_FILTERS: FilterOption<'ALL' | 'BREACHES'>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'BREACHES', label: 'Breaches only' },\n];\n\nconst ALL = 'ALL';\nconst PAGE_SIZE = 25;\n\nexport function MonitoringPage() {\n const [tab, setTab] = useState<MonitoringTab>('measurements');\n\n // Site directory — powers name lookups + the per-tab site filters/selects.\n const sitesQuery = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const allSites = useMemo(() => sitesQuery.data?.items ?? [], [sitesQuery.data]);\n const siteNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const s of allSites) map.set(s.id, s.name);\n return map;\n }, [allSites]);\n const siteName = (id?: string | null): string => (id ? (siteNameById.get(id) ?? '—') : '—');\n\n // ── Measurements tab state ────────────────────────────────────────────────\n const [measSiteId, setMeasSiteId] = useState<string>(ALL);\n const [breachFilter, setBreachFilter] = useState<'ALL' | 'BREACHES'>('ALL');\n const measPager = useCursorPager();\n\n const measurementsQuery = useEcoMeasurements({\n siteId: measSiteId === ALL ? undefined : measSiteId,\n breachesOnly: breachFilter === 'BREACHES' ? true : undefined,\n pagination: { limit: PAGE_SIZE, cursor: measPager.cursor },\n });\n const recordMeasurement = useEcoRecordMeasurement();\n\n const [measureOpen, setMeasureOpen] = useState(false);\n const [measureForm, setMeasureForm] = useState({\n siteId: '',\n parameter: '',\n value: '',\n unit: '',\n sensorId: '',\n });\n const dialogSensors = useEcoSensors({ siteId: measureForm.siteId || undefined });\n const dialogSensorList = dialogSensors.data?.items ?? [];\n\n // ── Sensors tab state ─────────────────────────────────────────────────────\n const [sensorSiteId, setSensorSiteId] = useState<string>(ALL);\n const [sensorStatusFilter, setSensorStatusFilter] = useState<EcoSensorStatus | 'ALL'>(ALL);\n\n const sensorsQuery = useEcoSensors({\n siteId: sensorSiteId === ALL ? undefined : sensorSiteId,\n status: sensorStatusFilter === ALL ? undefined : sensorStatusFilter,\n });\n const sensors = sensorsQuery.data?.items ?? [];\n const createSensor = useEcoCreateSensor();\n const setSensorStatus = useEcoSetSensorStatus();\n\n const [sensorOpen, setSensorOpen] = useState(false);\n const [sensorForm, setSensorForm] = useState({\n siteId: '',\n name: '',\n kind: 'AIR_QUALITY' as EcoSensorKind,\n unit: '',\n });\n\n // ── Thresholds tab state ──────────────────────────────────────────────────\n const [thresholdSiteId, setThresholdSiteId] = useState<string>(ALL);\n\n const thresholdsQuery = useEcoThresholds(thresholdSiteId === ALL ? undefined : thresholdSiteId);\n const thresholds = thresholdsQuery.data?.items ?? [];\n const createThreshold = useEcoCreateThreshold();\n const setThresholdActive = useEcoSetThresholdActive();\n\n const [thresholdOpen, setThresholdOpen] = useState(false);\n const [thresholdForm, setThresholdForm] = useState({\n siteId: '',\n parameter: '',\n unit: '',\n direction: 'ABOVE' as EcoThresholdDirection,\n severity: 'WARNING' as EcoThresholdSeverity,\n limitValue: '',\n minValue: '',\n maxValue: '',\n });\n\n const [error, setError] = useState<string | null>(null);\n\n const tabItems: TabItem<MonitoringTab>[] = [\n { value: 'measurements', label: 'Measurements', icon: <Activity className=\"h-4 w-4\" /> },\n {\n value: 'sensors',\n label: 'Sensors',\n icon: <Radio className=\"h-4 w-4\" />,\n count: sensorsQuery.data?.items?.length,\n },\n {\n value: 'thresholds',\n label: 'Thresholds',\n icon: <Gauge className=\"h-4 w-4\" />,\n count: thresholdsQuery.data?.items?.length,\n },\n ];\n\n // ── Mutations ─────────────────────────────────────────────────────────────\n const handleRecordMeasurement = async () => {\n if (\n !measureForm.siteId ||\n !measureForm.parameter.trim() ||\n !measureForm.unit.trim() ||\n !measureForm.value\n ) {\n setError('Site, parameter, value and unit are required.');\n return;\n }\n setError(null);\n try {\n await recordMeasurement.mutateAsync({\n siteId: measureForm.siteId,\n parameter: measureForm.parameter.trim(),\n value: Number(measureForm.value),\n unit: measureForm.unit.trim(),\n recordedAt: new Date().toISOString(),\n sensorId: measureForm.sensorId || null,\n source: 'MANUAL',\n });\n setMeasureOpen(false);\n setMeasureForm({ siteId: '', parameter: '', value: '', unit: '', sensorId: '' });\n measurementsQuery.refetch();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record measurement.');\n }\n };\n\n const handleAddSensor = async () => {\n if (!sensorForm.siteId || !sensorForm.name.trim()) {\n setError('Site and sensor name are required.');\n return;\n }\n setError(null);\n try {\n await createSensor.mutateAsync({\n name: sensorForm.name.trim(),\n siteId: sensorForm.siteId,\n kind: sensorForm.kind,\n unit: sensorForm.unit.trim() || null,\n });\n setSensorOpen(false);\n setSensorForm({ siteId: '', name: '', kind: 'AIR_QUALITY', unit: '' });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add sensor.');\n }\n };\n\n const handleAddThreshold = async () => {\n if (!thresholdForm.siteId || !thresholdForm.parameter.trim() || !thresholdForm.unit.trim()) {\n setError('Site, parameter and unit are required.');\n return;\n }\n setError(null);\n try {\n await createThreshold.mutateAsync({\n siteId: thresholdForm.siteId,\n parameter: thresholdForm.parameter.trim(),\n unit: thresholdForm.unit.trim(),\n direction: thresholdForm.direction,\n severity: thresholdForm.severity,\n limitValue: thresholdForm.limitValue ? Number(thresholdForm.limitValue) : null,\n minValue: thresholdForm.minValue ? Number(thresholdForm.minValue) : null,\n maxValue: thresholdForm.maxValue ? Number(thresholdForm.maxValue) : null,\n });\n setThresholdOpen(false);\n setThresholdForm({\n siteId: '',\n parameter: '',\n unit: '',\n direction: 'ABOVE',\n severity: 'WARNING',\n limitValue: '',\n minValue: '',\n maxValue: '',\n });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add threshold.');\n }\n };\n\n // ── Columns ───────────────────────────────────────────────────────────────\n const measurementColumns: Column<EcoMeasurement>[] = [\n {\n key: 'parameter',\n header: 'Parameter',\n render: (m) => <span className=\"font-medium text-text-primary\">{m.parameter}</span>,\n },\n {\n key: 'value',\n header: 'Value',\n align: 'right',\n render: (m) => (\n <span className=\"tabular-nums text-text-primary\">\n {formatNumber(m.value)} {m.unit}\n </span>\n ),\n },\n {\n key: 'source',\n header: 'Source',\n hideOnMobile: true,\n render: (m) => <span className=\"text-text-secondary\">{formatEnumLabel(m.source)}</span>,\n },\n {\n key: 'breach',\n header: 'Status',\n render: (m) =>\n m.isBreach ? (\n <StatusBadge status=\"BREACH\" className=\"uppercase\" />\n ) : (\n <StatusBadge status=\"OK\" className=\"uppercase\" />\n ),\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (m) => <span className=\"text-text-secondary\">{siteName(m.siteId)}</span>,\n },\n {\n key: 'recorded',\n header: 'Recorded',\n align: 'right',\n hideOnMobile: true,\n render: (m) => <span className=\"text-text-tertiary\">{formatDateTime(m.recordedAt)}</span>,\n },\n ];\n\n const sensorColumns: Column<EcoSensor>[] = [\n {\n key: 'name',\n header: 'Sensor',\n render: (s) => (\n <div>\n <p className=\"font-medium text-text-primary\">{s.name}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(s.kind)}</p>\n </div>\n ),\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (s) => <span className=\"text-text-secondary\">{siteName(s.siteId)}</span>,\n },\n {\n key: 'unit',\n header: 'Unit',\n hideOnMobile: true,\n render: (s) => <span className=\"text-text-secondary\">{s.unit ?? '—'}</span>,\n },\n { key: 'status', header: 'Status', render: (s) => <StatusBadge status={s.status} /> },\n {\n key: 'action',\n header: '',\n align: 'right',\n render: (s) => (\n <Select\n value={s.status}\n onValueChange={(v) => setSensorStatus.mutate({ id: s.id, status: v as EcoSensorStatus })}\n >\n <SelectTrigger className=\"h-8 w-36\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SENSOR_STATUSES.map((st) => (\n <SelectItem key={st} value={st}>\n {formatEnumLabel(st)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ),\n },\n ];\n\n const thresholdColumns: Column<EcoAlertThreshold>[] = [\n {\n key: 'parameter',\n header: 'Parameter',\n render: (t) => <span className=\"font-medium text-text-primary\">{t.parameter}</span>,\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (t) => <span className=\"text-text-secondary\">{siteName(t.siteId)}</span>,\n },\n {\n key: 'rule',\n header: 'Rule',\n render: (t) => (\n <span className=\"text-text-secondary\">\n {t.direction === 'OUTSIDE_RANGE'\n ? `${formatNumber(t.minValue)}–${formatNumber(t.maxValue)} ${t.unit}`\n : `${formatEnumLabel(t.direction)} ${formatNumber(t.limitValue)} ${t.unit}`}\n </span>\n ),\n },\n { key: 'severity', header: 'Severity', render: (t) => <StatusBadge status={t.severity} /> },\n {\n key: 'active',\n header: 'Active',\n align: 'right',\n render: (t) => (\n <Button\n size=\"sm\"\n variant={t.isActive ? 'outline' : 'ghost'}\n onClick={() => setThresholdActive.mutate({ id: t.id, isActive: !t.isActive })}\n >\n {t.isActive ? 'Active' : 'Inactive'}\n </Button>\n ),\n },\n ];\n\n // ── Reusable site <Select> options ────────────────────────────────────────\n const siteOptions = allSites.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ));\n\n const measurementRows = measurementsQuery.data?.items ?? [];\n\n return (\n <PageLayout\n title=\"Monitoring\"\n description=\"Environmental sensors, readings and alert thresholds across every monitored site.\"\n actions={\n tab === 'measurements' ? (\n <Button\n onClick={() => {\n setError(null);\n setMeasureOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record measurement\n </Button>\n ) : tab === 'sensors' ? (\n <Button\n onClick={() => {\n setError(null);\n setSensorOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add sensor\n </Button>\n ) : (\n <Button\n onClick={() => {\n setError(null);\n setThresholdOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add threshold\n </Button>\n )\n }\n >\n <Tabs items={tabItems} value={tab} onChange={setTab} className=\"mb-5\" />\n\n {/* ── Measurements ─────────────────────────────────────────────────── */}\n {tab === 'measurements' && (\n <>\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select\n value={measSiteId}\n onValueChange={(v) => {\n setMeasSiteId(v);\n measPager.reset();\n }}\n >\n <SelectTrigger className=\"w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All sites</SelectItem>\n {siteOptions}\n </SelectContent>\n </Select>\n <FilterTabs\n options={BREACH_FILTERS}\n value={breachFilter}\n onChange={(v) => {\n setBreachFilter(v);\n measPager.reset();\n }}\n />\n </div>\n\n {measurementsQuery.isLoading ? (\n <LoadingView label=\"Loading measurements…\" />\n ) : measurementsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load measurements.\"\n onRetry={() => measurementsQuery.refetch()}\n />\n ) : measurementRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No measurements\"\n description={\n measSiteId === ALL && breachFilter === 'ALL'\n ? 'Record a measurement to start building a reading history.'\n : 'No measurements match these filters.'\n }\n action={\n <Button\n onClick={() => {\n setError(null);\n setMeasureOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record measurement\n </Button>\n }\n />\n ) : (\n <>\n <DataTable columns={measurementColumns} rows={measurementRows} rowKey={(m) => m.id} />\n <Pagination\n page={measPager.page}\n pageSize={PAGE_SIZE}\n itemCount={measurementRows.length}\n hasMore={measurementsQuery.data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) =>\n measPager.onPageChange(target, measurementsQuery.data?.pageInfo?.cursor)\n }\n />\n </>\n )}\n </>\n )}\n\n {/* ── Sensors ──────────────────────────────────────────────────────── */}\n {tab === 'sensors' && (\n <>\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select value={sensorSiteId} onValueChange={setSensorSiteId}>\n <SelectTrigger className=\"w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All sites</SelectItem>\n {siteOptions}\n </SelectContent>\n </Select>\n <Select\n value={sensorStatusFilter}\n onValueChange={(v) => setSensorStatusFilter(v as EcoSensorStatus | 'ALL')}\n >\n <SelectTrigger className=\"w-44\">\n <SelectValue placeholder=\"All statuses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\n {SENSOR_STATUSES.map((st) => (\n <SelectItem key={st} value={st}>\n {formatEnumLabel(st)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {sensorsQuery.isLoading ? (\n <LoadingView label=\"Loading sensors…\" />\n ) : sensorsQuery.isError ? (\n <ErrorView message=\"Couldn't load sensors.\" onRetry={() => sensorsQuery.refetch()} />\n ) : sensors.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Radio className=\"h-10 w-10\" />}\n title=\"No sensors\"\n description={\n sensorSiteId === ALL && sensorStatusFilter === ALL\n ? 'Add a sensor to begin streaming or logging readings.'\n : 'No sensors match these filters.'\n }\n action={\n <Button\n onClick={() => {\n setError(null);\n setSensorOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add sensor\n </Button>\n }\n />\n ) : (\n <DataTable columns={sensorColumns} rows={sensors} rowKey={(s) => s.id} />\n )}\n </>\n )}\n\n {/* ── Thresholds ───────────────────────────────────────────────────── */}\n {tab === 'thresholds' && (\n <>\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select value={thresholdSiteId} onValueChange={setThresholdSiteId}>\n <SelectTrigger className=\"w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All sites</SelectItem>\n {siteOptions}\n </SelectContent>\n </Select>\n </div>\n\n {thresholdsQuery.isLoading ? (\n <LoadingView label=\"Loading thresholds…\" />\n ) : thresholdsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load thresholds.\"\n onRetry={() => thresholdsQuery.refetch()}\n />\n ) : thresholds.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No thresholds\"\n description={\n thresholdSiteId === ALL\n ? 'Define a threshold to flag readings that drift out of range.'\n : 'This site has no thresholds yet.'\n }\n action={\n <Button\n onClick={() => {\n setError(null);\n setThresholdOpen(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add threshold\n </Button>\n }\n />\n ) : (\n <DataTable columns={thresholdColumns} rows={thresholds} rowKey={(t) => t.id} />\n )}\n </>\n )}\n\n {/* ── Record measurement dialog ────────────────────────────────────── */}\n <ResponsiveDialog\n open={measureOpen}\n onOpenChange={(open) => {\n setMeasureOpen(open);\n if (!open) setError(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Record measurement</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Log a reading. It is checked against the site's active thresholds and flagged if\n it breaches.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mSite\">Site *</Label>\n <Select\n value={measureForm.siteId}\n onValueChange={(v) => setMeasureForm({ ...measureForm, siteId: v, sensorId: '' })}\n >\n <SelectTrigger id=\"mSite\">\n <SelectValue placeholder=\"Select a site\" />\n </SelectTrigger>\n <SelectContent>{siteOptions}</SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-3\">\n <div className=\"grid gap-1.5 sm:col-span-1\">\n <Label htmlFor=\"mParam\">Parameter *</Label>\n <Input\n id=\"mParam\"\n value={measureForm.parameter}\n onChange={(e) => setMeasureForm({ ...measureForm, parameter: e.target.value })}\n placeholder=\"PM2.5\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mValue\">Value *</Label>\n <Input\n id=\"mValue\"\n type=\"number\"\n step=\"any\"\n value={measureForm.value}\n onChange={(e) => setMeasureForm({ ...measureForm, value: e.target.value })}\n placeholder=\"42\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mUnit\">Unit *</Label>\n <Input\n id=\"mUnit\"\n value={measureForm.unit}\n onChange={(e) => setMeasureForm({ ...measureForm, unit: e.target.value })}\n placeholder=\"µg/m³\"\n />\n </div>\n </div>\n {measureForm.siteId && dialogSensorList.length > 0 && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"mSensor\">Sensor (optional)</Label>\n <Select\n value={measureForm.sensorId}\n onValueChange={(v) => setMeasureForm({ ...measureForm, sensorId: v })}\n >\n <SelectTrigger id=\"mSensor\">\n <SelectValue placeholder=\"Manual entry\" />\n </SelectTrigger>\n <SelectContent>\n {dialogSensorList.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-text-tertiary\">\n Select a specific sensor for context, or leave blank for a site-level reading.\n </p>\n </div>\n )}\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setMeasureOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleRecordMeasurement} disabled={recordMeasurement.isPending}>\n {recordMeasurement.isPending ? 'Recording…' : 'Record'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n\n {/* ── Add sensor dialog ────────────────────────────────────────────── */}\n <ResponsiveDialog\n open={sensorOpen}\n onOpenChange={(open) => {\n setSensorOpen(open);\n if (!open) setError(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add sensor</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Register a sensor and attribute it to a monitored site.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sSite\">Site *</Label>\n <Select\n value={sensorForm.siteId}\n onValueChange={(v) => setSensorForm({ ...sensorForm, siteId: v })}\n >\n <SelectTrigger id=\"sSite\">\n <SelectValue placeholder=\"Select a site\" />\n </SelectTrigger>\n <SelectContent>{siteOptions}</SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sName\">Name *</Label>\n <Input\n id=\"sName\"\n value={sensorForm.name}\n onChange={(e) => setSensorForm({ ...sensorForm, name: e.target.value })}\n placeholder=\"North gate PM2.5\"\n />\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sKind\">Kind</Label>\n <Select\n value={sensorForm.kind}\n onValueChange={(v) => setSensorForm({ ...sensorForm, kind: v as EcoSensorKind })}\n >\n <SelectTrigger id=\"sKind\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SENSOR_KINDS.map((k) => (\n <SelectItem key={k} value={k}>\n {formatEnumLabel(k)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"sUnit\">Unit</Label>\n <Input\n id=\"sUnit\"\n value={sensorForm.unit}\n onChange={(e) => setSensorForm({ ...sensorForm, unit: e.target.value })}\n placeholder=\"µg/m³\"\n />\n </div>\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setSensorOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleAddSensor} disabled={createSensor.isPending}>\n {createSensor.isPending ? 'Adding…' : 'Add sensor'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n\n {/* ── Add threshold dialog ─────────────────────────────────────────── */}\n <ResponsiveDialog\n open={thresholdOpen}\n onOpenChange={(open) => {\n setThresholdOpen(open);\n if (!open) setError(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add alert threshold</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Flag readings that drift out of an acceptable range.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tSite\">Site *</Label>\n <Select\n value={thresholdForm.siteId}\n onValueChange={(v) => setThresholdForm({ ...thresholdForm, siteId: v })}\n >\n <SelectTrigger id=\"tSite\">\n <SelectValue placeholder=\"Select a site\" />\n </SelectTrigger>\n <SelectContent>{siteOptions}</SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tParam\">Parameter *</Label>\n <Input\n id=\"tParam\"\n value={thresholdForm.parameter}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, parameter: e.target.value })\n }\n placeholder=\"PM2.5\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tUnit\">Unit *</Label>\n <Input\n id=\"tUnit\"\n value={thresholdForm.unit}\n onChange={(e) => setThresholdForm({ ...thresholdForm, unit: e.target.value })}\n placeholder=\"µg/m³\"\n />\n </div>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tDir\">Direction</Label>\n <Select\n value={thresholdForm.direction}\n onValueChange={(v) =>\n setThresholdForm({ ...thresholdForm, direction: v as EcoThresholdDirection })\n }\n >\n <SelectTrigger id=\"tDir\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {THRESHOLD_DIRECTIONS.map((d) => (\n <SelectItem key={d} value={d}>\n {formatEnumLabel(d)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tSev\">Severity</Label>\n <Select\n value={thresholdForm.severity}\n onValueChange={(v) =>\n setThresholdForm({ ...thresholdForm, severity: v as EcoThresholdSeverity })\n }\n >\n <SelectTrigger id=\"tSev\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {THRESHOLD_SEVERITIES.map((s) => (\n <SelectItem key={s} value={s}>\n {formatEnumLabel(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n {thresholdForm.direction === 'OUTSIDE_RANGE' ? (\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tMin\">Min value</Label>\n <Input\n id=\"tMin\"\n type=\"number\"\n step=\"any\"\n value={thresholdForm.minValue}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, minValue: e.target.value })\n }\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tMax\">Max value</Label>\n <Input\n id=\"tMax\"\n type=\"number\"\n step=\"any\"\n value={thresholdForm.maxValue}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, maxValue: e.target.value })\n }\n />\n </div>\n </div>\n ) : (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"tLimit\">Limit value</Label>\n <Input\n id=\"tLimit\"\n type=\"number\"\n step=\"any\"\n value={thresholdForm.limitValue}\n onChange={(e) =>\n setThresholdForm({ ...thresholdForm, limitValue: e.target.value })\n }\n placeholder=\"35\"\n />\n </div>\n )}\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setThresholdOpen(false)}>\n Cancel\n </Button>\n <Button onClick={handleAddThreshold} disabled={createThreshold.isPending}>\n {createThreshold.isPending ? 'Adding…' : 'Add threshold'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObservationsPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/observations/ObservationsPage.tsx"],"sourcesContent":["/**\n * Observations — a cross-species / cross-site field-observation feed.\n * @module ecoimpacthub/pages/observations\n *\n * Reads `ecoListObservations` (which selects the related species inline, so\n * `o.species?.commonName` is available per item); writes `ecoRecordObservation`.\n * Site & species name lookups are built from the `ecoListSites` /\n * `ecoListSpecies` connections. No mock data.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Binoculars, MapPin } from 'lucide-react';\nimport {\n Button,\n GlassCard,\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 { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { Pagination } from '../../components/Pagination';\nimport { EvidenceUpload } from '../../components/EvidenceUpload';\nimport { MethodBadge } from '../_shared/MethodBadge';\nimport { EvidenceThumb } from '../_shared/EvidenceThumb';\nimport {\n useEcoObservations,\n useEcoRecordObservation,\n useEcoSites,\n useEcoSpecies,\n useCursorPager,\n type UploadedEcoFile,\n} from '../../hooks';\nimport {\n formatCoords,\n formatDateTime,\n formatEnumLabel,\n formatNumber,\n} from '../../utils/formatters';\nimport type { EcoObservation, EcoObservationMethod } from '../../types';\n\nconst OBSERVATION_METHODS: EcoObservationMethod[] = [\n 'DIRECT_SIGHTING',\n 'CAMERA_TRAP',\n 'ACOUSTIC',\n 'TRACKS_SIGNS',\n 'EDNA',\n 'TELEMETRY',\n 'COMMUNITY_REPORT',\n];\n\nconst PAGE_SIZE = 25;\nconst LOOKUP_PAGE = { limit: 100, offset: 0 };\n/** Sentinel for the \"no site\" option (Radix Select items can't use an empty value). */\nconst NO_SITE = 'NONE';\n\ninterface RecordFormState {\n speciesId: string;\n observedAt: string;\n count: string;\n method: EcoObservationMethod;\n siteId: string;\n latitude: string;\n longitude: string;\n notes: string;\n evidence: UploadedEcoFile | null;\n}\n\n/** A datetime-local value (`YYYY-MM-DDTHH:mm`) for \"now\" in the viewer's timezone. */\nfunction nowLocalInputValue(): string {\n const now = new Date();\n const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\nfunction makeEmptyForm(): RecordFormState {\n return {\n speciesId: '',\n observedAt: nowLocalInputValue(),\n count: '1',\n method: 'DIRECT_SIGHTING',\n siteId: NO_SITE,\n latitude: '',\n longitude: '',\n notes: '',\n evidence: null,\n };\n}\n\nexport function ObservationsPage() {\n const [speciesFilter, setSpeciesFilter] = useState<string>('ALL');\n const [siteFilter, setSiteFilter] = useState<string>('ALL');\n const pager = useCursorPager();\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<RecordFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const sites = useEcoSites({ pagination: LOOKUP_PAGE });\n const species = useEcoSpecies({ pagination: LOOKUP_PAGE });\n\n const { data, isLoading, isError, refetch } = useEcoObservations({\n speciesId: speciesFilter === 'ALL' ? undefined : speciesFilter,\n siteId: siteFilter === 'ALL' ? undefined : siteFilter,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n const recordObservation = useEcoRecordObservation();\n\n const siteRows = useMemo(() => sites.data?.items ?? [], [sites.data]);\n const speciesRows = useMemo(() => species.data?.items ?? [], [species.data]);\n const rows = useMemo(() => data?.items ?? [], [data]);\n\n const siteNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const s of siteRows) map.set(s.id, s.name);\n return map;\n }, [siteRows]);\n\n const speciesNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const s of speciesRows) map.set(s.id, s.commonName);\n return map;\n }, [speciesRows]);\n\n const columns: Column<EcoObservation>[] = [\n {\n key: 'evidence',\n header: 'Evidence',\n render: (o) => (\n <EvidenceThumb\n fileId={o.evidence?.id}\n alt={o.species?.commonName ?? speciesNameById.get(o.speciesId)}\n />\n ),\n },\n {\n key: 'species',\n header: 'Species',\n render: (o) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">\n {o.species?.commonName ?? speciesNameById.get(o.speciesId) ?? 'Unknown species'}\n </p>\n {o.species?.scientificName && (\n <p className=\"truncate text-xs italic text-text-tertiary\">{o.species.scientificName}</p>\n )}\n </div>\n ),\n },\n {\n key: 'method',\n header: 'Method',\n render: (o) => <MethodBadge method={o.method} />,\n },\n {\n key: 'count',\n header: 'Count',\n align: 'right',\n sortValue: (o) => o.count,\n render: (o) => (\n <span className=\"tabular-nums font-medium text-text-primary\">×{formatNumber(o.count)}</span>\n ),\n },\n {\n key: 'observedAt',\n header: 'Observed',\n hideOnMobile: true,\n render: (o) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatDateTime(o.observedAt)}\n </span>\n ),\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (o) => {\n const name = o.siteId ? siteNameById.get(o.siteId) : undefined;\n return name ? (\n <span className=\"text-text-secondary\">{name}</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n );\n },\n },\n {\n key: 'coords',\n header: 'Coords',\n hideOnMobile: true,\n render: (o) => {\n const coords = formatCoords(o.latitude, o.longitude);\n if (coords === '—') return <span className=\"text-text-tertiary\">—</span>;\n return (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-text-secondary\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n {coords}\n </span>\n );\n },\n },\n {\n key: 'notes',\n header: 'Notes',\n hideOnMobile: true,\n render: (o) =>\n o.notes ? (\n <span className=\"block max-w-xs truncate text-text-secondary\" title={o.notes}>\n {o.notes}\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const resetForm = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleRecord = async () => {\n if (!form.speciesId) {\n setFormError('Species is required.');\n return;\n }\n try {\n await recordObservation.mutateAsync({\n speciesId: form.speciesId,\n observedAt: form.observedAt\n ? new Date(form.observedAt).toISOString()\n : new Date().toISOString(),\n count: form.count ? Number(form.count) : 1,\n method: form.method,\n siteId: form.siteId && form.siteId !== NO_SITE ? form.siteId : null,\n latitude: form.latitude ? Number(form.latitude) : null,\n longitude: form.longitude ? Number(form.longitude) : null,\n notes: form.notes.trim() || null,\n evidenceFileId: form.evidence?.fileId ?? null,\n });\n setDialogOpen(false);\n resetForm();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to record observation.');\n }\n };\n\n const filtersActive = speciesFilter !== 'ALL' || siteFilter !== 'ALL';\n\n return (\n <PageLayout\n title=\"Field Observations\"\n description=\"Wildlife & biodiversity sightings across every species and site — your citizen-science field log.\"\n actions={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record observation\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Species\n </span>\n <Select\n value={speciesFilter}\n onValueChange={(v) => {\n setSpeciesFilter(v);\n pager.reset();\n }}\n >\n <SelectTrigger className=\"w-full sm:w-52\">\n <SelectValue placeholder=\"All species\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All species</SelectItem>\n {speciesRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.commonName}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Site\n </span>\n <Select\n value={siteFilter}\n onValueChange={(v) => {\n setSiteFilter(v);\n pager.reset();\n }}\n >\n <SelectTrigger className=\"w-full sm:w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All sites</SelectItem>\n {siteRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n </GlassCard>\n\n {isLoading ? (\n <LoadingView label=\"Loading observations…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load observations.\" onRetry={() => refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Binoculars className=\"h-10 w-10\" />}\n title=\"No observations yet\"\n description={\n filtersActive\n ? 'No observations match these filters.'\n : 'Record your first field observation to start building your biodiversity dataset.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record observation\n </Button>\n }\n />\n ) : (\n <>\n <DataTable columns={columns} rows={rows} rowKey={(o) => o.id} />\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo?.cursor)}\n />\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>Record observation</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Log a wildlife or biodiversity sighting in the field.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"species\">Species *</Label>\n <Select\n value={form.speciesId}\n onValueChange={(v) => setForm({ ...form, speciesId: v })}\n >\n <SelectTrigger id=\"species\">\n <SelectValue placeholder=\"Select a species\" />\n </SelectTrigger>\n <SelectContent>\n {speciesRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.commonName}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {speciesRows.length === 0 && (\n <p className=\"text-xs text-text-tertiary\">\n No species defined yet — add one in the Species section first.\n </p>\n )}\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"observedAt\">Observed at</Label>\n <Input\n id=\"observedAt\"\n type=\"datetime-local\"\n value={form.observedAt}\n onChange={(e) => setForm({ ...form, observedAt: e.target.value })}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"count\">Count</Label>\n <Input\n id=\"count\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.count}\n onChange={(e) => setForm({ ...form, count: e.target.value })}\n placeholder=\"1\"\n />\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=\"method\">Method</Label>\n <Select\n value={form.method}\n onValueChange={(v) => setForm({ ...form, method: v as EcoObservationMethod })}\n >\n <SelectTrigger id=\"method\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {OBSERVATION_METHODS.map((m) => (\n <SelectItem key={m} value={m}>\n {formatEnumLabel(m)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\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 {siteRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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=\"lat\">Latitude</Label>\n <Input\n id=\"lat\"\n type=\"number\"\n step=\"any\"\n value={form.latitude}\n onChange={(e) => setForm({ ...form, latitude: e.target.value })}\n placeholder=\"12.9716\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"lng\">Longitude</Label>\n <Input\n id=\"lng\"\n type=\"number\"\n step=\"any\"\n value={form.longitude}\n onChange={(e) => setForm({ ...form, longitude: e.target.value })}\n placeholder=\"77.5946\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"notes\">Notes</Label>\n <Textarea\n id=\"notes\"\n value={form.notes}\n onChange={(e) => setForm({ ...form, notes: e.target.value })}\n placeholder=\"Behaviour, habitat, weather or other field context.\"\n rows={2}\n />\n </div>\n\n <EvidenceUpload\n id=\"obsEvidence\"\n label=\"Evidence photo (optional)\"\n value={form.evidence}\n onChange={(evidence) => setForm({ ...form, evidence })}\n hint=\"Attach a photo, audio clip or PDF as supporting evidence.\"\n />\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={handleRecord} disabled={recordObservation.isPending}>\n {recordObservation.isPending ? 'Recording…' : 'Record observation'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ObservationsPage.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/observations/ObservationsPage.tsx"],"sourcesContent":["/**\n * Observations — a cross-species / cross-site field-observation feed.\n * @module ecoimpacthub/pages/observations\n *\n * Reads `ecoListObservations` (which selects the related species inline, so\n * `o.species?.commonName` is available per item); writes `ecoRecordObservation`.\n * Site & species name lookups are built from the `ecoListSites` /\n * `ecoListSpecies` connections. No mock data.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Binoculars, MapPin } from 'lucide-react';\nimport {\n Button,\n GlassCard,\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 { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { Pagination } from '../../components/Pagination';\nimport { EvidenceUpload } from '../../components/EvidenceUpload';\nimport { MethodBadge } from '../_shared/MethodBadge';\nimport { EvidenceThumb } from '../_shared/EvidenceThumb';\nimport {\n useEcoObservations,\n useEcoRecordObservation,\n useEcoSites,\n useEcoSpecies,\n useCursorPager,\n type UploadedEcoFile,\n} from '../../hooks';\nimport {\n formatCoords,\n formatDateTime,\n formatEnumLabel,\n formatNumber,\n} from '../../utils/formatters';\nimport type { EcoObservation, EcoObservationMethod } from '../../types';\n\nconst OBSERVATION_METHODS: EcoObservationMethod[] = [\n 'DIRECT_SIGHTING',\n 'CAMERA_TRAP',\n 'ACOUSTIC',\n 'TRACKS_SIGNS',\n 'EDNA',\n 'TELEMETRY',\n 'COMMUNITY_REPORT',\n];\n\nconst PAGE_SIZE = 25;\nconst LOOKUP_PAGE = { limit: 100, offset: 0 };\n/** Sentinel for the \"no site\" option (Radix Select items can't use an empty value). */\nconst NO_SITE = 'NONE';\n\ninterface RecordFormState {\n speciesId: string;\n observedAt: string;\n count: string;\n method: EcoObservationMethod;\n siteId: string;\n latitude: string;\n longitude: string;\n notes: string;\n evidence: UploadedEcoFile | null;\n}\n\n/** A datetime-local value (`YYYY-MM-DDTHH:mm`) for \"now\" in the viewer's timezone. */\nfunction nowLocalInputValue(): string {\n const now = new Date();\n const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\nfunction makeEmptyForm(): RecordFormState {\n return {\n speciesId: '',\n observedAt: nowLocalInputValue(),\n count: '1',\n method: 'DIRECT_SIGHTING',\n siteId: NO_SITE,\n latitude: '',\n longitude: '',\n notes: '',\n evidence: null,\n };\n}\n\nexport function ObservationsPage() {\n const [speciesFilter, setSpeciesFilter] = useState<string>('ALL');\n const [siteFilter, setSiteFilter] = useState<string>('ALL');\n const pager = useCursorPager();\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<RecordFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const sites = useEcoSites({ pagination: LOOKUP_PAGE });\n const species = useEcoSpecies({ pagination: LOOKUP_PAGE });\n\n const { data, isLoading, isError, refetch } = useEcoObservations({\n speciesId: speciesFilter === 'ALL' ? undefined : speciesFilter,\n siteId: siteFilter === 'ALL' ? undefined : siteFilter,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n const recordObservation = useEcoRecordObservation();\n\n const siteRows = useMemo(() => sites.data?.items ?? [], [sites.data]);\n const speciesRows = useMemo(() => species.data?.items ?? [], [species.data]);\n const rows = useMemo(() => data?.items ?? [], [data]);\n\n const siteNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const s of siteRows) map.set(s.id, s.name);\n return map;\n }, [siteRows]);\n\n const speciesNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const s of speciesRows) map.set(s.id, s.commonName);\n return map;\n }, [speciesRows]);\n\n const columns: Column<EcoObservation>[] = [\n {\n key: 'evidence',\n header: 'Evidence',\n render: (o) => (\n <EvidenceThumb\n fileId={o.evidence?.id}\n alt={o.species?.commonName ?? speciesNameById.get(o.speciesId)}\n />\n ),\n },\n {\n key: 'species',\n header: 'Species',\n render: (o) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">\n {o.species?.commonName ?? speciesNameById.get(o.speciesId) ?? 'Unknown species'}\n </p>\n {o.species?.scientificName && (\n <p className=\"truncate text-xs italic text-text-tertiary\">{o.species.scientificName}</p>\n )}\n </div>\n ),\n },\n {\n key: 'method',\n header: 'Method',\n render: (o) => <MethodBadge method={o.method} />,\n },\n {\n key: 'count',\n header: 'Count',\n align: 'right',\n sortValue: (o) => o.count,\n render: (o) => (\n <span className=\"tabular-nums font-medium text-text-primary\">×{formatNumber(o.count)}</span>\n ),\n },\n {\n key: 'observedAt',\n header: 'Observed',\n hideOnMobile: true,\n render: (o) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatDateTime(o.observedAt)}\n </span>\n ),\n },\n {\n key: 'site',\n header: 'Site',\n hideOnMobile: true,\n render: (o) => {\n const name = o.siteId ? siteNameById.get(o.siteId) : undefined;\n return name ? (\n <span className=\"text-text-secondary\">{name}</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n );\n },\n },\n {\n key: 'coords',\n header: 'Coords',\n hideOnMobile: true,\n render: (o) => {\n const coords = formatCoords(o.latitude, o.longitude);\n if (coords === '—') return <span className=\"text-text-tertiary\">—</span>;\n return (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-text-secondary\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0 text-text-tertiary\" aria-hidden=\"true\" />\n {coords}\n </span>\n );\n },\n },\n {\n key: 'notes',\n header: 'Notes',\n hideOnMobile: true,\n render: (o) =>\n o.notes ? (\n <span className=\"block max-w-xs truncate text-text-secondary\" title={o.notes}>\n {o.notes}\n </span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const resetForm = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleRecord = async () => {\n if (!form.speciesId) {\n setFormError('Species is required.');\n return;\n }\n try {\n await recordObservation.mutateAsync({\n speciesId: form.speciesId,\n observedAt: form.observedAt\n ? new Date(form.observedAt).toISOString()\n : new Date().toISOString(),\n count: form.count ? Number(form.count) : 1,\n method: form.method,\n siteId: form.siteId && form.siteId !== NO_SITE ? form.siteId : null,\n latitude: form.latitude ? Number(form.latitude) : null,\n longitude: form.longitude ? Number(form.longitude) : null,\n notes: form.notes.trim() || null,\n evidenceFileId: form.evidence?.fileId ?? null,\n });\n setDialogOpen(false);\n resetForm();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to record observation.');\n }\n };\n\n const filtersActive = speciesFilter !== 'ALL' || siteFilter !== 'ALL';\n\n return (\n <PageLayout\n title=\"Field Observations\"\n description=\"Wildlife & biodiversity sightings across every species and site — your citizen-science field log.\"\n actions={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record observation\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Species\n </span>\n <Select\n value={speciesFilter}\n onValueChange={(v) => {\n setSpeciesFilter(v);\n pager.reset();\n }}\n >\n <SelectTrigger className=\"w-full sm:w-52\">\n <SelectValue placeholder=\"All species\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All species</SelectItem>\n {speciesRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.commonName}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n Site\n </span>\n <Select\n value={siteFilter}\n onValueChange={(v) => {\n setSiteFilter(v);\n pager.reset();\n }}\n >\n <SelectTrigger className=\"w-full sm:w-52\">\n <SelectValue placeholder=\"All sites\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All sites</SelectItem>\n {siteRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n </GlassCard>\n\n {isLoading ? (\n <LoadingView label=\"Loading observations…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load observations.\" onRetry={() => refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Binoculars className=\"h-10 w-10\" />}\n title=\"No observations yet\"\n description={\n filtersActive\n ? 'No observations match these filters.'\n : 'Record your first field observation to start building your biodiversity dataset.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Record observation\n </Button>\n }\n />\n ) : (\n <>\n <DataTable columns={columns} rows={rows} rowKey={(o) => o.id} />\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo?.cursor)}\n />\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>Record observation</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Log a wildlife or biodiversity sighting in the field.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"species\">Species *</Label>\n <Select\n value={form.speciesId}\n onValueChange={(v) => setForm({ ...form, speciesId: v })}\n >\n <SelectTrigger id=\"species\">\n <SelectValue placeholder=\"Select a species\" />\n </SelectTrigger>\n <SelectContent>\n {speciesRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.commonName}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {speciesRows.length === 0 && (\n <p className=\"text-xs text-text-tertiary\">\n No species defined yet — add one in the Species section first.\n </p>\n )}\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"observedAt\">Observed at</Label>\n <Input\n id=\"observedAt\"\n type=\"datetime-local\"\n value={form.observedAt}\n onChange={(e) => setForm({ ...form, observedAt: e.target.value })}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"count\">Count</Label>\n <Input\n id=\"count\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.count}\n onChange={(e) => setForm({ ...form, count: e.target.value })}\n placeholder=\"1\"\n />\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=\"method\">Method</Label>\n <Select\n value={form.method}\n onValueChange={(v) => setForm({ ...form, method: v as EcoObservationMethod })}\n >\n <SelectTrigger id=\"method\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {OBSERVATION_METHODS.map((m) => (\n <SelectItem key={m} value={m}>\n {formatEnumLabel(m)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\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 {siteRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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=\"lat\">Latitude</Label>\n <Input\n id=\"lat\"\n type=\"number\"\n step=\"any\"\n value={form.latitude}\n onChange={(e) => setForm({ ...form, latitude: e.target.value })}\n placeholder=\"12.9716\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"lng\">Longitude</Label>\n <Input\n id=\"lng\"\n type=\"number\"\n step=\"any\"\n value={form.longitude}\n onChange={(e) => setForm({ ...form, longitude: e.target.value })}\n placeholder=\"77.5946\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"notes\">Notes</Label>\n <Textarea\n id=\"notes\"\n value={form.notes}\n onChange={(e) => setForm({ ...form, notes: e.target.value })}\n placeholder=\"Behaviour, habitat, weather or other field context.\"\n rows={2}\n />\n </div>\n\n <EvidenceUpload\n id=\"obsEvidence\"\n label=\"Evidence photo (optional)\"\n value={form.evidence}\n onChange={(evidence) => setForm({ ...form, evidence })}\n hint=\"Attach a photo, audio clip or PDF as supporting evidence.\"\n />\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={handleRecord} disabled={recordObservation.isPending}>\n {recordObservation.isPending ? 'Recording…' : 'Record observation'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|