@burdenoff/microfe-ecoimpacthub 2026.704.2 → 2026.704.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/ecoimpacthub/components/EcoDiscussion.js +109 -0
  2. package/dist/ecoimpacthub/components/EcoDiscussion.js.map +1 -0
  3. package/dist/ecoimpacthub/components/EcoTags.js +149 -0
  4. package/dist/ecoimpacthub/components/EcoTags.js.map +1 -0
  5. package/dist/ecoimpacthub/components/EvidenceUpload.js +93 -0
  6. package/dist/ecoimpacthub/components/EvidenceUpload.js.map +1 -0
  7. package/dist/ecoimpacthub/components/FileLink.js +36 -0
  8. package/dist/ecoimpacthub/components/FileLink.js.map +1 -0
  9. package/dist/ecoimpacthub/components/PageLayout.js +2 -2
  10. package/dist/ecoimpacthub/components/PageLayout.js.map +1 -1
  11. package/dist/ecoimpacthub/hooks/index.js +3 -0
  12. package/dist/ecoimpacthub/hooks/useEcoComments.js +45 -0
  13. package/dist/ecoimpacthub/hooks/useEcoComments.js.map +1 -0
  14. package/dist/ecoimpacthub/hooks/useEcoFiles.js +81 -0
  15. package/dist/ecoimpacthub/hooks/useEcoFiles.js.map +1 -0
  16. package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js +169 -166
  17. package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js.map +1 -1
  18. package/dist/ecoimpacthub/hooks/useEcoTags.js +89 -0
  19. package/dist/ecoimpacthub/hooks/useEcoTags.js.map +1 -0
  20. package/dist/ecoimpacthub/pages/carbon/CarbonPage.js +4 -0
  21. package/dist/ecoimpacthub/pages/carbon/CarbonPage.js.map +1 -1
  22. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +124 -101
  23. package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
  24. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +153 -126
  25. package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
  26. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +241 -230
  27. package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
  28. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +260 -225
  29. package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
  30. package/package.json +1 -1
@@ -2,18 +2,20 @@ import { cn as e } from "../../utils/cn.js";
2
2
  import { PageLayout as t } from "../../components/PageLayout.js";
3
3
  import { formatCoords as n, formatDateTime as r, formatEnumLabel as i, formatNumber as ee } from "../../utils/formatters.js";
4
4
  import { ErrorView as te, LoadingView as ne } from "../../components/StateViews.js";
5
- import { EmptyState as a } from "../../components/EmptyState.js";
6
- import { useEcoObservations as o, useEcoRecordObservation as s, useEcoSites as c, useEcoSpecies as re } from "../../hooks/useEcoImpactHubApi.js";
7
- import { useCursorPager as ie } from "../../hooks/useCursorPager.js";
5
+ import { EmptyState as re } from "../../components/EmptyState.js";
6
+ import { useEcoObservations as ie, useEcoRecordObservation as a, useEcoSites as o, useEcoSpecies as s } from "../../hooks/useEcoImpactHubApi.js";
7
+ import { useCursorPager as c } from "../../hooks/useCursorPager.js";
8
8
  import "../../hooks/index.js";
9
- import { DataTable as l } from "../../components/DataTable.js";
10
- import { Pagination as u } from "../../components/Pagination.js";
11
- import { useMemo as d, useState as f } from "react";
12
- import { Binoculars as p, Plus as m } from "lucide-react";
13
- import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
14
- import { Button as v, Input as y, Label as b, ResponsiveDialog as x, ResponsiveDialogContent as ae, ResponsiveDialogDescription as oe, ResponsiveDialogFooter as S, ResponsiveDialogHeader as C, ResponsiveDialogTitle as w, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k, Textarea as se } from "@burdenoff/fe-libs/ui";
9
+ import { DataTable as ae } from "../../components/DataTable.js";
10
+ import { Pagination as l } from "../../components/Pagination.js";
11
+ import { EvidenceUpload as u } from "../../components/EvidenceUpload.js";
12
+ import { FileLink as d } from "../../components/FileLink.js";
13
+ import { useMemo as f, useState as p } from "react";
14
+ import { Binoculars as m, Plus as h } from "lucide-react";
15
+ import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
16
+ import { Button as y, Input as b, Label as x, ResponsiveDialog as oe, ResponsiveDialogContent as se, ResponsiveDialogDescription as S, ResponsiveDialogFooter as ce, ResponsiveDialogHeader as C, ResponsiveDialogTitle as w, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k, Textarea as le } from "@burdenoff/fe-libs/ui";
15
17
  //#region src/ecoimpacthub/pages/observations/ObservationsPage.tsx
