@burdenoff/microfe-ecoimpacthub 2026.704.2 → 2026.704.3
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/ecoimpacthub/components/EcoDiscussion.js +109 -0
- package/dist/ecoimpacthub/components/EcoDiscussion.js.map +1 -0
- package/dist/ecoimpacthub/components/EcoTags.js +149 -0
- package/dist/ecoimpacthub/components/EcoTags.js.map +1 -0
- package/dist/ecoimpacthub/components/EvidenceUpload.js +93 -0
- package/dist/ecoimpacthub/components/EvidenceUpload.js.map +1 -0
- package/dist/ecoimpacthub/components/FileLink.js +36 -0
- package/dist/ecoimpacthub/components/FileLink.js.map +1 -0
- package/dist/ecoimpacthub/components/PageLayout.js +2 -2
- package/dist/ecoimpacthub/components/PageLayout.js.map +1 -1
- package/dist/ecoimpacthub/hooks/index.js +3 -0
- package/dist/ecoimpacthub/hooks/useEcoComments.js +45 -0
- package/dist/ecoimpacthub/hooks/useEcoComments.js.map +1 -0
- package/dist/ecoimpacthub/hooks/useEcoFiles.js +81 -0
- package/dist/ecoimpacthub/hooks/useEcoFiles.js.map +1 -0
- package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js +169 -166
- package/dist/ecoimpacthub/hooks/useEcoImpactHubApi.js.map +1 -1
- package/dist/ecoimpacthub/hooks/useEcoTags.js +89 -0
- package/dist/ecoimpacthub/hooks/useEcoTags.js.map +1 -0
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +124 -101
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +153 -126
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +241 -230
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +260 -225
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
- 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
|
|
6
|
-
import { useEcoObservations as
|
|
7
|
-
import { useCursorPager as
|
|
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
|
|
10
|
-
import { Pagination as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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
|
|
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] =
|
|
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 =
|
|
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]),
|
|
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]),
|
|
63
|
+
}, [X]), ge = [
|
|
61
64
|
{
|
|
62
65
|
key: "species",
|
|
63
66
|
header: "Species",
|
|
64
|
-
render: (e) => /* @__PURE__ */
|
|
67
|
+
render: (e) => /* @__PURE__ */ v("div", {
|
|
65
68
|
className: "min-w-0",
|
|
66
|
-
children: [/* @__PURE__ */
|
|
69
|
+
children: [/* @__PURE__ */ _("p", {
|
|
67
70
|
className: "font-semibold text-text-primary",
|
|
68
|
-
children: e.species?.commonName ??
|
|
69
|
-
}), e.species?.scientificName && /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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 ?
|
|
109
|
-
return t ? /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
-
},
|
|
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
|
-
},
|
|
167
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
183
|
+
actions: /* @__PURE__ */ v(y, {
|
|
171
184
|
onClick: Q,
|
|
172
|
-
children: [/* @__PURE__ */
|
|
185
|
+
children: [/* @__PURE__ */ _(h, { className: "mr-1.5 h-4 w-4" }), " Record observation"]
|
|
173
186
|
}),
|
|
174
187
|
children: [
|
|
175
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ v("div", {
|
|
176
189
|
className: "mb-5 flex flex-wrap items-center gap-3",
|
|
177
|
-
children: [/* @__PURE__ */
|
|
190
|
+
children: [/* @__PURE__ */ v(T, {
|
|
178
191
|
value: N,
|
|
179
192
|
onValueChange: (e) => {
|
|
180
193
|
F(e), R.reset();
|
|
181
194
|
},
|
|
182
|
-
children: [/* @__PURE__ */
|
|
195
|
+
children: [/* @__PURE__ */ _(O, {
|
|
183
196
|
className: "w-52",
|
|
184
|
-
children: /* @__PURE__ */
|
|
185
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
201
|
+
}), X.map((e) => /* @__PURE__ */ _(D, {
|
|
189
202
|
value: e.id,
|
|
190
203
|
children: e.commonName
|
|
191
204
|
}, e.id))] })]
|
|
192
|
-
}), /* @__PURE__ */
|
|
205
|
+
}), /* @__PURE__ */ v(T, {
|
|
193
206
|
value: I,
|
|
194
207
|
onValueChange: (e) => {
|
|
195
208
|
L(e), R.reset();
|
|
196
209
|
},
|
|
197
|
-
children: [/* @__PURE__ */
|
|
210
|
+
children: [/* @__PURE__ */ _(O, {
|
|
198
211
|
className: "w-52",
|
|
199
|
-
children: /* @__PURE__ */
|
|
200
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
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
|
-
|
|
222
|
+
de ? /* @__PURE__ */ _(ne, { label: "Loading observations…" }) : fe ? /* @__PURE__ */ _(te, {
|
|
210
223
|
message: "Couldn't load observations.",
|
|
211
|
-
onRetry: () =>
|
|
212
|
-
}) : Z.length === 0 ? /* @__PURE__ */
|
|
224
|
+
onRetry: () => pe()
|
|
225
|
+
}) : Z.length === 0 ? /* @__PURE__ */ _(re, {
|
|
213
226
|
illustration: "empty-data",
|
|
214
|
-
icon: /* @__PURE__ */
|
|
227
|
+
icon: /* @__PURE__ */ _(m, { className: "h-10 w-10" }),
|
|
215
228
|
title: "No observations yet",
|
|
216
|
-
description:
|
|
217
|
-
action: /* @__PURE__ */
|
|
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__ */
|
|
232
|
+
children: [/* @__PURE__ */ _(h, { className: "mr-1.5 h-4 w-4" }), " Record observation"]
|
|
220
233
|
})
|
|
221
|
-
}) : /* @__PURE__ */
|
|
222
|
-
columns:
|
|
234
|
+
}) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(ae, {
|
|
235
|
+
columns: ge,
|
|
223
236
|
rows: Z,
|
|
224
237
|
rowKey: (e) => e.id
|
|
225
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
245
|
+
/* @__PURE__ */ _(oe, {
|
|
233
246
|
open: z,
|
|
234
247
|
onOpenChange: (e) => {
|
|
235
248
|
B(e), e || $();
|
|
236
249
|
},
|
|
237
|
-
children: /* @__PURE__ */
|
|
238
|
-
/* @__PURE__ */
|
|
239
|
-
/* @__PURE__ */
|
|
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__ */
|
|
255
|
+
/* @__PURE__ */ v("div", {
|
|
243
256
|
className: "grid gap-1.5",
|
|
244
257
|
children: [
|
|
245
|
-
/* @__PURE__ */
|
|
258
|
+
/* @__PURE__ */ _(x, {
|
|
246
259
|
htmlFor: "species",
|
|
247
260
|
children: "Species *"
|
|
248
261
|
}),
|
|
249
|
-
/* @__PURE__ */
|
|
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__ */
|
|
268
|
+
children: [/* @__PURE__ */ _(O, {
|
|
256
269
|
id: "species",
|
|
257
|
-
children: /* @__PURE__ */
|
|
258
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
282
|
+
/* @__PURE__ */ v("div", {
|
|
270
283
|
className: "grid gap-2 sm:grid-cols-2",
|
|
271
|
-
children: [/* @__PURE__ */
|
|
284
|
+
children: [/* @__PURE__ */ v("div", {
|
|
272
285
|
className: "grid gap-1.5",
|
|
273
|
-
children: [/* @__PURE__ */
|
|
286
|
+
children: [/* @__PURE__ */ _(x, {
|
|
274
287
|
htmlFor: "observedAt",
|
|
275
288
|
children: "Observed at"
|
|
276
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
298
|
+
}), /* @__PURE__ */ v("div", {
|
|
286
299
|
className: "grid gap-1.5",
|
|
287
|
-
children: [/* @__PURE__ */
|
|
300
|
+
children: [/* @__PURE__ */ _(x, {
|
|
288
301
|
htmlFor: "count",
|
|
289
302
|
children: "Count"
|
|
290
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
317
|
+
/* @__PURE__ */ v("div", {
|
|
305
318
|
className: "grid gap-2 sm:grid-cols-2",
|
|
306
|
-
children: [/* @__PURE__ */
|
|
319
|
+
children: [/* @__PURE__ */ v("div", {
|
|
307
320
|
className: "grid gap-1.5",
|
|
308
|
-
children: [/* @__PURE__ */
|
|
321
|
+
children: [/* @__PURE__ */ _(x, {
|
|
309
322
|
htmlFor: "method",
|
|
310
323
|
children: "Method"
|
|
311
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
330
|
+
children: [/* @__PURE__ */ _(O, {
|
|
318
331
|
id: "method",
|
|
319
|
-
children: /* @__PURE__ */
|
|
320
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
338
|
+
}), /* @__PURE__ */ v("div", {
|
|
326
339
|
className: "grid gap-1.5",
|
|
327
|
-
children: [/* @__PURE__ */
|
|
340
|
+
children: [/* @__PURE__ */ _(x, {
|
|
328
341
|
htmlFor: "site",
|
|
329
342
|
children: "Site"
|
|
330
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
349
|
+
children: [/* @__PURE__ */ _(O, {
|
|
337
350
|
id: "site",
|
|
338
|
-
children: /* @__PURE__ */
|
|
339
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
362
|
+
/* @__PURE__ */ v("div", {
|
|
350
363
|
className: "grid gap-2 sm:grid-cols-2",
|
|
351
|
-
children: [/* @__PURE__ */
|
|
364
|
+
children: [/* @__PURE__ */ v("div", {
|
|
352
365
|
className: "grid gap-1.5",
|
|
353
|
-
children: [/* @__PURE__ */
|
|
366
|
+
children: [/* @__PURE__ */ _(x, {
|
|
354
367
|
htmlFor: "lat",
|
|
355
368
|
children: "Latitude"
|
|
356
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
380
|
+
}), /* @__PURE__ */ v("div", {
|
|
368
381
|
className: "grid gap-1.5",
|
|
369
|
-
children: [/* @__PURE__ */
|
|
382
|
+
children: [/* @__PURE__ */ _(x, {
|
|
370
383
|
htmlFor: "lng",
|
|
371
384
|
children: "Longitude"
|
|
372
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
398
|
+
/* @__PURE__ */ v("div", {
|
|
386
399
|
className: "grid gap-1.5",
|
|
387
|
-
children: [/* @__PURE__ */
|
|
400
|
+
children: [/* @__PURE__ */ _(x, {
|
|
388
401
|
htmlFor: "notes",
|
|
389
402
|
children: "Notes"
|
|
390
|
-
}), /* @__PURE__ */
|
|
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
|
-
|
|
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__ */
|
|
430
|
+
/* @__PURE__ */ v(ce, { children: [/* @__PURE__ */ _(y, {
|
|
408
431
|
variant: "outline",
|
|
409
432
|
onClick: () => B(!1),
|
|
410
433
|
children: "Cancel"
|
|
411
|
-
}), /* @__PURE__ */
|
|
412
|
-
onClick:
|
|
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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"}
|