anton-bakker-feedback 0.1.0 → 0.2.0

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 (68) hide show
  1. package/README.md +78 -0
  2. package/dist/react/admin/FeedbackDetailPage.d.ts +30 -0
  3. package/dist/react/admin/FeedbackDetailPage.d.ts.map +1 -0
  4. package/dist/react/admin/FeedbackUpdatePage.d.ts +34 -0
  5. package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -0
  6. package/dist/react/admin/__tests__/FeedbackDetailPage.test.d.ts +2 -0
  7. package/dist/react/admin/__tests__/FeedbackDetailPage.test.d.ts.map +1 -0
  8. package/dist/react/admin/__tests__/FeedbackUpdatePage.integration.test.d.ts +2 -0
  9. package/dist/react/admin/__tests__/FeedbackUpdatePage.integration.test.d.ts.map +1 -0
  10. package/dist/react/admin/__tests__/FeedbackUpdatePage.test.d.ts +2 -0
  11. package/dist/react/admin/__tests__/FeedbackUpdatePage.test.d.ts.map +1 -0
  12. package/dist/react/admin/components/BrowserContext.d.ts +32 -0
  13. package/dist/react/admin/components/BrowserContext.d.ts.map +1 -0
  14. package/dist/react/admin/components/DataTable.d.ts +55 -0
  15. package/dist/react/admin/components/DataTable.d.ts.map +1 -0
  16. package/dist/react/admin/components/DialogueHistory.d.ts +27 -0
  17. package/dist/react/admin/components/DialogueHistory.d.ts.map +1 -0
  18. package/dist/react/admin/components/EditableFields.d.ts +126 -0
  19. package/dist/react/admin/components/EditableFields.d.ts.map +1 -0
  20. package/dist/react/admin/components/FeedbackListPage.d.ts +19 -0
  21. package/dist/react/admin/components/FeedbackListPage.d.ts.map +1 -0
  22. package/dist/react/admin/components/FeedbackListPage.test.d.ts +2 -0
  23. package/dist/react/admin/components/FeedbackListPage.test.d.ts.map +1 -0
  24. package/dist/react/admin/components/FilterControls.d.ts +28 -0
  25. package/dist/react/admin/components/FilterControls.d.ts.map +1 -0
  26. package/dist/react/admin/components/NavigationLink.d.ts +31 -0
  27. package/dist/react/admin/components/NavigationLink.d.ts.map +1 -0
  28. package/dist/react/admin/components/QuickActions.d.ts +43 -0
  29. package/dist/react/admin/components/QuickActions.d.ts.map +1 -0
  30. package/dist/react/admin/components/ScreenshotViewer.d.ts +38 -0
  31. package/dist/react/admin/components/ScreenshotViewer.d.ts.map +1 -0
  32. package/dist/react/admin/components/SortControls.d.ts +29 -0
  33. package/dist/react/admin/components/SortControls.d.ts.map +1 -0
  34. package/dist/react/admin/components/StatusBadge.d.ts +23 -0
  35. package/dist/react/admin/components/StatusBadge.d.ts.map +1 -0
  36. package/dist/react/admin/components/TypeBadge.d.ts +23 -0
  37. package/dist/react/admin/components/TypeBadge.d.ts.map +1 -0
  38. package/dist/react/admin/components/__tests__/BrowserContext.test.d.ts +2 -0
  39. package/dist/react/admin/components/__tests__/BrowserContext.test.d.ts.map +1 -0
  40. package/dist/react/admin/components/__tests__/DialogueHistory.test.d.ts +2 -0
  41. package/dist/react/admin/components/__tests__/DialogueHistory.test.d.ts.map +1 -0
  42. package/dist/react/admin/components/__tests__/EditableFields.test.d.ts +2 -0
  43. package/dist/react/admin/components/__tests__/EditableFields.test.d.ts.map +1 -0
  44. package/dist/react/admin/components/__tests__/QuickActions.test.d.ts +2 -0
  45. package/dist/react/admin/components/__tests__/QuickActions.test.d.ts.map +1 -0
  46. package/dist/react/admin/components/__tests__/ScreenshotViewer.test.d.ts +2 -0
  47. package/dist/react/admin/components/__tests__/ScreenshotViewer.test.d.ts.map +1 -0
  48. package/dist/react/admin/components/__tests__/badges.test.d.ts +2 -0
  49. package/dist/react/admin/components/__tests__/badges.test.d.ts.map +1 -0
  50. package/dist/react/admin/components/__tests__/controls.test.d.ts +2 -0
  51. package/dist/react/admin/components/__tests__/controls.test.d.ts.map +1 -0
  52. package/dist/react/admin/components/__tests__/table-navigation.test.d.ts +2 -0
  53. package/dist/react/admin/components/__tests__/table-navigation.test.d.ts.map +1 -0
  54. package/dist/react/admin/components/index.d.ts +13 -0
  55. package/dist/react/admin/components/index.d.ts.map +1 -0
  56. package/dist/react/admin/hooks/useFeedbackFilters.d.ts +31 -0
  57. package/dist/react/admin/hooks/useFeedbackFilters.d.ts.map +1 -0
  58. package/dist/react/admin/hooks/useFeedbackSort.d.ts +42 -0
  59. package/dist/react/admin/hooks/useFeedbackSort.d.ts.map +1 -0
  60. package/dist/react/admin/hooks/useStatusTransition.d.ts +18 -0
  61. package/dist/react/admin/hooks/useStatusTransition.d.ts.map +1 -0
  62. package/dist/react/admin/index.d.ts +6 -0
  63. package/dist/react/admin/index.d.ts.map +1 -0
  64. package/dist/react/admin/index.js +1313 -0
  65. package/dist/react/admin/index.js.map +1 -0
  66. package/dist/react/admin/types.d.ts +38 -0
  67. package/dist/react/admin/types.d.ts.map +1 -0
  68. package/package.json +8 -1