16
- var ce = [
18
+ var ue = [
17
19
  "DIRECT_SIGHTING",
18
20
  "CAMERA_TRAP",
19
21
  "ACOUSTIC",
@@ -38,35 +40,36 @@ function P() {
38
40
  siteId: M,
39
41
  latitude: "",
40
42
  longitude: "",
41
- notes: ""
43
+ notes: "",
44
+ evidence: null
42
45
  };
43
46
  }
44
47
  function F() {
45
- let [N, F] = f("ALL"), [I, L] = f("ALL"), R = ie(), [z, B] = f(!1), [V, H] = f(P), [U, W] = f(null), G = c({ pagination: j }), K = re({ pagination: j }), { data: q, isLoading: le, isError: ue, refetch: de } = o({
48
+ let [N, F] = p("ALL"), [I, L] = p("ALL"), R = c(), [z, B] = p(!1), [V, H] = p(P), [U, W] = p(null), G = o({ pagination: j }), K = s({ pagination: j }), { data: q, isLoading: de, isError: fe, refetch: pe } = ie({
46
49
  speciesId: N === "ALL" ? void 0 : N,
47
50
  siteId: I === "ALL" ? void 0 : I,
48
51
  pagination: {
49
52
  limit: A,
50
53
  cursor: R.cursor
51
54
  }
52
- }), J = s(), Y = d(() => G.data?.items ?? [], [G.data]), X = d(() => K.data?.items ?? [], [K.data]), Z = d(() => q?.items ?? [], [q]), fe = d(() => {
55
+ }), J = a(), Y = f(() => G.data?.items ?? [], [G.data]), X = f(() => K.data?.items ?? [], [K.data]), Z = f(() => q?.items ?? [], [q]), me = f(() => {
53
56
  let e = /* @__PURE__ */ new Map();
54
57
  for (let t of Y) e.set(t.id, t.name);
55
58
  return e;
56
- }, [Y]), pe = d(() => {
59
+ }, [Y]), he = f(() => {
57
60
  let e = /* @__PURE__ */ new Map();
58
61
  for (let t of X) e.set(t.id, t.commonName);
59
62
  return e;
60
- }, [X]), me = [
63
+ }, [X]), ge = [
61
64
  {
62
65
  key: "species",
63
66
  header: "Species",
64
- render: (e) => /* @__PURE__ */ _("div", {
67
+ render: (e) => /* @__PURE__ */ v("div", {
65
68
  className: "min-w-0",
66
- children: [/* @__PURE__ */ g("p", {
69
+ children: [/* @__PURE__ */ _("p", {
67
70
  className: "font-semibold text-text-primary",
68
- children: e.species?.commonName ?? pe.get(e.speciesId) ?? "Unknown species"
69
- }), e.species?.scientificName && /* @__PURE__ */ g("p", {
71
+ children: e.species?.commonName ?? he.get(e.speciesId) ?? "Unknown species"
72
+ }), e.species?.scientificName && /* @__PURE__ */ _("p", {
70
73
  className: "truncate text-xs italic text-text-tertiary",
71
74
  children: e.species.scientificName
72
75
  })]
@@ -75,7 +78,7 @@ function F() {
75
78
  {
76
79
  key: "method",
77
80
  header: "Method",
78
- render: (e) => /* @__PURE__ */ g("span", {
81
+ render: (e) => /* @__PURE__ */ _("span", {
79
82
  className: "inline-flex rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
80
83
  children: i(e.method)
81
84
  })
@@ -85,7 +88,7 @@ function F() {
85
88
  header: "Count",
86
89
  align: "right",
87
90
  sortValue: (e) => e.count,
88
- render: (e) => /* @__PURE__ */ _("span", {
91
+ render: (e) => /* @__PURE__ */ v("span", {
89
92
  className: "tabular-nums text-text-secondary",
90
93
  children: ["×", ee(e.count)]
91
94
  })
@@ -95,7 +98,7 @@ function F() {
95
98
  header: "Observed",
96
99
  align: "right",
97
100
  hideOnMobile: !0,
98
- render: (e) => /* @__PURE__ */ g("span", {
101
+ render: (e) => /* @__PURE__ */ _("span", {
99
102
  className: "whitespace-nowrap text-text-secondary",
100
103
  children: r(e.observedAt)
101
104
  })
@@ -105,11 +108,11 @@ function F() {
105
108
  header: "Site",
106
109
  hideOnMobile: !0,
107
110
  render: (e) => {
108
- let t = e.siteId ? fe.get(e.siteId) : void 0;
109
- return t ? /* @__PURE__ */ g("span", {
111
+ let t = e.siteId ? me.get(e.siteId) : void 0;
112
+ return t ? /* @__PURE__ */ _("span", {
110
113
  className: "text-text-secondary",
111
114
  children: t
112
- }) : /* @__PURE__ */ g("span", {
115
+ }) : /* @__PURE__ */ _("span", {
113
116
  className: "text-text-tertiary",
114
117
  children: "—"
115
118
  });
@@ -121,7 +124,7 @@ function F() {
121
124
  hideOnMobile: !0,
122
125
  render: (t) => {
123
126
  let r = n(t.latitude, t.longitude);
124
- return /* @__PURE__ */ g("span", {
127
+ return /* @__PURE__ */ _("span", {
125
128
  className: e("tabular-nums", r === "—" ? "text-text-tertiary" : "text-text-secondary"),
126
129
  children: r
127
130
  });
@@ -131,11 +134,20 @@ function F() {
131
134
  key: "notes",
132
135
  header: "Notes",
133
136
  hideOnMobile: !0,
134
- render: (e) => e.notes ? /* @__PURE__ */ g("span", {
137
+ render: (e) => e.notes ? /* @__PURE__ */ _("span", {
135
138
  className: "block max-w-xs truncate text-text-secondary",
136
139
  title: e.notes,
137
140
  children: e.notes
138
- }) : /* @__PURE__ */ g("span", {
141
+ }) : /* @__PURE__ */ _("span", {
142
+ className: "text-text-tertiary",
143
+ children: "—"
144
+ })
145
+ },
146
+ {
147
+ key: "evidence",
148
+ header: "Evidence",
149
+ hideOnMobile: !0,
150
+ render: (e) => e.evidence?.id ? /* @__PURE__ */ _(d, { fileId: e.evidence.id }) : /* @__PURE__ */ _("span", {
139
151
  className: "text-text-tertiary",
140
152
  children: "—"
141
153
  })
@@ -144,7 +156,7 @@ function F() {
144
156
  H(P()), W(null), B(!0);
145
157
  }, $ = () => {
146
158
  H(P()), W(null);
147
- }, he = async () => {
159
+ }, _e = async () => {
148
160
  if (!V.speciesId) {
149
161
  W("Species is required.");
150
162
  return;
@@ -158,122 +170,123 @@ function F() {
158
170
  siteId: V.siteId && V.siteId !== M ? V.siteId : null,
159
171
  latitude: V.latitude ? Number(V.latitude) : null,
160
172
  longitude: V.longitude ? Number(V.longitude) : null,
161
- notes: V.notes.trim() || null
173
+ notes: V.notes.trim() || null,
174
+ evidenceFileId: V.evidence?.fileId ?? null
162
175
  }), B(!1), $();
163
176
  } catch (e) {
164
177
  W(e instanceof Error ? e.message : "Failed to record observation.");
165
178
  }
166
- }, ge = N !== "ALL" || I !== "ALL";
167
- return /* @__PURE__ */ _(t, {
179
+ }, ve = N !== "ALL" || I !== "ALL";
180
+ return /* @__PURE__ */ v(t, {
168
181
  title: "Field Observations",
169
182
  description: "Wildlife & biodiversity sightings across every species and site — your citizen-science field log.",
170
- actions: /* @__PURE__ */ _(v, {
183
+ actions: /* @__PURE__ */ v(y, {
171
184
  onClick: Q,
172
- children: [/* @__PURE__ */ g(m, { className: "mr-1.5 h-4 w-4" }), " Record observation"]
185
+ children: [/* @__PURE__ */ _(h, { className: "mr-1.5 h-4 w-4" }), " Record observation"]
173
186
  }),
174
187
  children: [
175
- /* @__PURE__ */ _("div", {
188
+ /* @__PURE__ */ v("div", {
176
189
  className: "mb-5 flex flex-wrap items-center gap-3",
177
- children: [/* @__PURE__ */ _(T, {
190
+ children: [/* @__PURE__ */ v(T, {
178
191
  value: N,
179
192
  onValueChange: (e) => {
180
193
  F(e), R.reset();
181
194
  },
182
- children: [/* @__PURE__ */ g(O, {
195
+ children: [/* @__PURE__ */ _(O, {
183
196
  className: "w-52",
184
- children: /* @__PURE__ */ g(k, { placeholder: "All species" })
185
- }), /* @__PURE__ */ _(E, { children: [/* @__PURE__ */ g(D, {
197
+ children: /* @__PURE__ */ _(k, { placeholder: "All species" })
198
+ }), /* @__PURE__ */ v(E, { children: [/* @__PURE__ */ _(D, {
186
199
  value: "ALL",
187
200
  children: "All species"
188
- }), X.map((e) => /* @__PURE__ */ g(D, {
201
+ }), X.map((e) => /* @__PURE__ */ _(D, {
189
202
  value: e.id,
190
203
  children: e.commonName
191
204
  }, e.id))] })]
192
- }), /* @__PURE__ */ _(T, {
205
+ }), /* @__PURE__ */ v(T, {
193
206
  value: I,
194
207
  onValueChange: (e) => {
195
208
  L(e), R.reset();
196
209
  },
197
- children: [/* @__PURE__ */ g(O, {
210
+ children: [/* @__PURE__ */ _(O, {
198
211
  className: "w-52",
199
- children: /* @__PURE__ */ g(k, { placeholder: "All sites" })
200
- }), /* @__PURE__ */ _(E, { children: [/* @__PURE__ */ g(D, {
212
+ children: /* @__PURE__ */ _(k, { placeholder: "All sites" })
213
+ }), /* @__PURE__ */ v(E, { children: [/* @__PURE__ */ _(D, {
201
214
  value: "ALL",
202
215
  children: "All sites"
203
- }), Y.map((e) => /* @__PURE__ */ g(D, {
216
+ }), Y.map((e) => /* @__PURE__ */ _(D, {
204
217
  value: e.id,
205
218
  children: e.name
206
219
  }, e.id))] })]
207
220
  })]
208
221
  }),
209
- le ? /* @__PURE__ */ g(ne, { label: "Loading observations…" }) : ue ? /* @__PURE__ */ g(te, {
222
+ de ? /* @__PURE__ */ _(ne, { label: "Loading observations…" }) : fe ? /* @__PURE__ */ _(te, {
210
223
  message: "Couldn't load observations.",
211
- onRetry: () => de()
212
- }) : Z.length === 0 ? /* @__PURE__ */ g(a, {
224
+ onRetry: () => pe()
225
+ }) : Z.length === 0 ? /* @__PURE__ */ _(re, {
213
226
  illustration: "empty-data",
214
- icon: /* @__PURE__ */ g(p, { className: "h-10 w-10" }),
227
+ icon: /* @__PURE__ */ _(m, { className: "h-10 w-10" }),
215
228
  title: "No observations yet",
216
- description: ge ? "No observations match these filters." : "Record your first field observation to start building your biodiversity dataset.",
217
- action: /* @__PURE__ */ _(v, {
229
+ description: ve ? "No observations match these filters." : "Record your first field observation to start building your biodiversity dataset.",
230
+ action: /* @__PURE__ */ v(y, {
218
231
  onClick: Q,
219
- children: [/* @__PURE__ */ g(m, { className: "mr-1.5 h-4 w-4" }), " Record observation"]
232
+ children: [/* @__PURE__ */ _(h, { className: "mr-1.5 h-4 w-4" }), " Record observation"]
220
233
  })
221
- }) : /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(l, {
222
- columns: me,
234
+ }) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(ae, {
235
+ columns: ge,
223
236
  rows: Z,
224
237
  rowKey: (e) => e.id
225
- }), /* @__PURE__ */ g(u, {
238
+ }), /* @__PURE__ */ _(l, {
226
239
  page: R.page,
227
240
  pageSize: A,
228
241
  itemCount: Z.length,
229
242
  hasMore: q?.pageInfo?.hasMore ?? !1,
230
243
  onPageChange: (e) => R.onPageChange(e, q?.pageInfo?.cursor)
231
244
  })] }),
232
- /* @__PURE__ */ g(x, {
245
+ /* @__PURE__ */ _(oe, {
233
246
  open: z,
234
247
  onOpenChange: (e) => {
235
248
  B(e), e || $();
236
249
  },
237
- children: /* @__PURE__ */ _(ae, { children: [
238
- /* @__PURE__ */ _(C, { children: [/* @__PURE__ */ g(w, { children: "Record observation" }), /* @__PURE__ */ g(oe, { children: "Log a wildlife or biodiversity sighting in the field." })] }),
239
- /* @__PURE__ */ _("div", {
250
+ children: /* @__PURE__ */ v(se, { children: [
251
+ /* @__PURE__ */ v(C, { children: [/* @__PURE__ */ _(w, { children: "Record observation" }), /* @__PURE__ */ _(S, { children: "Log a wildlife or biodiversity sighting in the field." })] }),
252
+ /* @__PURE__ */ v("div", {
240
253
  className: "grid gap-4 py-2",
241
254
  children: [
242
- /* @__PURE__ */ _("div", {
255
+ /* @__PURE__ */ v("div", {
243
256
  className: "grid gap-1.5",
244
257
  children: [
245
- /* @__PURE__ */ g(b, {
258
+ /* @__PURE__ */ _(x, {
246
259
  htmlFor: "species",
247
260
  children: "Species *"
248
261
  }),
249
- /* @__PURE__ */ _(T, {
262
+ /* @__PURE__ */ v(T, {
250
263
  value: V.speciesId,
251
264
  onValueChange: (e) => H({
252
265
  ...V,
253
266
  speciesId: e
254
267
  }),
255
- children: [/* @__PURE__ */ g(O, {
268
+ children: [/* @__PURE__ */ _(O, {
256
269
  id: "species",
257
- children: /* @__PURE__ */ g(k, { placeholder: "Select a species" })
258
- }), /* @__PURE__ */ g(E, { children: X.map((e) => /* @__PURE__ */ g(D, {
270
+ children: /* @__PURE__ */ _(k, { placeholder: "Select a species" })
271
+ }), /* @__PURE__ */ _(E, { children: X.map((e) => /* @__PURE__ */ _(D, {
259
272
  value: e.id,
260
273
  children: e.commonName
261
274
  }, e.id)) })]
262
275
  }),
263
- X.length === 0 && /* @__PURE__ */ g("p", {
276
+ X.length === 0 && /* @__PURE__ */ _("p", {
264
277
  className: "text-xs text-text-tertiary",
265
278
  children: "No species defined yet — add one in the Species section first."
266
279
  })
267
280
  ]
268
281
  }),
269
- /* @__PURE__ */ _("div", {
282
+ /* @__PURE__ */ v("div", {
270
283
  className: "grid gap-2 sm:grid-cols-2",
271
- children: [/* @__PURE__ */ _("div", {
284
+ children: [/* @__PURE__ */ v("div", {
272
285
  className: "grid gap-1.5",
273
- children: [/* @__PURE__ */ g(b, {
286
+ children: [/* @__PURE__ */ _(x, {
274
287
  htmlFor: "observedAt",
275
288
  children: "Observed at"
276
- }), /* @__PURE__ */ g(y, {
289
+ }), /* @__PURE__ */ _(b, {
277
290
  id: "observedAt",
278
291
  type: "datetime-local",
279
292
  value: V.observedAt,
@@ -282,12 +295,12 @@ function F() {
282
295
  observedAt: e.target.value
283
296
  })
284
297
  })]
285
- }), /* @__PURE__ */ _("div", {
298
+ }), /* @__PURE__ */ v("div", {
286
299
  className: "grid gap-1.5",
287
- children: [/* @__PURE__ */ g(b, {
300
+ children: [/* @__PURE__ */ _(x, {
288
301
  htmlFor: "count",
289
302
  children: "Count"
290
- }), /* @__PURE__ */ g(y, {
303
+ }), /* @__PURE__ */ _(b, {
291
304
  id: "count",
292
305
  type: "number",
293
306
  min: "1",
@@ -301,59 +314,59 @@ function F() {
301
314
  })]
302
315
  })]
303
316
  }),
304
- /* @__PURE__ */ _("div", {
317
+ /* @__PURE__ */ v("div", {
305
318
  className: "grid gap-2 sm:grid-cols-2",
306
- children: [/* @__PURE__ */ _("div", {
319
+ children: [/* @__PURE__ */ v("div", {
307
320
  className: "grid gap-1.5",
308
- children: [/* @__PURE__ */ g(b, {
321
+ children: [/* @__PURE__ */ _(x, {
309
322
  htmlFor: "method",
310
323
  children: "Method"
311
- }), /* @__PURE__ */ _(T, {
324
+ }), /* @__PURE__ */ v(T, {
312
325
  value: V.method,
313
326
  onValueChange: (e) => H({
314
327
  ...V,
315
328
  method: e
316
329
  }),
317
- children: [/* @__PURE__ */ g(O, {
330
+ children: [/* @__PURE__ */ _(O, {
318
331
  id: "method",
319
- children: /* @__PURE__ */ g(k, {})
320
- }), /* @__PURE__ */ g(E, { children: ce.map((e) => /* @__PURE__ */ g(D, {
332
+ children: /* @__PURE__ */ _(k, {})
333
+ }), /* @__PURE__ */ _(E, { children: ue.map((e) => /* @__PURE__ */ _(D, {
321
334
  value: e,
322
335
  children: i(e)
323
336
  }, e)) })]
324
337
  })]
325
- }), /* @__PURE__ */ _("div", {
338
+ }), /* @__PURE__ */ v("div", {
326
339
  className: "grid gap-1.5",
327
- children: [/* @__PURE__ */ g(b, {
340
+ children: [/* @__PURE__ */ _(x, {
328
341
  htmlFor: "site",
329
342
  children: "Site"
330
- }), /* @__PURE__ */ _(T, {
343
+ }), /* @__PURE__ */ v(T, {
331
344
  value: V.siteId,
332
345
  onValueChange: (e) => H({
333
346
  ...V,
334
347
  siteId: e
335
348
  }),
336
- children: [/* @__PURE__ */ g(O, {
349
+ children: [/* @__PURE__ */ _(O, {
337
350
  id: "site",
338
- children: /* @__PURE__ */ g(k, { placeholder: "No site" })
339
- }), /* @__PURE__ */ _(E, { children: [/* @__PURE__ */ g(D, {
351
+ children: /* @__PURE__ */ _(k, { placeholder: "No site" })
352
+ }), /* @__PURE__ */ v(E, { children: [/* @__PURE__ */ _(D, {
340
353
  value: M,
341
354
  children: "No site"
342
- }), Y.map((e) => /* @__PURE__ */ g(D, {
355
+ }), Y.map((e) => /* @__PURE__ */ _(D, {
343
356
  value: e.id,
344
357
  children: e.name
345
358
  }, e.id))] })]
346
359
  })]
347
360
  })]
348
361
  }),
349
- /* @__PURE__ */ _("div", {
362
+ /* @__PURE__ */ v("div", {
350
363
  className: "grid gap-2 sm:grid-cols-2",
351
- children: [/* @__PURE__ */ _("div", {
364
+ children: [/* @__PURE__ */ v("div", {
352
365
  className: "grid gap-1.5",
353
- children: [/* @__PURE__ */ g(b, {
366
+ children: [/* @__PURE__ */ _(x, {
354
367
  htmlFor: "lat",
355
368
  children: "Latitude"
356
- }), /* @__PURE__ */ g(y, {
369
+ }), /* @__PURE__ */ _(b, {
357
370
  id: "lat",
358
371
  type: "number",
359
372
  step: "any",
@@ -364,12 +377,12 @@ function F() {
364
377
  }),
365
378
  placeholder: "12.9716"
366
379
  })]
367
- }), /* @__PURE__ */ _("div", {
380
+ }), /* @__PURE__ */ v("div", {
368
381
  className: "grid gap-1.5",
369
- children: [/* @__PURE__ */ g(b, {
382
+ children: [/* @__PURE__ */ _(x, {
370
383
  htmlFor: "lng",
371
384
  children: "Longitude"
372
- }), /* @__PURE__ */ g(y, {
385
+ }), /* @__PURE__ */ _(b, {
373
386
  id: "lng",
374
387
  type: "number",
375
388
  step: "any",
@@ -382,12 +395,12 @@ function F() {
382
395
  })]
383
396
  })]
384
397
  }),
385
- /* @__PURE__ */ _("div", {
398
+ /* @__PURE__ */ v("div", {
386
399
  className: "grid gap-1.5",
387
- children: [/* @__PURE__ */ g(b, {
400
+ children: [/* @__PURE__ */ _(x, {
388
401
  htmlFor: "notes",
389
402
  children: "Notes"
390
- }), /* @__PURE__ */ g(se, {
403
+ }), /* @__PURE__ */ _(le, {
391
404
  id: "notes",
392
405
  value: V.notes,
393
406
  onChange: (e) => H({
@@ -398,18 +411,28 @@ function F() {
398
411
  rows: 2
399
412
  })]
400
413
  }),
401
- U && /* @__PURE__ */ g("p", {
414
+ /* @__PURE__ */ _(u, {
415
+ id: "obsEvidence",
416
+ label: "Evidence photo (optional)",
417
+ value: V.evidence,
418
+ onChange: (e) => H({
419
+ ...V,
420
+ evidence: e
421
+ }),
422
+ hint: "Attach a photo, audio clip or PDF as supporting evidence."
423
+ }),
424
+ U && /* @__PURE__ */ _("p", {
402
425
  className: "text-sm text-status-error-text",
403
426
  children: U
404
427
  })
405
428
  ]
406
429
  }),
407
- /* @__PURE__ */ _(S, { children: [/* @__PURE__ */ g(v, {
430
+ /* @__PURE__ */ v(ce, { children: [/* @__PURE__ */ _(y, {
408
431
  variant: "outline",
409
432
  onClick: () => B(!1),
410
433
  children: "Cancel"
411
- }), /* @__PURE__ */ g(v, {
412
- onClick: he,
434
+ }), /* @__PURE__ */ _(y, {
435
+ onClick: _e,
413
436
  disabled: J.isPending,
414
437
  children: J.isPending ? "Recording…" : "Record observation"
415
438
  })] })
@@ -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 } 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 { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useEcoObservations,\n useEcoRecordObservation,\n useEcoSites,\n useEcoSpecies,\n useCursorPager,\n} from '../../hooks';\nimport {\n formatCoords,\n formatDateTime,\n formatEnumLabel,\n formatNumber,\n} from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\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}\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 };\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: '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) => (\n <span className=\"inline-flex rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(o.method)}\n </span>\n ),\n },\n {\n key: 'count',\n header: 'Count',\n align: 'right',\n sortValue: (o) => o.count,\n render: (o) => (\n <span className=\"tabular-nums text-text-secondary\">×{formatNumber(o.count)}</span>\n ),\n },\n {\n key: 'observedAt',\n header: 'Observed',\n align: 'right',\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: 'Coordinates',\n hideOnMobile: true,\n render: (o) => {\n const coords = formatCoords(o.latitude, o.longitude);\n return (\n <span\n className={cn(\n 'tabular-nums',\n coords === '—' ? 'text-text-tertiary' : 'text-text-secondary'\n )}\n >\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 });\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 <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select\n value={speciesFilter}\n onValueChange={(v) => {\n setSpeciesFilter(v);\n pager.reset();\n }}\n >\n <SelectTrigger className=\"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 <Select\n value={siteFilter}\n onValueChange={(v) => {\n setSiteFilter(v);\n pager.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 {siteRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\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 {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 } 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 { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { Pagination } from '../../components/Pagination';\nimport { EvidenceUpload } from '../../components/EvidenceUpload';\nimport { FileLink } from '../../components/FileLink';\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 { cn } from '../../utils/cn';\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: '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) => (\n <span className=\"inline-flex rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(o.method)}\n </span>\n ),\n },\n {\n key: 'count',\n header: 'Count',\n align: 'right',\n sortValue: (o) => o.count,\n render: (o) => (\n <span className=\"tabular-nums text-text-secondary\">×{formatNumber(o.count)}</span>\n ),\n },\n {\n key: 'observedAt',\n header: 'Observed',\n align: 'right',\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: 'Coordinates',\n hideOnMobile: true,\n render: (o) => {\n const coords = formatCoords(o.latitude, o.longitude);\n return (\n <span\n className={cn(\n 'tabular-nums',\n coords === '—' ? 'text-text-tertiary' : 'text-text-secondary'\n )}\n >\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 key: 'evidence',\n header: 'Evidence',\n hideOnMobile: true,\n render: (o) =>\n o.evidence?.id ? (\n <FileLink fileId={o.evidence.id} />\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 <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <Select\n value={speciesFilter}\n onValueChange={(v) => {\n setSpeciesFilter(v);\n pager.reset();\n }}\n >\n <SelectTrigger className=\"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 <Select\n value={siteFilter}\n onValueChange={(v) => {\n setSiteFilter(v);\n pager.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 {siteRows.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\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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"}