@@ -0,0 +1,1313 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import React, { useState, useCallback, useEffect } from "react";
3
+ const DEFAULT_FILTERS = {
4
+ status: "",
5
+ type: "",
6
+ environment: ""
7
+ };
8
+ function useFeedbackFilters() {
9
+ const [filters, setFiltersState] = useState(DEFAULT_FILTERS);
10
+ const setFilters = useCallback((next) => {
11
+ setFiltersState(next);
12
+ }, []);
13
+ const clearFilters = useCallback(() => {
14
+ setFiltersState(DEFAULT_FILTERS);
15
+ }, []);
16
+ const applyFilters = useCallback(
17
+ (items) => {
18
+ return items.filter((item) => {
19
+ if (filters.status !== "" && item.status !== filters.status) {
20
+ return false;
21
+ }
22
+ if (filters.type !== "" && item.type !== filters.type) {
23
+ return false;
24
+ }
25
+ if (filters.environment !== "" && item.environment !== filters.environment) {
26
+ return false;
27
+ }
28
+ return true;
29
+ });
30
+ },
31
+ [filters]
32
+ );
33
+ return { filters, setFilters, clearFilters, applyFilters };
34
+ }
35
+ const DEFAULT_SORT = {
36
+ field: "priority",
37
+ direction: "desc"
38
+ };
39
+ function compareStrings(a, b, direction) {
40
+ const strA = a ?? "";
41
+ const strB = b ?? "";
42
+ const cmp = strA.localeCompare(strB);
43
+ return direction === "asc" ? cmp : -cmp;
44
+ }
45
+ function compareNumbers(a, b, direction) {
46
+ const numA = a ?? 0;
47
+ const numB = b ?? 0;
48
+ const cmp = numA - numB;
49
+ return direction === "asc" ? cmp : -cmp;
50
+ }
51
+ function compareDates(a, b, direction) {
52
+ const timeA = a ? new Date(a).getTime() : 0;
53
+ const timeB = b ? new Date(b).getTime() : 0;
54
+ const cmp = timeA - timeB;
55
+ return direction === "asc" ? cmp : -cmp;
56
+ }
57
+ function sortFeedbackItems(items, sort) {
58
+ const { field, direction } = sort;
59
+ return [...items].sort((a, b) => {
60
+ switch (field) {
61
+ case "priority":
62
+ return compareNumbers(a.priority, b.priority, direction);
63
+ case "submittedAt":
64
+ return compareDates(a.submittedAt, b.submittedAt, direction);
65
+ case "status":
66
+ return compareStrings(a.status, b.status, direction);
67
+ case "type":
68
+ return compareStrings(a.type, b.type, direction);
69
+ case "reporterCount":
70
+ return compareNumbers(a.reporterCount, b.reporterCount, direction);
71
+ default:
72
+ return 0;
73
+ }
74
+ });
75
+ }
76
+ function useFeedbackSort() {
77
+ const [sort, setSortState] = useState(DEFAULT_SORT);
78
+ const setSortField = useCallback((field) => {
79
+ setSortState((prev) => {
80
+ if (prev.field === field) {
81
+ return { field, direction: prev.direction === "asc" ? "desc" : "asc" };
82
+ }
83
+ return { field, direction: "desc" };
84
+ });
85
+ }, []);
86
+ const setSort = useCallback((next) => {
87
+ setSortState(next);
88
+ }, []);
89
+ const applySort = useCallback(
90
+ (items) => sortFeedbackItems(items, sort),
91
+ [sort]
92
+ );
93
+ return { sort, setSortField, setSort, applySort };
94
+ }
95
+ const STATUS_LABELS$1 = {
96
+ open: "Open",
97
+ approved: "Approved",
98
+ deferred: "Deferred",
99
+ resolved: "Resolved",
100
+ wontfix: "Won't fix"
101
+ };
102
+ function StatusBadge({ status, className }) {
103
+ return /* @__PURE__ */ jsx("span", { className, "data-status": status, children: STATUS_LABELS$1[status] ?? status });
104
+ }
105
+ const TYPE_LABELS$1 = {
106
+ bug: "Bug",
107
+ feature: "Feature",
108
+ ux: "UX issue",
109
+ other: "Other"
110
+ };
111
+ function TypeBadge({ type, className }) {
112
+ return /* @__PURE__ */ jsx("span", { className, "data-type": type, children: TYPE_LABELS$1[type] ?? type });
113
+ }
114
+ const STATUS_OPTIONS = [
115
+ { value: "", label: "All" },
116
+ { value: "open", label: "Open" },
117
+ { value: "approved", label: "Approved" },
118
+ { value: "deferred", label: "Deferred" },
119
+ { value: "resolved", label: "Resolved" },
120
+ { value: "wontfix", label: "Won't fix" }
121
+ ];
122
+ const TYPE_OPTIONS = [
123
+ { value: "", label: "All" },
124
+ { value: "bug", label: "Bug" },
125
+ { value: "feature", label: "Feature" },
126
+ { value: "ux", label: "UX issue" },
127
+ { value: "other", label: "Other" }
128
+ ];
129
+ const ENVIRONMENT_OPTIONS = [
130
+ { value: "", label: "All" },
131
+ { value: "development", label: "Development" },
132
+ { value: "staging", label: "Staging" },
133
+ { value: "production", label: "Production" }
134
+ ];
135
+ function FilterControls({ filters, onChange, className }) {
136
+ const statusId = "feedback-admin-filter-status";
137
+ const typeId = "feedback-admin-filter-type";
138
+ const environmentId = "feedback-admin-filter-environment";
139
+ const handleStatusChange = (e) => {
140
+ onChange({ ...filters, status: e.target.value });
141
+ };
142
+ const handleTypeChange = (e) => {
143
+ onChange({ ...filters, type: e.target.value });
144
+ };
145
+ const handleEnvironmentChange = (e) => {
146
+ onChange({ ...filters, environment: e.target.value });
147
+ };
148
+ return /* @__PURE__ */ jsxs("div", { className, role: "search", "aria-label": "Filter feedback items", children: [
149
+ /* @__PURE__ */ jsxs("div", { className: "feedback-admin-filter__group", children: [
150
+ /* @__PURE__ */ jsx("label", { htmlFor: statusId, children: "Status" }),
151
+ /* @__PURE__ */ jsx(
152
+ "select",
153
+ {
154
+ id: statusId,
155
+ value: filters.status,
156
+ onChange: handleStatusChange,
157
+ "aria-label": "Filter by status",
158
+ "aria-describedby": `${statusId}-desc`,
159
+ children: STATUS_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
160
+ }
161
+ ),
162
+ /* @__PURE__ */ jsx("span", { id: `${statusId}-desc`, className: "feedback-admin-sr-only", children: "Filter feedback items by lifecycle status" })
163
+ ] }),
164
+ /* @__PURE__ */ jsxs("div", { className: "feedback-admin-filter__group", children: [
165
+ /* @__PURE__ */ jsx("label", { htmlFor: typeId, children: "Type" }),
166
+ /* @__PURE__ */ jsx(
167
+ "select",
168
+ {
169
+ id: typeId,
170
+ value: filters.type,
171
+ onChange: handleTypeChange,
172
+ "aria-label": "Filter by type",
173
+ "aria-describedby": `${typeId}-desc`,
174
+ children: TYPE_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
175
+ }
176
+ ),
177
+ /* @__PURE__ */ jsx("span", { id: `${typeId}-desc`, className: "feedback-admin-sr-only", children: "Filter feedback items by type" })
178
+ ] }),
179
+ /* @__PURE__ */ jsxs("div", { className: "feedback-admin-filter__group", children: [
180
+ /* @__PURE__ */ jsx("label", { htmlFor: environmentId, children: "Environment" }),
181
+ /* @__PURE__ */ jsx(
182
+ "select",
183
+ {
184
+ id: environmentId,
185
+ value: filters.environment,
186
+ onChange: handleEnvironmentChange,
187
+ "aria-label": "Filter by environment",
188
+ "aria-describedby": `${environmentId}-desc`,
189
+ children: ENVIRONMENT_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
190
+ }
191
+ ),
192
+ /* @__PURE__ */ jsx("span", { id: `${environmentId}-desc`, className: "feedback-admin-sr-only", children: "Filter feedback items by environment" })
193
+ ] })
194
+ ] });
195
+ }
196
+ function formatDate(iso) {
197
+ try {
198
+ return new Date(iso).toLocaleDateString(void 0, {
199
+ year: "numeric",
200
+ month: "short",
201
+ day: "numeric"
202
+ });
203
+ } catch {
204
+ return iso;
205
+ }
206
+ }
207
+ function SortIndicator({
208
+ active,
209
+ direction,
210
+ className
211
+ }) {
212
+ if (!active) {
213
+ return /* @__PURE__ */ jsx("span", { className, "aria-hidden": "true", children: " ↕" });
214
+ }
215
+ return /* @__PURE__ */ jsx("span", { className, "aria-hidden": "true", children: direction === "asc" ? " ↑" : " ↓" });
216
+ }
217
+ function FeedbackListPage({
218
+ items,
219
+ loading = false,
220
+ onNavigate,
221
+ className,
222
+ tableClassName,
223
+ filterClassName,
224
+ emptyStateMessage = "No feedback items found."
225
+ }) {
226
+ const { filters, setFilters, clearFilters, applyFilters } = useFeedbackFilters();
227
+ const { sort, setSortField, applySort } = useFeedbackSort();
228
+ const visibleItems = applySort(applyFilters(items));
229
+ function thProps(field) {
230
+ return {
231
+ scope: "col",
232
+ className: `feedback-admin-list__th feedback-admin-list__th--sortable${sort.field === field ? " feedback-admin-list__th--sorted" : ""}`,
233
+ onClick: () => setSortField(field),
234
+ onKeyDown: (e) => {
235
+ if (e.key === "Enter" || e.key === " ") {
236
+ e.preventDefault();
237
+ setSortField(field);
238
+ }
239
+ },
240
+ tabIndex: 0,
241
+ role: "columnheader",
242
+ "aria-sort": sort.field === field ? sort.direction === "asc" ? "ascending" : "descending" : "none"
243
+ };
244
+ }
245
+ const renderBody = () => {
246
+ if (loading) {
247
+ return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
248
+ "td",
249
+ {
250
+ colSpan: 7,
251
+ className: "feedback-admin-list__loading",
252
+ "aria-live": "polite",
253
+ "aria-busy": "true",
254
+ children: "Loading…"
255
+ }
256
+ ) });
257
+ }
258
+ if (visibleItems.length === 0) {
259
+ return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: 7, className: "feedback-admin-list__empty", "aria-live": "polite", children: emptyStateMessage }) });
260
+ }
261
+ return visibleItems.map((item) => /* @__PURE__ */ jsxs(
262
+ "tr",
263
+ {
264
+ className: "feedback-admin-list__row feedback-admin-list__row--clickable",
265
+ onClick: () => onNavigate(`/feedback/${item.id}`),
266
+ onKeyDown: (e) => {
267
+ if (e.key === "Enter" || e.key === " ") {
268
+ e.preventDefault();
269
+ onNavigate(`/feedback/${item.id}`);
270
+ }
271
+ },
272
+ tabIndex: 0,
273
+ role: "button",
274
+ "aria-label": `View feedback item ${item.id}`,
275
+ children: [
276
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--title", children: item.content }),
277
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--type", children: /* @__PURE__ */ jsx(
278
+ TypeBadge,
279
+ {
280
+ type: item.type,
281
+ className: `feedback-admin-badge feedback-admin-badge--type feedback-admin-badge--${item.type}`
282
+ }
283
+ ) }),
284
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--status", children: /* @__PURE__ */ jsx(
285
+ StatusBadge,
286
+ {
287
+ status: item.status,
288
+ className: `feedback-admin-badge feedback-admin-badge--status feedback-admin-badge--${item.status}`
289
+ }
290
+ ) }),
291
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--priority", children: item.priority }),
292
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--reporters", children: item.reporterCount.toLocaleString() }),
293
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--environment", children: item.environment ?? "—" }),
294
+ /* @__PURE__ */ jsx("td", { className: "feedback-admin-list__td feedback-admin-list__td--date", children: formatDate(item.submittedAt) })
295
+ ]
296
+ },
297
+ item.id
298
+ ));
299
+ };
300
+ return /* @__PURE__ */ jsxs("div", { className: `feedback-admin-list${className ? ` ${className}` : ""}`, children: [
301
+ /* @__PURE__ */ jsxs("div", { className: `feedback-admin-list__filters${filterClassName ? ` ${filterClassName}` : ""}`, children: [
302
+ /* @__PURE__ */ jsx(
303
+ FilterControls,
304
+ {
305
+ filters,
306
+ onChange: setFilters,
307
+ className: "feedback-admin-filter-controls"
308
+ }
309
+ ),
310
+ /* @__PURE__ */ jsx(
311
+ "button",
312
+ {
313
+ type: "button",
314
+ className: "feedback-admin-list__clear-btn",
315
+ onClick: clearFilters,
316
+ "aria-label": "Clear all filters",
317
+ children: "Clear filters"
318
+ }
319
+ )
320
+ ] }),
321
+ /* @__PURE__ */ jsx("div", { className: `feedback-admin-list__table-wrapper${tableClassName ? ` ${tableClassName}` : ""}`, children: /* @__PURE__ */ jsxs("table", { className: "feedback-admin-list__table", role: "grid", "aria-label": "Feedback items", children: [
322
+ /* @__PURE__ */ jsx("thead", { className: "feedback-admin-list__thead", children: /* @__PURE__ */ jsxs("tr", { children: [
323
+ /* @__PURE__ */ jsx(
324
+ "th",
325
+ {
326
+ scope: "col",
327
+ className: "feedback-admin-list__th",
328
+ children: "Title"
329
+ }
330
+ ),
331
+ /* @__PURE__ */ jsxs("th", { ...thProps("type"), children: [
332
+ "Type",
333
+ /* @__PURE__ */ jsx(
334
+ SortIndicator,
335
+ {
336
+ active: sort.field === "type",
337
+ direction: sort.direction,
338
+ className: "feedback-admin-list__sort-icon"
339
+ }
340
+ )
341
+ ] }),
342
+ /* @__PURE__ */ jsxs("th", { ...thProps("status"), children: [
343
+ "Status",
344
+ /* @__PURE__ */ jsx(
345
+ SortIndicator,
346
+ {
347
+ active: sort.field === "status",
348
+ direction: sort.direction,
349
+ className: "feedback-admin-list__sort-icon"
350
+ }
351
+ )
352
+ ] }),
353
+ /* @__PURE__ */ jsxs("th", { ...thProps("priority"), children: [
354
+ "Priority",
355
+ /* @__PURE__ */ jsx(
356
+ SortIndicator,
357
+ {
358
+ active: sort.field === "priority",
359
+ direction: sort.direction,
360
+ className: "feedback-admin-list__sort-icon"
361
+ }
362
+ )
363
+ ] }),
364
+ /* @__PURE__ */ jsxs("th", { ...thProps("reporterCount"), children: [
365
+ "Reporters",
366
+ /* @__PURE__ */ jsx(
367
+ SortIndicator,
368
+ {
369
+ active: sort.field === "reporterCount",
370
+ direction: sort.direction,
371
+ className: "feedback-admin-list__sort-icon"
372
+ }
373
+ )
374
+ ] }),
375
+ /* @__PURE__ */ jsx(
376
+ "th",
377
+ {
378
+ scope: "col",
379
+ className: "feedback-admin-list__th",
380
+ children: "Environment"
381
+ }
382
+ ),
383
+ /* @__PURE__ */ jsxs("th", { ...thProps("submittedAt"), children: [
384
+ "Date",
385
+ /* @__PURE__ */ jsx(
386
+ SortIndicator,
387
+ {
388
+ active: sort.field === "submittedAt",
389
+ direction: sort.direction,
390
+ className: "feedback-admin-list__sort-icon"
391
+ }
392
+ )
393
+ ] })
394
+ ] }) }),
395
+ /* @__PURE__ */ jsx("tbody", { className: "feedback-admin-list__tbody", children: renderBody() })
396
+ ] }) })
397
+ ] });
398
+ }
399
+ function DialogueHistory({
400
+ dialogue,
401
+ className
402
+ }) {
403
+ if (!dialogue || dialogue.length === 0) {
404
+ return /* @__PURE__ */ jsx("div", { className, "data-testid": "dialogue-history-empty", children: /* @__PURE__ */ jsx("p", { className: "feedback-admin-dialogue__empty", children: "No dialogue recorded." }) });
405
+ }
406
+ return /* @__PURE__ */ jsx("div", { className, "data-testid": "dialogue-history", children: /* @__PURE__ */ jsx("ol", { className: "feedback-admin-dialogue__list", "aria-label": "Dialogue history", children: dialogue.map((entry, index) => {
407
+ if (!entry || typeof entry !== "object") {
408
+ return null;
409
+ }
410
+ const role = entry.role === "assistant" ? "assistant" : "user";
411
+ const content = typeof entry.content === "string" ? entry.content : "";
412
+ const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : "";
413
+ let displayTime = "";
414
+ if (timestamp) {
415
+ try {
416
+ displayTime = new Date(timestamp).toLocaleString();
417
+ } catch {
418
+ displayTime = timestamp;
419
+ }
420
+ }
421
+ return /* @__PURE__ */ jsxs(
422
+ "li",
423
+ {
424
+ className: "feedback-admin-dialogue__turn",
425
+ "data-role": role,
426
+ children: [
427
+ /* @__PURE__ */ jsxs("header", { className: "feedback-admin-dialogue__turn-header", children: [
428
+ /* @__PURE__ */ jsx(
429
+ "span",
430
+ {
431
+ className: "feedback-admin-dialogue__turn-role",
432
+ "aria-label": `${role} message`,
433
+ children: role === "user" ? "User" : "Assistant"
434
+ }
435
+ ),
436
+ displayTime && /* @__PURE__ */ jsx(
437
+ "time",
438
+ {
439
+ className: "feedback-admin-dialogue__turn-time",
440
+ dateTime: timestamp,
441
+ children: displayTime
442
+ }
443
+ )
444
+ ] }),
445
+ /* @__PURE__ */ jsx("div", { className: "feedback-admin-dialogue__turn-content", children: content.split("\n").map((line, lineIndex) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
446
+ line,
447
+ lineIndex < content.split("\n").length - 1 && /* @__PURE__ */ jsx("br", {})
448
+ ] }, lineIndex)) })
449
+ ]
450
+ },
451
+ `dialogue-${index}`
452
+ );
453
+ }) }) });
454
+ }
455
+ function ScreenshotViewer({
456
+ screenshotS3Key,
457
+ generateScreenshotUrl,
458
+ alt = "Feedback screenshot",
459
+ className
460
+ }) {
461
+ const [imageUrl, setImageUrl] = useState(null);
462
+ const [loading, setLoading] = useState(false);
463
+ const [error, setError] = useState(null);
464
+ useEffect(() => {
465
+ if (!screenshotS3Key) {
466
+ setImageUrl(null);
467
+ setLoading(false);
468
+ setError(null);
469
+ return;
470
+ }
471
+ if (!generateScreenshotUrl) {
472
+ setImageUrl(null);
473
+ setLoading(false);
474
+ setError(null);
475
+ return;
476
+ }
477
+ setLoading(true);
478
+ setError(null);
479
+ generateScreenshotUrl(screenshotS3Key).then((url) => {
480
+ setImageUrl(url);
481
+ setLoading(false);
482
+ }).catch(() => {
483
+ setImageUrl(null);
484
+ setLoading(false);
485
+ setError("Failed to load screenshot.");
486
+ });
487
+ }, [screenshotS3Key, generateScreenshotUrl]);
488
+ return /* @__PURE__ */ jsxs("div", { className, "data-testid": "screenshot-viewer", children: [
489
+ loading && /* @__PURE__ */ jsx("p", { className: "feedback-admin-screenshot__loading", children: "Loading screenshot…" }),
490
+ !loading && imageUrl && /* @__PURE__ */ jsx(
491
+ "img",
492
+ {
493
+ src: imageUrl,
494
+ alt,
495
+ className: "feedback-admin-screenshot__image"
496
+ }
497
+ ),
498
+ !loading && !imageUrl && screenshotS3Key && !generateScreenshotUrl && /* @__PURE__ */ jsx("p", { className: "feedback-admin-screenshot__configure", children: "Configure generateScreenshotUrl to display screenshot." }),
499
+ !loading && !imageUrl && screenshotS3Key && generateScreenshotUrl && error && /* @__PURE__ */ jsx("p", { className: "feedback-admin-screenshot__error", children: error }),
500
+ !loading && !screenshotS3Key && /* @__PURE__ */ jsx("p", { className: "feedback-admin-screenshot__missing", children: "No screenshot captured." })
501
+ ] });
502
+ }
503
+ function BrowserContext({
504
+ context,
505
+ className
506
+ }) {
507
+ if (!context) {
508
+ return /* @__PURE__ */ jsx("div", { className, "data-testid": "browser-context-empty", children: /* @__PURE__ */ jsx("p", { className: "feedback-admin-context__empty", children: "No browser context available." }) });
509
+ }
510
+ const { url, pageTitle, userAgent, viewport, locale, timezone } = context;
511
+ return /* @__PURE__ */ jsx("div", { className, "data-testid": "browser-context", children: /* @__PURE__ */ jsxs("dl", { className: "feedback-admin-context__list", children: [
512
+ url && /* @__PURE__ */ jsxs(Fragment, { children: [
513
+ /* @__PURE__ */ jsx("dt", { className: "feedback-admin-context__term", children: "URL" }),
514
+ /* @__PURE__ */ jsx("dd", { className: "feedback-admin-context__value feedback-admin-context__value--url", children: url })
515
+ ] }),
516
+ pageTitle && /* @__PURE__ */ jsxs(Fragment, { children: [
517
+ /* @__PURE__ */ jsx("dt", { className: "feedback-admin-context__term", children: "Page title" }),
518
+ /* @__PURE__ */ jsx("dd", { className: "feedback-admin-context__value", children: pageTitle })
519
+ ] }),
520
+ userAgent && /* @__PURE__ */ jsxs(Fragment, { children: [
521
+ /* @__PURE__ */ jsx("dt", { className: "feedback-admin-context__term", children: "User agent" }),
522
+ /* @__PURE__ */ jsx("dd", { className: "feedback-admin-context__value feedback-admin-context__value--user-agent", children: userAgent })
523
+ ] }),
524
+ viewport && /* @__PURE__ */ jsxs(Fragment, { children: [
525
+ /* @__PURE__ */ jsx("dt", { className: "feedback-admin-context__term", children: "Viewport" }),
526
+ /* @__PURE__ */ jsxs("dd", { className: "feedback-admin-context__value", children: [
527
+ viewport.width,
528
+ " × ",
529
+ viewport.height
530
+ ] })
531
+ ] }),
532
+ locale && /* @__PURE__ */ jsxs(Fragment, { children: [
533
+ /* @__PURE__ */ jsx("dt", { className: "feedback-admin-context__term", children: "Locale" }),
534
+ /* @__PURE__ */ jsx("dd", { className: "feedback-admin-context__value", children: locale })
535
+ ] }),
536
+ timezone && /* @__PURE__ */ jsxs(Fragment, { children: [
537
+ /* @__PURE__ */ jsx("dt", { className: "feedback-admin-context__term", children: "Timezone" }),
538
+ /* @__PURE__ */ jsx("dd", { className: "feedback-admin-context__value", children: timezone })
539
+ ] })
540
+ ] }) });
541
+ }
542
+ const ALLOWED_TRANSITIONS$1 = {
543
+ open: ["approved", "deferred", "wontfix"],
544
+ approved: ["resolved", "deferred", "wontfix"],
545
+ deferred: ["open", "approved", "wontfix"],
546
+ resolved: ["open"],
547
+ wontfix: ["open"]
548
+ };
549
+ const STATUS_TRANSITION_LABELS = {
550
+ open: "Reopen",
551
+ approved: "Approve",
552
+ deferred: "Defer",
553
+ resolved: "Resolve",
554
+ wontfix: "Won't fix"
555
+ };
556
+ function QuickActions({
557
+ item,
558
+ updateItem,
559
+ onNavigate,
560
+ className
561
+ }) {
562
+ const [saving, setSaving] = useState(false);
563
+ const [error, setError] = useState(null);
564
+ const allowedTargets = ALLOWED_TRANSITIONS$1[item.status] ?? [];
565
+ const handleStatusTransition = async (targetStatus) => {
566
+ setSaving(true);
567
+ setError(null);
568
+ try {
569
+ await updateItem(item.id, { status: targetStatus });
570
+ } catch {
571
+ setError("Failed to update status. Please try again.");
572
+ } finally {
573
+ setSaving(false);
574
+ }
575
+ };
576
+ const handleEdit = () => {
577
+ onNavigate(`/feedback/${item.id}/edit`);
578
+ };
579
+ const handleBackToList = () => {
580
+ onNavigate("/feedback");
581
+ };
582
+ return /* @__PURE__ */ jsxs("div", { className, "data-testid": "quick-actions", children: [
583
+ /* @__PURE__ */ jsx("div", { className: "feedback-admin-actions__status-group", role: "group", "aria-label": "Status actions", children: allowedTargets.map((targetStatus) => /* @__PURE__ */ jsx(
584
+ "button",
585
+ {
586
+ type: "button",
587
+ className: "feedback-admin-actions__status-btn",
588
+ "data-target-status": targetStatus,
589
+ onClick: () => void handleStatusTransition(targetStatus),
590
+ disabled: saving,
591
+ "aria-disabled": saving,
592
+ children: STATUS_TRANSITION_LABELS[targetStatus]
593
+ },
594
+ targetStatus
595
+ )) }),
596
+ error && /* @__PURE__ */ jsx(
597
+ "p",
598
+ {
599
+ className: "feedback-admin-actions__error",
600
+ role: "alert",
601
+ "data-testid": "quick-actions-error",
602
+ children: error
603
+ }
604
+ ),
605
+ /* @__PURE__ */ jsxs("div", { className: "feedback-admin-actions__nav-group", children: [
606
+ /* @__PURE__ */ jsx(
607
+ "button",
608
+ {
609
+ type: "button",
610
+ className: "feedback-admin-actions__edit-btn",
611
+ onClick: handleEdit,
612
+ disabled: saving,
613
+ "aria-disabled": saving,
614
+ "data-testid": "quick-actions-edit",
615
+ children: "Edit"
616
+ }
617
+ ),
618
+ /* @__PURE__ */ jsx(
619
+ "button",
620
+ {
621
+ type: "button",
622
+ className: "feedback-admin-actions__back-btn",
623
+ onClick: handleBackToList,
624
+ disabled: saving,
625
+ "aria-disabled": saving,
626
+ "data-testid": "quick-actions-back",
627
+ children: "Back to list"
628
+ }
629
+ )
630
+ ] })
631
+ ] });
632
+ }
633
+ function FeedbackDetailPage({
634
+ feedbackId,
635
+ fetchItem,
636
+ onNavigate,
637
+ currentUser,
638
+ className,
639
+ generateScreenshotUrl
640
+ }) {
641
+ const [item, setItem] = useState(null);
642
+ const [loading, setLoading] = useState(true);
643
+ const [error, setError] = useState(null);
644
+ useEffect(() => {
645
+ let cancelled = false;
646
+ setLoading(true);
647
+ setError(null);
648
+ fetchItem(feedbackId).then((fetched) => {
649
+ if (!cancelled) {
650
+ setItem(fetched);
651
+ setLoading(false);
652
+ }
653
+ }).catch(() => {
654
+ if (!cancelled) {
655
+ setError("Failed to load feedback item.");
656
+ setLoading(false);
657
+ }
658
+ });
659
+ return () => {
660
+ cancelled = true;
661
+ };
662
+ }, [feedbackId, fetchItem]);
663
+ if (loading) {
664
+ return /* @__PURE__ */ jsx(
665
+ "div",
666
+ {
667
+ className,
668
+ "data-testid": "feedback-detail-loading",
669
+ "aria-busy": "true",
670
+ "aria-label": "Loading feedback item",
671
+ children: /* @__PURE__ */ jsx("p", { className: "feedback-admin-detail__loading", children: "Loading…" })
672
+ }
673
+ );
674
+ }
675
+ if (error || !item) {
676
+ return /* @__PURE__ */ jsxs(
677
+ "div",
678
+ {
679
+ className,
680
+ "data-testid": "feedback-detail-error",
681
+ role: "alert",
682
+ children: [
683
+ /* @__PURE__ */ jsx("p", { className: "feedback-admin-detail__error", children: error ?? "Feedback item not found." }),
684
+ /* @__PURE__ */ jsx(
685
+ "button",
686
+ {
687
+ type: "button",
688
+ className: "feedback-admin-detail__back-btn",
689
+ onClick: () => onNavigate("/feedback"),
690
+ "data-testid": "feedback-detail-back-error",
691
+ children: "Back to list"
692
+ }
693
+ )
694
+ ]
695
+ }
696
+ );
697
+ }
698
+ const handleUpdateItem = async (id, patch) => {
699
+ setItem((prev) => prev ? { ...prev, ...patch } : prev);
700
+ try {
701
+ const updated = await fetchItem(id);
702
+ setItem(updated);
703
+ } catch {
704
+ }
705
+ };
706
+ const formatDate2 = (isoString) => {
707
+ try {
708
+ return new Date(isoString).toLocaleString();
709
+ } catch {
710
+ return isoString;
711
+ }
712
+ };
713
+ return /* @__PURE__ */ jsxs(
714
+ "article",
715
+ {
716
+ className,
717
+ "data-testid": "feedback-detail",
718
+ "data-feedback-id": item.id,
719
+ children: [
720
+ /* @__PURE__ */ jsxs("header", { className: "feedback-admin-detail__header", children: [
721
+ /* @__PURE__ */ jsx("h1", { className: "feedback-admin-detail__title", children: item.content.length > 120 ? `${item.content.slice(0, 120)}…` : item.content }),
722
+ /* @__PURE__ */ jsxs("div", { className: "feedback-admin-detail__badges", children: [
723
+ /* @__PURE__ */ jsx(
724
+ TypeBadge,
725
+ {
726
+ type: item.type,
727
+ className: "feedback-admin-detail__type-badge"
728
+ }
729
+ ),
730
+ /* @__PURE__ */ jsx(
731
+ StatusBadge,
732
+ {
733
+ status: item.status,
734
+ className: "feedback-admin-detail__status-badge"
735
+ }
736
+ ),
737
+ /* @__PURE__ */ jsxs(
738
+ "span",
739
+ {
740
+ className: "feedback-admin-detail__priority",
741
+ "data-priority": item.priority,
742
+ "aria-label": `Priority: ${item.priority}`,
743
+ children: [
744
+ "Priority: ",
745
+ item.priority
746
+ ]
747
+ }
748
+ )
749
+ ] })
750
+ ] }),
751
+ /* @__PURE__ */ jsx(
752
+ "section",
753
+ {
754
+ className: "feedback-admin-detail__metadata",
755
+ "aria-label": "Metadata",
756
+ children: /* @__PURE__ */ jsxs("dl", { className: "feedback-admin-detail__metadata-grid", children: [
757
+ item.environment && /* @__PURE__ */ jsxs(Fragment, { children: [
758
+ /* @__PURE__ */ jsx("dt", { children: "Environment" }),
759
+ /* @__PURE__ */ jsx("dd", { "data-testid": "detail-environment", children: item.environment })
760
+ ] }),
761
+ /* @__PURE__ */ jsx("dt", { children: "Submitted" }),
762
+ /* @__PURE__ */ jsx("dd", { "data-testid": "detail-submitted-at", children: formatDate2(item.submittedAt) }),
763
+ /* @__PURE__ */ jsx("dt", { children: "Reporters" }),
764
+ /* @__PURE__ */ jsx("dd", { "data-testid": "detail-reporter-count", children: item.reporterCount }),
765
+ item.assignedTo && /* @__PURE__ */ jsxs(Fragment, { children: [
766
+ /* @__PURE__ */ jsx("dt", { children: "Assigned to" }),
767
+ /* @__PURE__ */ jsx("dd", { "data-testid": "detail-assigned-to", children: item.assignedTo })
768
+ ] }),
769
+ item.version && /* @__PURE__ */ jsxs(Fragment, { children: [
770
+ /* @__PURE__ */ jsx("dt", { children: "Version" }),
771
+ /* @__PURE__ */ jsx("dd", { "data-testid": "detail-version", children: item.version })
772
+ ] }),
773
+ item.tags && item.tags.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
774
+ /* @__PURE__ */ jsx("dt", { children: "Tags" }),
775
+ /* @__PURE__ */ jsx("dd", { "data-testid": "detail-tags", children: item.tags.join(", ") })
776
+ ] })
777
+ ] })
778
+ }
779
+ ),
780
+ /* @__PURE__ */ jsxs(
781
+ "section",
782
+ {
783
+ className: "feedback-admin-detail__description",
784
+ "aria-label": "Description",
785
+ children: [
786
+ /* @__PURE__ */ jsx("h2", { className: "feedback-admin-detail__section-heading", children: "Description" }),
787
+ /* @__PURE__ */ jsx(
788
+ "p",
789
+ {
790
+ className: "feedback-admin-detail__content",
791
+ "data-testid": "detail-content",
792
+ children: item.content
793
+ }
794
+ )
795
+ ]
796
+ }
797
+ ),
798
+ /* @__PURE__ */ jsxs(
799
+ "section",
800
+ {
801
+ className: "feedback-admin-detail__dialogue",
802
+ "aria-label": "Dialogue history",
803
+ children: [
804
+ /* @__PURE__ */ jsx("h2", { className: "feedback-admin-detail__section-heading", children: "Dialogue history" }),
805
+ /* @__PURE__ */ jsx(
806
+ DialogueHistory,
807
+ {
808
+ dialogue: item.dialogueHistory,
809
+ className: "feedback-admin-detail__dialogue-history"
810
+ }
811
+ )
812
+ ]
813
+ }
814
+ ),
815
+ /* @__PURE__ */ jsxs(
816
+ "section",
817
+ {
818
+ className: "feedback-admin-detail__screenshot",
819
+ "aria-label": "Screenshot",
820
+ children: [
821
+ /* @__PURE__ */ jsx("h2", { className: "feedback-admin-detail__section-heading", children: "Screenshot" }),
822
+ /* @__PURE__ */ jsx(
823
+ ScreenshotViewer,
824
+ {
825
+ ...item.screenshotS3Key !== void 0 ? { screenshotS3Key: item.screenshotS3Key } : {},
826
+ ...generateScreenshotUrl !== void 0 ? { generateScreenshotUrl } : {},
827
+ alt: `Screenshot for feedback ${item.id}`,
828
+ className: "feedback-admin-detail__screenshot-viewer"
829
+ }
830
+ )
831
+ ]
832
+ }
833
+ ),
834
+ /* @__PURE__ */ jsxs(
835
+ "section",
836
+ {
837
+ className: "feedback-admin-detail__context",
838
+ "aria-label": "Browser context",
839
+ children: [
840
+ /* @__PURE__ */ jsx("h2", { className: "feedback-admin-detail__section-heading", children: "Browser context" }),
841
+ /* @__PURE__ */ jsx(
842
+ BrowserContext,
843
+ {
844
+ context: item.context,
845
+ className: "feedback-admin-detail__browser-context"
846
+ }
847
+ )
848
+ ]
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsx(
852
+ QuickActions,
853
+ {
854
+ item,
855
+ updateItem: handleUpdateItem,
856
+ onNavigate,
857
+ ...currentUser !== void 0 ? { currentUser } : {},
858
+ className: "feedback-admin-detail__quick-actions"
859
+ }
860
+ )
861
+ ]
862
+ }
863
+ );
864
+ }
865
+ const ALLOWED_TRANSITIONS = {
866
+ open: ["approved", "deferred", "wontfix"],
867
+ approved: ["open", "resolved", "deferred"],
868
+ deferred: ["open", "approved", "wontfix"],
869
+ resolved: ["open", "approved"],
870
+ wontfix: ["open", "deferred"]
871
+ };
872
+ function useStatusTransition(currentStatus) {
873
+ const allowedTargets = ALLOWED_TRANSITIONS[currentStatus] ?? [];
874
+ return { allowedTargets };
875
+ }
876
+ const STATUS_LABELS = {
877
+ open: "Open",
878
+ approved: "Approved",
879
+ deferred: "Deferred",
880
+ resolved: "Resolved",
881
+ wontfix: "Won't fix"
882
+ };
883
+ const TYPE_LABELS = {
884
+ bug: "Bug",
885
+ feature: "Feature",
886
+ ux: "UX issue",
887
+ other: "Other"
888
+ };
889
+ const SCOPE_LABELS = {
890
+ isolated: "Isolated",
891
+ "cross-cutting": "Cross-cutting",
892
+ systemic: "Systemic"
893
+ };
894
+ const SIZE_LABELS = {
895
+ trivial: "Trivial",
896
+ small: "Small",
897
+ medium: "Medium",
898
+ large: "Large"
899
+ };
900
+ const ALL_TYPES = ["bug", "feature", "ux", "other"];
901
+ const ALL_SCOPES = ["isolated", "cross-cutting", "systemic"];
902
+ const ALL_SIZES = ["trivial", "small", "medium", "large"];
903
+ function StatusSelect({
904
+ currentStatus,
905
+ allowedTargets,
906
+ value,
907
+ onChange,
908
+ className
909
+ }) {
910
+ const id = "feedback-admin-status-select";
911
+ return /* @__PURE__ */ jsxs("div", { className, children: [
912
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: "Status" }),
913
+ /* @__PURE__ */ jsxs(
914
+ "select",
915
+ {
916
+ id,
917
+ value,
918
+ "aria-label": "Status",
919
+ onChange: (e) => onChange(e.target.value),
920
+ children: [
921
+ /* @__PURE__ */ jsx("option", { value: currentStatus, children: STATUS_LABELS[currentStatus] }),
922
+ allowedTargets.map((target) => /* @__PURE__ */ jsx("option", { value: target, children: STATUS_LABELS[target] }, target))
923
+ ]
924
+ }
925
+ )
926
+ ] });
927
+ }
928
+ function TypeSelect({
929
+ value,
930
+ onChange,
931
+ className
932
+ }) {
933
+ const id = "feedback-admin-type-select";
934
+ return /* @__PURE__ */ jsxs("div", { className, children: [
935
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: "Type" }),
936
+ /* @__PURE__ */ jsx(
937
+ "select",
938
+ {
939
+ id,
940
+ value,
941
+ "aria-label": "Type",
942
+ onChange: (e) => onChange(e.target.value),
943
+ children: ALL_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type, children: TYPE_LABELS[type] }, type))
944
+ }
945
+ )
946
+ ] });
947
+ }
948
+ function ScopeSelect({
949
+ value,
950
+ onChange,
951
+ className
952
+ }) {
953
+ const id = "feedback-admin-scope-select";
954
+ return /* @__PURE__ */ jsxs("div", { className, children: [
955
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: "Scope" }),
956
+ /* @__PURE__ */ jsxs(
957
+ "select",
958
+ {
959
+ id,
960
+ value: value ?? "",
961
+ "aria-label": "Scope",
962
+ onChange: (e) => {
963
+ const v = e.target.value;
964
+ onChange(v === "" ? void 0 : v);
965
+ },
966
+ children: [
967
+ /* @__PURE__ */ jsx("option", { value: "", children: "— not set —" }),
968
+ ALL_SCOPES.map((scope) => /* @__PURE__ */ jsx("option", { value: scope, children: SCOPE_LABELS[scope] }, scope))
969
+ ]
970
+ }
971
+ )
972
+ ] });
973
+ }
974
+ function SizeSelect({
975
+ value,
976
+ onChange,
977
+ className
978
+ }) {
979
+ const id = "feedback-admin-size-select";
980
+ return /* @__PURE__ */ jsxs("div", { className, children: [
981
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: "Size" }),
982
+ /* @__PURE__ */ jsxs(
983
+ "select",
984
+ {
985
+ id,
986
+ value: value ?? "",
987
+ "aria-label": "Size",
988
+ onChange: (e) => {
989
+ const v = e.target.value;
990
+ onChange(v === "" ? void 0 : v);
991
+ },
992
+ children: [
993
+ /* @__PURE__ */ jsx("option", { value: "", children: "— not set —" }),
994
+ ALL_SIZES.map((size) => /* @__PURE__ */ jsx("option", { value: size, children: SIZE_LABELS[size] }, size))
995
+ ]
996
+ }
997
+ )
998
+ ] });
999
+ }
1000
+ function AssigneeInput({
1001
+ value,
1002
+ onChange,
1003
+ currentUser,
1004
+ className
1005
+ }) {
1006
+ const id = "feedback-admin-assignee-input";
1007
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1008
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: "Assigned to" }),
1009
+ /* @__PURE__ */ jsx(
1010
+ "input",
1011
+ {
1012
+ id,
1013
+ type: "text",
1014
+ value: value ?? "",
1015
+ "aria-label": "Assigned to",
1016
+ onChange: (e) => {
1017
+ const v = e.target.value;
1018
+ onChange(v === "" ? void 0 : v);
1019
+ }
1020
+ }
1021
+ ),
1022
+ currentUser !== void 0 && /* @__PURE__ */ jsx(
1023
+ "button",
1024
+ {
1025
+ type: "button",
1026
+ onClick: () => onChange(currentUser.userId),
1027
+ children: "Assign to me"
1028
+ }
1029
+ )
1030
+ ] });
1031
+ }
1032
+ function TagsInput({
1033
+ value,
1034
+ onChange,
1035
+ className
1036
+ }) {
1037
+ const id = "feedback-admin-tags-input";
1038
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1039
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: "Tags" }),
1040
+ /* @__PURE__ */ jsx(
1041
+ "input",
1042
+ {
1043
+ id,
1044
+ type: "text",
1045
+ value: value.join(", "),
1046
+ "aria-label": "Tags",
1047
+ placeholder: "Comma-separated",
1048
+ onChange: (e) => {
1049
+ const parsed = e.target.value.split(",").map((s) => s.trim()).filter(Boolean);
1050
+ onChange(parsed);
1051
+ }
1052
+ }
1053
+ )
1054
+ ] });
1055
+ }
1056
+ function CorrectionNote({
1057
+ value,
1058
+ onChange,
1059
+ required,
1060
+ className
1061
+ }) {
1062
+ const id = "feedback-admin-correction-note";
1063
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1064
+ /* @__PURE__ */ jsxs("label", { htmlFor: id, children: [
1065
+ "Correction note",
1066
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: " *" })
1067
+ ] }),
1068
+ /* @__PURE__ */ jsx(
1069
+ "textarea",
1070
+ {
1071
+ id,
1072
+ value,
1073
+ required,
1074
+ "aria-required": required,
1075
+ "aria-label": "Correction note",
1076
+ placeholder: "Describe why the classification is changing…",
1077
+ onChange: (e) => onChange(e.target.value)
1078
+ }
1079
+ )
1080
+ ] });
1081
+ }
1082
+ const EditableFields = {
1083
+ StatusSelect,
1084
+ TypeSelect,
1085
+ ScopeSelect,
1086
+ SizeSelect,
1087
+ AssigneeInput,
1088
+ TagsInput,
1089
+ CorrectionNote
1090
+ };
1091
+ function FeedbackUpdatePage({
1092
+ feedbackId,
1093
+ fetchItem,
1094
+ updateItem,
1095
+ onNavigate,
1096
+ currentUser,
1097
+ className
1098
+ }) {
1099
+ const [item, setItem] = useState(null);
1100
+ const [loading, setLoading] = useState(true);
1101
+ const [status, setStatus] = useState("open");
1102
+ const [type, setType] = useState("bug");
1103
+ const [scope, setScope] = useState(void 0);
1104
+ const [size, setSize] = useState(void 0);
1105
+ const [assignedTo, setAssignedTo] = useState(void 0);
1106
+ const [tags, setTags] = useState([]);
1107
+ const [correctionNote, setCorrectionNote] = useState("");
1108
+ const [saving, setSaving] = useState(false);
1109
+ const { allowedTargets } = useStatusTransition(status);
1110
+ useEffect(() => {
1111
+ let cancelled = false;
1112
+ setLoading(true);
1113
+ fetchItem(feedbackId).then((loadedItem) => {
1114
+ if (cancelled) return;
1115
+ setItem(loadedItem);
1116
+ setStatus(loadedItem.status);
1117
+ setType(loadedItem.type);
1118
+ setScope(loadedItem.scope);
1119
+ setSize(loadedItem.size);
1120
+ setAssignedTo(loadedItem.assignedTo);
1121
+ setTags(loadedItem.tags ?? []);
1122
+ setCorrectionNote("");
1123
+ setLoading(false);
1124
+ });
1125
+ return () => {
1126
+ cancelled = true;
1127
+ };
1128
+ }, [feedbackId, fetchItem]);
1129
+ const classificationChanged = item !== null && (type !== item.type || scope !== item.scope || size !== item.size);
1130
+ const saveDisabled = saving || item === null || classificationChanged && correctionNote.trim() === "";
1131
+ function handleCancel() {
1132
+ onNavigate(`/feedback/${feedbackId}`);
1133
+ }
1134
+ async function handleSave() {
1135
+ if (item === null || saveDisabled) return;
1136
+ setSaving(true);
1137
+ try {
1138
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1139
+ const patch = {
1140
+ lastModifiedAt: now
1141
+ };
1142
+ if (status !== item.status) {
1143
+ patch.status = status;
1144
+ if (status === "resolved") {
1145
+ patch.resolvedAt = now;
1146
+ patch.resolvedBy = currentUser?.userId ?? "unknown";
1147
+ }
1148
+ }
1149
+ if (type !== item.type) patch.type = type;
1150
+ if (scope !== item.scope) {
1151
+ if (scope !== void 0) {
1152
+ patch.scope = scope;
1153
+ } else {
1154
+ patch.scope = void 0;
1155
+ }
1156
+ }
1157
+ if (size !== item.size) {
1158
+ if (size !== void 0) {
1159
+ patch.size = size;
1160
+ } else {
1161
+ patch.size = void 0;
1162
+ }
1163
+ }
1164
+ if (assignedTo !== item.assignedTo) {
1165
+ if (assignedTo !== void 0) {
1166
+ patch.assignedTo = assignedTo;
1167
+ } else {
1168
+ patch.assignedTo = void 0;
1169
+ }
1170
+ }
1171
+ if (JSON.stringify(tags) !== JSON.stringify(item.tags)) {
1172
+ patch.tags = tags;
1173
+ }
1174
+ if (classificationChanged) {
1175
+ const changedFields = [];
1176
+ const before = {};
1177
+ const after = {};
1178
+ if (type !== item.type) {
1179
+ changedFields.push("type");
1180
+ before.type = item.type;
1181
+ after.type = type;
1182
+ }
1183
+ if (scope !== item.scope) {
1184
+ changedFields.push("scope");
1185
+ before.scope = item.scope;
1186
+ after.scope = scope;
1187
+ }
1188
+ if (size !== item.size) {
1189
+ changedFields.push("size");
1190
+ before.size = item.size;
1191
+ after.size = size;
1192
+ }
1193
+ const correctionEntry = {
1194
+ field: changedFields.join(", "),
1195
+ before,
1196
+ after,
1197
+ correctedBy: currentUser?.userId ?? "unknown",
1198
+ correctedAt: now,
1199
+ reason: correctionNote
1200
+ };
1201
+ patch.corrections = [...item.corrections ?? [], correctionEntry];
1202
+ }
1203
+ await updateItem(feedbackId, patch);
1204
+ onNavigate(`/feedback/${feedbackId}`);
1205
+ } finally {
1206
+ setSaving(false);
1207
+ }
1208
+ }
1209
+ if (loading) {
1210
+ return /* @__PURE__ */ jsx("article", { "data-page": "feedback-update", className, children: /* @__PURE__ */ jsx("p", { "data-loading": "true", children: "Loading…" }) });
1211
+ }
1212
+ if (item === null) {
1213
+ return /* @__PURE__ */ jsx("article", { "data-page": "feedback-update", className, children: /* @__PURE__ */ jsx("p", { children: "Feedback item not found." }) });
1214
+ }
1215
+ return /* @__PURE__ */ jsxs("article", { "data-page": "feedback-update", className, children: [
1216
+ /* @__PURE__ */ jsxs("header", { children: [
1217
+ /* @__PURE__ */ jsxs("h1", { children: [
1218
+ item.id,
1219
+ " — Edit"
1220
+ ] }),
1221
+ /* @__PURE__ */ jsxs("nav", { children: [
1222
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onNavigate(`/feedback/${feedbackId}`), children: "← Back to detail" }),
1223
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleCancel, children: "Cancel" })
1224
+ ] })
1225
+ ] }),
1226
+ /* @__PURE__ */ jsxs("section", { "data-section": "read-only-context", "aria-label": "Read-only context", children: [
1227
+ /* @__PURE__ */ jsx(StatusBadge, { status: item.status }),
1228
+ /* @__PURE__ */ jsx(TypeBadge, { type: item.type }),
1229
+ /* @__PURE__ */ jsx("div", { "data-field": "content", children: /* @__PURE__ */ jsx("p", { children: item.content }) }),
1230
+ /* @__PURE__ */ jsxs("div", { "data-field": "metadata", children: [
1231
+ /* @__PURE__ */ jsx("span", { "data-field": "submittedAt", children: item.submittedAt }),
1232
+ item.environment && /* @__PURE__ */ jsx("span", { "data-field": "environment", children: item.environment }),
1233
+ /* @__PURE__ */ jsx("span", { "data-field": "reporterCount", children: item.reporterCount })
1234
+ ] })
1235
+ ] }),
1236
+ /* @__PURE__ */ jsxs("section", { "data-section": "edit-form", children: [
1237
+ /* @__PURE__ */ jsx(
1238
+ EditableFields.StatusSelect,
1239
+ {
1240
+ currentStatus: item.status,
1241
+ allowedTargets,
1242
+ value: status,
1243
+ onChange: setStatus
1244
+ }
1245
+ ),
1246
+ /* @__PURE__ */ jsx(
1247
+ EditableFields.TypeSelect,
1248
+ {
1249
+ value: type,
1250
+ onChange: setType
1251
+ }
1252
+ ),
1253
+ /* @__PURE__ */ jsx(
1254
+ EditableFields.ScopeSelect,
1255
+ {
1256
+ value: scope,
1257
+ onChange: setScope
1258
+ }
1259
+ ),
1260
+ /* @__PURE__ */ jsx(
1261
+ EditableFields.SizeSelect,
1262
+ {
1263
+ value: size,
1264
+ onChange: setSize
1265
+ }
1266
+ ),
1267
+ /* @__PURE__ */ jsx(
1268
+ EditableFields.AssigneeInput,
1269
+ {
1270
+ value: assignedTo,
1271
+ onChange: setAssignedTo,
1272
+ ...currentUser !== void 0 ? { currentUser } : {}
1273
+ }
1274
+ ),
1275
+ /* @__PURE__ */ jsx(
1276
+ EditableFields.TagsInput,
1277
+ {
1278
+ value: tags,
1279
+ onChange: setTags
1280
+ }
1281
+ ),
1282
+ classificationChanged && /* @__PURE__ */ jsx(
1283
+ EditableFields.CorrectionNote,
1284
+ {
1285
+ value: correctionNote,
1286
+ onChange: setCorrectionNote,
1287
+ required: true
1288
+ }
1289
+ )
1290
+ ] }),
1291
+ /* @__PURE__ */ jsxs("footer", { children: [
1292
+ /* @__PURE__ */ jsx(
1293
+ "button",
1294
+ {
1295
+ type: "submit",
1296
+ disabled: saveDisabled,
1297
+ "aria-disabled": saveDisabled,
1298
+ onClick: () => {
1299
+ void handleSave();
1300
+ },
1301
+ children: saving ? "Saving…" : "Save changes"
1302
+ }
1303
+ ),
1304
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleCancel, disabled: saving, children: "Cancel" })
1305
+ ] })
1306
+ ] });
1307
+ }
1308
+ export {
1309
+ FeedbackDetailPage,
1310
+ FeedbackListPage,
1311
+ FeedbackUpdatePage
1312
+ };
1313
+ //# sourceMappingURL=index.js.map