@squaredr/fieldcraft-pro 1.2.0 → 1.4.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.
- package/dist/chunk-22T5PY6L.mjs +1081 -0
- package/dist/{chunk-4BX7FCXH.mjs → chunk-BK4SMEW4.mjs} +840 -21
- package/dist/{chunk-GNBXIG63.mjs → chunk-BYT2P3I6.mjs} +1264 -88
- package/dist/form-builder/index.d.mts +7 -367
- package/dist/form-builder/index.d.ts +7 -367
- package/dist/form-builder/index.js +1266 -90
- package/dist/form-builder/index.mjs +1 -1
- package/dist/index-nvIvXlmc.d.mts +398 -0
- package/dist/index-nvIvXlmc.d.ts +398 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2926 -335
- package/dist/index.mjs +4 -4
- package/dist/preview-schema-DFvV4y6J.d.mts +66 -0
- package/dist/preview-schema-DFvV4y6J.d.ts +66 -0
- package/dist/response-viewer/index.d.mts +9 -1
- package/dist/response-viewer/index.d.ts +9 -1
- package/dist/response-viewer/index.js +838 -19
- package/dist/response-viewer/index.mjs +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme-editor/index.d.mts +30 -26
- package/dist/theme-editor/index.d.ts +30 -26
- package/dist/theme-editor/index.js +702 -22
- package/dist/theme-editor/index.mjs +1 -1
- package/package.json +8 -6
- package/theme-editor/styles.css +266 -62
- package/dist/chunk-7LQW5QYT.mjs +0 -407
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from './chunk-QQ4JZGTD.mjs';
|
|
2
2
|
import { requireLicense } from './chunk-VECQKSWS.mjs';
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { Button, Separator,
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { Input, Button, Separator, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@squaredr/fieldcraft-react';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
// src/response-viewer/clinical-display-data.ts
|
|
@@ -140,10 +140,30 @@ function getScoreSeverity(instrumentKey, score) {
|
|
|
140
140
|
if (!thresholds) return void 0;
|
|
141
141
|
return thresholds.find((t) => score >= t.min && score <= t.max);
|
|
142
142
|
}
|
|
143
|
-
function ResponseTable({
|
|
143
|
+
function ResponseTable({
|
|
144
|
+
schema,
|
|
145
|
+
responses,
|
|
146
|
+
onRowClick,
|
|
147
|
+
selectable,
|
|
148
|
+
selectedIds,
|
|
149
|
+
onToggleSelect,
|
|
150
|
+
onSelectAll
|
|
151
|
+
}) {
|
|
144
152
|
const questions = getAllQuestions(schema);
|
|
153
|
+
const allPageSelected = selectable && responses.length > 0 && responses.every(
|
|
154
|
+
(r) => r.sessionToken && selectedIds?.has(r.sessionToken)
|
|
155
|
+
);
|
|
145
156
|
return /* @__PURE__ */ jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
|
|
146
157
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-muted", children: [
|
|
158
|
+
selectable && /* @__PURE__ */ jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsx(
|
|
159
|
+
"input",
|
|
160
|
+
{
|
|
161
|
+
type: "checkbox",
|
|
162
|
+
checked: allPageSelected,
|
|
163
|
+
onChange: () => onSelectAll?.(),
|
|
164
|
+
className: "accent-primary"
|
|
165
|
+
}
|
|
166
|
+
) }),
|
|
147
167
|
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
|
|
148
168
|
questions.map((q) => /* @__PURE__ */ jsx(
|
|
149
169
|
"th",
|
|
@@ -162,6 +182,19 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
162
182
|
onClick: () => onRowClick?.(response),
|
|
163
183
|
className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
|
|
164
184
|
children: [
|
|
185
|
+
selectable && /* @__PURE__ */ jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsx(
|
|
186
|
+
"input",
|
|
187
|
+
{
|
|
188
|
+
type: "checkbox",
|
|
189
|
+
checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
|
|
190
|
+
onChange: (e) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
if (response.sessionToken) onToggleSelect?.(response.sessionToken);
|
|
193
|
+
},
|
|
194
|
+
onClick: (e) => e.stopPropagation(),
|
|
195
|
+
className: "accent-primary"
|
|
196
|
+
}
|
|
197
|
+
) }),
|
|
165
198
|
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
|
|
166
199
|
questions.map((q) => /* @__PURE__ */ jsx(
|
|
167
200
|
"td",
|
|
@@ -179,7 +212,7 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
179
212
|
responses.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
180
213
|
"td",
|
|
181
214
|
{
|
|
182
|
-
colSpan: questions.length + 2,
|
|
215
|
+
colSpan: questions.length + 2 + (selectable ? 1 : 0),
|
|
183
216
|
className: "px-3 py-8 text-center text-muted-foreground",
|
|
184
217
|
children: "No responses yet"
|
|
185
218
|
}
|
|
@@ -267,15 +300,37 @@ function formatCellValue(value, type) {
|
|
|
267
300
|
if (typeof value === "object") return JSON.stringify(value);
|
|
268
301
|
return String(value);
|
|
269
302
|
}
|
|
270
|
-
function ResponseCard({
|
|
303
|
+
function ResponseCard({
|
|
304
|
+
response,
|
|
305
|
+
fields,
|
|
306
|
+
onClick,
|
|
307
|
+
selectable,
|
|
308
|
+
selected,
|
|
309
|
+
onToggleSelect
|
|
310
|
+
}) {
|
|
271
311
|
return /* @__PURE__ */ jsxs(
|
|
272
312
|
"div",
|
|
273
313
|
{
|
|
274
314
|
onClick,
|
|
275
|
-
className: onClick ? "border border-border rounded-lg p-4 bg-card cursor-pointer transition-shadow hover:shadow-md" : "border border-border rounded-lg p-4 bg-card",
|
|
315
|
+
className: (onClick ? "border border-border rounded-lg p-4 bg-card cursor-pointer transition-shadow hover:shadow-md" : "border border-border rounded-lg p-4 bg-card") + (selected ? " ring-2 ring-primary" : ""),
|
|
276
316
|
children: [
|
|
277
317
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-between mb-3 text-xs text-muted-foreground", children: [
|
|
278
|
-
/* @__PURE__ */
|
|
318
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
319
|
+
selectable && /* @__PURE__ */ jsx(
|
|
320
|
+
"input",
|
|
321
|
+
{
|
|
322
|
+
type: "checkbox",
|
|
323
|
+
checked: !!selected,
|
|
324
|
+
onChange: (e) => {
|
|
325
|
+
e.stopPropagation();
|
|
326
|
+
onToggleSelect?.();
|
|
327
|
+
},
|
|
328
|
+
onClick: (e) => e.stopPropagation(),
|
|
329
|
+
className: "accent-primary"
|
|
330
|
+
}
|
|
331
|
+
),
|
|
332
|
+
/* @__PURE__ */ jsx("span", { children: new Date(response.submittedAt).toLocaleString() })
|
|
333
|
+
] }),
|
|
279
334
|
response.completionTimeMs != null && /* @__PURE__ */ jsxs("span", { children: [
|
|
280
335
|
Math.round(response.completionTimeMs / 1e3),
|
|
281
336
|
"s"
|
|
@@ -366,18 +421,19 @@ function formatCardValue(value, type) {
|
|
|
366
421
|
if (typeof value === "object") return JSON.stringify(value);
|
|
367
422
|
return String(value);
|
|
368
423
|
}
|
|
369
|
-
function ResponseDetail({ response, fields, onBack }) {
|
|
424
|
+
function ResponseDetail({ response, fields, onBack, onDelete }) {
|
|
370
425
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
371
426
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-5 pb-3 border-b border-border", children: [
|
|
372
427
|
onBack && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onBack, children: "Back" }),
|
|
373
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
428
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
374
429
|
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-foreground", children: "Response Detail" }),
|
|
375
430
|
/* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
|
|
376
431
|
"Submitted ",
|
|
377
432
|
new Date(response.submittedAt).toLocaleString(),
|
|
378
433
|
response.completionTimeMs != null && ` \u2014 ${Math.round(response.completionTimeMs / 1e3)}s`
|
|
379
434
|
] })
|
|
380
|
-
] })
|
|
435
|
+
] }),
|
|
436
|
+
onDelete && /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: onDelete, children: "Delete" })
|
|
381
437
|
] }),
|
|
382
438
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4", children: fields.map((field) => /* @__PURE__ */ jsxs(
|
|
383
439
|
"div",
|
|
@@ -730,6 +786,491 @@ function formatFallbackValue(value) {
|
|
|
730
786
|
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
731
787
|
return String(value);
|
|
732
788
|
}
|
|
789
|
+
function TimelineView({ responses, questions, onSelect }) {
|
|
790
|
+
const grouped = useMemo(() => groupByDay(responses), [responses]);
|
|
791
|
+
if (responses.length === 0) {
|
|
792
|
+
return /* @__PURE__ */ jsx("div", { className: "p-8 text-center text-muted-foreground text-sm", children: "No responses to display." });
|
|
793
|
+
}
|
|
794
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 space-y-6", children: grouped.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
795
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
796
|
+
/* @__PURE__ */ jsx("div", { className: "h-px flex-1 bg-border" }),
|
|
797
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground shrink-0", children: group.dateLabel }),
|
|
798
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
|
|
799
|
+
"(",
|
|
800
|
+
group.responses.length,
|
|
801
|
+
")"
|
|
802
|
+
] }),
|
|
803
|
+
/* @__PURE__ */ jsx("div", { className: "h-px flex-1 bg-border" })
|
|
804
|
+
] }),
|
|
805
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2 ml-4 border-l-2 border-border/50 pl-4", children: group.responses.map((response, idx) => /* @__PURE__ */ jsx(
|
|
806
|
+
TimelineEntry,
|
|
807
|
+
{
|
|
808
|
+
response,
|
|
809
|
+
questions,
|
|
810
|
+
onView: () => onSelect(response)
|
|
811
|
+
},
|
|
812
|
+
response.sessionToken || idx
|
|
813
|
+
)) })
|
|
814
|
+
] }, group.dateLabel)) });
|
|
815
|
+
}
|
|
816
|
+
function TimelineEntry({
|
|
817
|
+
response,
|
|
818
|
+
questions,
|
|
819
|
+
onView
|
|
820
|
+
}) {
|
|
821
|
+
const time = new Date(response.submittedAt).toLocaleTimeString([], {
|
|
822
|
+
hour: "2-digit",
|
|
823
|
+
minute: "2-digit"
|
|
824
|
+
});
|
|
825
|
+
const preview = [];
|
|
826
|
+
for (const q of questions) {
|
|
827
|
+
if (preview.length >= 3) break;
|
|
828
|
+
const val = response.values[q.id];
|
|
829
|
+
if (val != null && val !== "") {
|
|
830
|
+
preview.push({ questionId: q.id, label: q.label, type: q.type, value: val });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-start gap-3 group", children: [
|
|
834
|
+
/* @__PURE__ */ jsx("div", { className: "absolute -left-[21px] top-2.5 w-2 h-2 rounded-full bg-border group-hover:bg-primary transition-colors" }),
|
|
835
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 rounded border border-border bg-card p-3 hover:border-primary/40 transition-colors", children: [
|
|
836
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
837
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: time }),
|
|
838
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
839
|
+
response.totalScore != null && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
840
|
+
"Score: ",
|
|
841
|
+
response.totalScore
|
|
842
|
+
] }),
|
|
843
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onView, children: "View" })
|
|
844
|
+
] })
|
|
845
|
+
] }),
|
|
846
|
+
preview.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: preview.map((field) => /* @__PURE__ */ jsxs("div", { className: "text-xs", children: [
|
|
847
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
848
|
+
field.label,
|
|
849
|
+
": "
|
|
850
|
+
] }),
|
|
851
|
+
/* @__PURE__ */ jsx("span", { className: "text-foreground", children: formatPreviewValue(field.value, field.type) })
|
|
852
|
+
] }, field.questionId)) })
|
|
853
|
+
] })
|
|
854
|
+
] });
|
|
855
|
+
}
|
|
856
|
+
function groupByDay(responses) {
|
|
857
|
+
const sorted = [...responses].sort(
|
|
858
|
+
(a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()
|
|
859
|
+
);
|
|
860
|
+
const groups = /* @__PURE__ */ new Map();
|
|
861
|
+
for (const r of sorted) {
|
|
862
|
+
const date = new Date(r.submittedAt).toLocaleDateString(void 0, {
|
|
863
|
+
weekday: "long",
|
|
864
|
+
year: "numeric",
|
|
865
|
+
month: "long",
|
|
866
|
+
day: "numeric"
|
|
867
|
+
});
|
|
868
|
+
const existing = groups.get(date);
|
|
869
|
+
if (existing) {
|
|
870
|
+
existing.push(r);
|
|
871
|
+
} else {
|
|
872
|
+
groups.set(date, [r]);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return Array.from(groups.entries()).map(([dateLabel, responses2]) => ({
|
|
876
|
+
dateLabel,
|
|
877
|
+
responses: responses2
|
|
878
|
+
}));
|
|
879
|
+
}
|
|
880
|
+
function formatPreviewValue(value, type) {
|
|
881
|
+
if (value == null) return "\u2014";
|
|
882
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
883
|
+
if (Array.isArray(value)) return value.length > 0 ? value.slice(0, 2).join(", ") + (value.length > 2 ? "..." : "") : "\u2014";
|
|
884
|
+
if (typeof value === "object") {
|
|
885
|
+
if (type === "legal_name") {
|
|
886
|
+
const n = value;
|
|
887
|
+
return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
|
|
888
|
+
}
|
|
889
|
+
return "...";
|
|
890
|
+
}
|
|
891
|
+
const str = String(value);
|
|
892
|
+
return str.length > 40 ? str.slice(0, 40) + "..." : str;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/response-viewer/stats-utils.ts
|
|
896
|
+
function computeStats(responses, schema) {
|
|
897
|
+
if (responses.length === 0) {
|
|
898
|
+
return {
|
|
899
|
+
totalCount: 0,
|
|
900
|
+
completionRate: 0,
|
|
901
|
+
avgCompletionTimeMs: null,
|
|
902
|
+
dateRange: null,
|
|
903
|
+
fieldSummaries: []
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const questions = getStatsQuestions(schema);
|
|
907
|
+
const requiredIds = new Set(questions.filter((q) => q.required).map((q) => q.id));
|
|
908
|
+
let completedCount = 0;
|
|
909
|
+
for (const r of responses) {
|
|
910
|
+
let allFilled = true;
|
|
911
|
+
for (const id of requiredIds) {
|
|
912
|
+
if (r.values[id] == null || r.values[id] === "") {
|
|
913
|
+
allFilled = false;
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (allFilled) completedCount++;
|
|
918
|
+
}
|
|
919
|
+
const completionRate = requiredIds.size > 0 ? completedCount / responses.length : 1;
|
|
920
|
+
const completionTimes = responses.map((r) => r.completionTimeMs).filter((t) => t != null && t > 0);
|
|
921
|
+
const avgCompletionTimeMs = completionTimes.length > 0 ? completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length : null;
|
|
922
|
+
const timestamps = responses.map((r) => new Date(r.submittedAt).getTime());
|
|
923
|
+
const earliest = new Date(Math.min(...timestamps)).toISOString();
|
|
924
|
+
const latest = new Date(Math.max(...timestamps)).toISOString();
|
|
925
|
+
const fieldSummaries = questions.map((q) => computeFieldSummary(q, responses));
|
|
926
|
+
return {
|
|
927
|
+
totalCount: responses.length,
|
|
928
|
+
completionRate,
|
|
929
|
+
avgCompletionTimeMs,
|
|
930
|
+
dateRange: { earliest, latest },
|
|
931
|
+
fieldSummaries
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function computeFieldSummary(question, responses) {
|
|
935
|
+
const values = responses.map((r) => r.values[question.id]).filter((v) => v != null && v !== "");
|
|
936
|
+
const filledCount = values.length;
|
|
937
|
+
const filledRate = responses.length > 0 ? filledCount / responses.length : 0;
|
|
938
|
+
const base = {
|
|
939
|
+
fieldId: question.id,
|
|
940
|
+
label: question.label,
|
|
941
|
+
type: question.type,
|
|
942
|
+
filledCount,
|
|
943
|
+
filledRate
|
|
944
|
+
};
|
|
945
|
+
if (isNumericType(question.type)) {
|
|
946
|
+
const nums = values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => !isNaN(n));
|
|
947
|
+
if (nums.length > 0) {
|
|
948
|
+
return {
|
|
949
|
+
...base,
|
|
950
|
+
kind: "numeric",
|
|
951
|
+
mean: nums.reduce((a, b) => a + b, 0) / nums.length,
|
|
952
|
+
min: Math.min(...nums),
|
|
953
|
+
max: Math.max(...nums)
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return { ...base, kind: "other" };
|
|
957
|
+
}
|
|
958
|
+
if (isCategoricalType(question.type)) {
|
|
959
|
+
const counts = /* @__PURE__ */ new Map();
|
|
960
|
+
for (const v of values) {
|
|
961
|
+
const key = String(v);
|
|
962
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
963
|
+
}
|
|
964
|
+
let mode = "";
|
|
965
|
+
let modeCount = 0;
|
|
966
|
+
for (const [key, count] of counts) {
|
|
967
|
+
if (count > modeCount) {
|
|
968
|
+
mode = key;
|
|
969
|
+
modeCount = count;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
...base,
|
|
974
|
+
kind: "categorical",
|
|
975
|
+
mode,
|
|
976
|
+
modeCount,
|
|
977
|
+
uniqueCount: counts.size
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return { ...base, kind: "other" };
|
|
981
|
+
}
|
|
982
|
+
function isNumericType(type) {
|
|
983
|
+
return [
|
|
984
|
+
"number",
|
|
985
|
+
"slider",
|
|
986
|
+
"rating",
|
|
987
|
+
"nps",
|
|
988
|
+
"opinion_scale",
|
|
989
|
+
"pain_scale"
|
|
990
|
+
].includes(type);
|
|
991
|
+
}
|
|
992
|
+
function isCategoricalType(type) {
|
|
993
|
+
return [
|
|
994
|
+
"single_select",
|
|
995
|
+
"dropdown",
|
|
996
|
+
"country_select",
|
|
997
|
+
"boolean",
|
|
998
|
+
"consent",
|
|
999
|
+
"multi_select",
|
|
1000
|
+
"checkbox_group"
|
|
1001
|
+
].includes(type);
|
|
1002
|
+
}
|
|
1003
|
+
function getStatsQuestions(schema) {
|
|
1004
|
+
const questions = [];
|
|
1005
|
+
for (const section of schema.sections) {
|
|
1006
|
+
for (const q of section.questions) {
|
|
1007
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
questions.push(q);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return questions;
|
|
1014
|
+
}
|
|
1015
|
+
function StatsPanel({ schema, responses, onClose }) {
|
|
1016
|
+
const stats = useMemo(() => computeStats(responses, schema), [responses, schema]);
|
|
1017
|
+
if (stats.totalCount === 0) {
|
|
1018
|
+
return /* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: "No responses to compute statistics." });
|
|
1019
|
+
}
|
|
1020
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
1021
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
1022
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: "Statistics" }),
|
|
1023
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
1024
|
+
] }),
|
|
1025
|
+
/* @__PURE__ */ jsxs("div", { className: "px-4 py-3", children: [
|
|
1026
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-3", children: [
|
|
1027
|
+
/* @__PURE__ */ jsx(
|
|
1028
|
+
StatCard,
|
|
1029
|
+
{
|
|
1030
|
+
label: "Total Responses",
|
|
1031
|
+
value: String(stats.totalCount)
|
|
1032
|
+
}
|
|
1033
|
+
),
|
|
1034
|
+
/* @__PURE__ */ jsx(
|
|
1035
|
+
StatCard,
|
|
1036
|
+
{
|
|
1037
|
+
label: "Completion Rate",
|
|
1038
|
+
value: `${Math.round(stats.completionRate * 100)}%`
|
|
1039
|
+
}
|
|
1040
|
+
),
|
|
1041
|
+
/* @__PURE__ */ jsx(
|
|
1042
|
+
StatCard,
|
|
1043
|
+
{
|
|
1044
|
+
label: "Avg. Completion Time",
|
|
1045
|
+
value: formatTime(stats.avgCompletionTimeMs)
|
|
1046
|
+
}
|
|
1047
|
+
),
|
|
1048
|
+
/* @__PURE__ */ jsx(
|
|
1049
|
+
StatCard,
|
|
1050
|
+
{
|
|
1051
|
+
label: "Date Range",
|
|
1052
|
+
value: formatDateRange(stats.dateRange)
|
|
1053
|
+
}
|
|
1054
|
+
)
|
|
1055
|
+
] }),
|
|
1056
|
+
stats.fieldSummaries.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2", children: stats.fieldSummaries.slice(0, 6).map((f) => /* @__PURE__ */ jsxs(
|
|
1057
|
+
"div",
|
|
1058
|
+
{
|
|
1059
|
+
className: "px-2.5 py-2 rounded border border-border bg-background text-xs",
|
|
1060
|
+
children: [
|
|
1061
|
+
/* @__PURE__ */ jsx("div", { className: "text-muted-foreground truncate mb-1", children: f.label }),
|
|
1062
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
1063
|
+
/* @__PURE__ */ jsxs("span", { className: "text-foreground font-medium", children: [
|
|
1064
|
+
Math.round(f.filledRate * 100),
|
|
1065
|
+
"% filled"
|
|
1066
|
+
] }),
|
|
1067
|
+
f.kind === "numeric" && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
1068
|
+
"avg ",
|
|
1069
|
+
f.mean.toFixed(1),
|
|
1070
|
+
" (",
|
|
1071
|
+
f.min,
|
|
1072
|
+
"\u2013",
|
|
1073
|
+
f.max,
|
|
1074
|
+
")"
|
|
1075
|
+
] }),
|
|
1076
|
+
f.kind === "categorical" && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground truncate", children: [
|
|
1077
|
+
"top: ",
|
|
1078
|
+
f.mode,
|
|
1079
|
+
" (",
|
|
1080
|
+
f.modeCount,
|
|
1081
|
+
")"
|
|
1082
|
+
] })
|
|
1083
|
+
] })
|
|
1084
|
+
]
|
|
1085
|
+
},
|
|
1086
|
+
f.fieldId
|
|
1087
|
+
)) })
|
|
1088
|
+
] })
|
|
1089
|
+
] });
|
|
1090
|
+
}
|
|
1091
|
+
function StatCard({ label, value }) {
|
|
1092
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 rounded border border-border bg-background", children: [
|
|
1093
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] text-muted-foreground mb-0.5", children: label }),
|
|
1094
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-foreground", children: value })
|
|
1095
|
+
] });
|
|
1096
|
+
}
|
|
1097
|
+
function formatTime(ms) {
|
|
1098
|
+
if (ms == null) return "\u2014";
|
|
1099
|
+
const seconds = Math.round(ms / 1e3);
|
|
1100
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1101
|
+
const minutes = Math.floor(seconds / 60);
|
|
1102
|
+
const remainingSeconds = seconds % 60;
|
|
1103
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
1104
|
+
}
|
|
1105
|
+
function formatDateRange(range) {
|
|
1106
|
+
if (!range) return "\u2014";
|
|
1107
|
+
const fmt = (iso) => new Date(iso).toLocaleDateString();
|
|
1108
|
+
return `${fmt(range.earliest)} \u2013 ${fmt(range.latest)}`;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/response-viewer/chart-utils.ts
|
|
1112
|
+
function computeFieldDistribution(responses, fieldId) {
|
|
1113
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1114
|
+
let total = 0;
|
|
1115
|
+
for (const r of responses) {
|
|
1116
|
+
const raw = r.values[fieldId];
|
|
1117
|
+
if (raw == null || raw === "") continue;
|
|
1118
|
+
total++;
|
|
1119
|
+
if (typeof raw === "boolean") {
|
|
1120
|
+
const key = raw ? "Yes" : "No";
|
|
1121
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1122
|
+
} else if (Array.isArray(raw)) {
|
|
1123
|
+
for (const item of raw) {
|
|
1124
|
+
const key = String(item);
|
|
1125
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1126
|
+
}
|
|
1127
|
+
} else {
|
|
1128
|
+
const key = String(raw);
|
|
1129
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (total === 0) return [];
|
|
1133
|
+
const entries = [];
|
|
1134
|
+
for (const [value, count] of counts) {
|
|
1135
|
+
entries.push({
|
|
1136
|
+
value,
|
|
1137
|
+
count,
|
|
1138
|
+
percentage: count / total * 100
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
entries.sort((a, b) => b.count - a.count);
|
|
1142
|
+
return entries;
|
|
1143
|
+
}
|
|
1144
|
+
function getChartableFieldIds(schema) {
|
|
1145
|
+
const chartable = [];
|
|
1146
|
+
const chartTypes = /* @__PURE__ */ new Set([
|
|
1147
|
+
"single_select",
|
|
1148
|
+
"dropdown",
|
|
1149
|
+
"country_select",
|
|
1150
|
+
"boolean",
|
|
1151
|
+
"consent",
|
|
1152
|
+
"multi_select",
|
|
1153
|
+
"checkbox_group",
|
|
1154
|
+
"rating",
|
|
1155
|
+
"nps",
|
|
1156
|
+
"opinion_scale",
|
|
1157
|
+
"pain_scale"
|
|
1158
|
+
]);
|
|
1159
|
+
for (const section of schema.sections) {
|
|
1160
|
+
for (const q of section.questions) {
|
|
1161
|
+
if (chartTypes.has(q.type)) {
|
|
1162
|
+
chartable.push({ id: q.id, label: q.label, type: q.type });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return chartable;
|
|
1167
|
+
}
|
|
1168
|
+
function ChartPanel({ schema, responses, onClose }) {
|
|
1169
|
+
const chartableFields = useMemo(() => getChartableFieldIds(schema), [schema]);
|
|
1170
|
+
const [selectedFieldId, setSelectedFieldId] = useState(chartableFields[0]?.id ?? "");
|
|
1171
|
+
const distribution = useMemo(() => {
|
|
1172
|
+
if (!selectedFieldId) return [];
|
|
1173
|
+
return computeFieldDistribution(responses, selectedFieldId);
|
|
1174
|
+
}, [responses, selectedFieldId]);
|
|
1175
|
+
const selectedField = chartableFields.find((f) => f.id === selectedFieldId);
|
|
1176
|
+
const maxCount = distribution.length > 0 ? Math.max(...distribution.map((d) => d.count)) : 1;
|
|
1177
|
+
if (chartableFields.length === 0) {
|
|
1178
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-4 py-6 border-b border-border bg-muted/30 text-center", children: [
|
|
1179
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "No chartable fields found in this schema." }),
|
|
1180
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "Charts work with select, dropdown, boolean, rating, and similar field types." })
|
|
1181
|
+
] });
|
|
1182
|
+
}
|
|
1183
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
1184
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
1185
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1186
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: "Field Distribution" }),
|
|
1187
|
+
/* @__PURE__ */ jsx(
|
|
1188
|
+
"select",
|
|
1189
|
+
{
|
|
1190
|
+
value: selectedFieldId,
|
|
1191
|
+
onChange: (e) => setSelectedFieldId(e.target.value),
|
|
1192
|
+
className: "text-xs bg-background border border-border rounded px-2 py-1 text-foreground",
|
|
1193
|
+
children: chartableFields.map((f) => /* @__PURE__ */ jsx("option", { value: f.id, children: f.label }, f.id))
|
|
1194
|
+
}
|
|
1195
|
+
)
|
|
1196
|
+
] }),
|
|
1197
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
1198
|
+
] }),
|
|
1199
|
+
/* @__PURE__ */ jsxs("div", { className: "px-4 py-3", children: [
|
|
1200
|
+
distribution.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground text-center py-4", children: "No data for this field." }) : /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
1201
|
+
distribution.slice(0, 10).map((entry) => /* @__PURE__ */ jsx(BarRow, { entry, maxCount }, entry.value)),
|
|
1202
|
+
distribution.length > 10 && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-2", children: [
|
|
1203
|
+
"+",
|
|
1204
|
+
distribution.length - 10,
|
|
1205
|
+
" more values"
|
|
1206
|
+
] })
|
|
1207
|
+
] }),
|
|
1208
|
+
selectedField && /* @__PURE__ */ jsxs("div", { className: "mt-3 text-[11px] text-muted-foreground", children: [
|
|
1209
|
+
responses.length,
|
|
1210
|
+
" response",
|
|
1211
|
+
responses.length !== 1 ? "s" : "",
|
|
1212
|
+
" \xB7 ",
|
|
1213
|
+
distribution.length,
|
|
1214
|
+
" unique value",
|
|
1215
|
+
distribution.length !== 1 ? "s" : ""
|
|
1216
|
+
] })
|
|
1217
|
+
] })
|
|
1218
|
+
] });
|
|
1219
|
+
}
|
|
1220
|
+
function BarRow({ entry, maxCount }) {
|
|
1221
|
+
const widthPct = maxCount > 0 ? entry.count / maxCount * 100 : 0;
|
|
1222
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1223
|
+
/* @__PURE__ */ jsx("div", { className: "w-24 shrink-0 text-xs text-foreground truncate", title: entry.value, children: entry.value }),
|
|
1224
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 h-5 bg-background rounded overflow-hidden border border-border/50", children: /* @__PURE__ */ jsx(
|
|
1225
|
+
"div",
|
|
1226
|
+
{
|
|
1227
|
+
className: "h-full bg-primary/60 rounded transition-all",
|
|
1228
|
+
style: { width: `${widthPct}%` }
|
|
1229
|
+
}
|
|
1230
|
+
) }),
|
|
1231
|
+
/* @__PURE__ */ jsxs("div", { className: "w-16 shrink-0 text-xs text-muted-foreground text-right", children: [
|
|
1232
|
+
entry.count,
|
|
1233
|
+
" (",
|
|
1234
|
+
Math.round(entry.percentage),
|
|
1235
|
+
"%)"
|
|
1236
|
+
] })
|
|
1237
|
+
] });
|
|
1238
|
+
}
|
|
1239
|
+
function ConfirmDialog({
|
|
1240
|
+
title,
|
|
1241
|
+
message,
|
|
1242
|
+
confirmLabel = "Confirm",
|
|
1243
|
+
cancelLabel = "Cancel",
|
|
1244
|
+
variant = "default",
|
|
1245
|
+
onConfirm,
|
|
1246
|
+
onCancel
|
|
1247
|
+
}) {
|
|
1248
|
+
return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1249
|
+
/* @__PURE__ */ jsx(
|
|
1250
|
+
"div",
|
|
1251
|
+
{
|
|
1252
|
+
className: "absolute inset-0 bg-black/50",
|
|
1253
|
+
onClick: onCancel
|
|
1254
|
+
}
|
|
1255
|
+
),
|
|
1256
|
+
/* @__PURE__ */ jsxs("div", { className: "relative bg-background border border-border rounded-lg shadow-lg p-6 max-w-sm w-full mx-4", children: [
|
|
1257
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground mb-2", children: title }),
|
|
1258
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: message }),
|
|
1259
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
1260
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onCancel, children: cancelLabel }),
|
|
1261
|
+
/* @__PURE__ */ jsx(
|
|
1262
|
+
Button,
|
|
1263
|
+
{
|
|
1264
|
+
variant: variant === "destructive" ? "destructive" : "default",
|
|
1265
|
+
size: "sm",
|
|
1266
|
+
onClick: onConfirm,
|
|
1267
|
+
children: confirmLabel
|
|
1268
|
+
}
|
|
1269
|
+
)
|
|
1270
|
+
] })
|
|
1271
|
+
] })
|
|
1272
|
+
] });
|
|
1273
|
+
}
|
|
733
1274
|
|
|
734
1275
|
// src/response-viewer/export-utils.ts
|
|
735
1276
|
function buildCsvContent(schema, responses, options = {}) {
|
|
@@ -864,6 +1405,84 @@ function paginate(items, currentPage, pageSize) {
|
|
|
864
1405
|
};
|
|
865
1406
|
}
|
|
866
1407
|
|
|
1408
|
+
// src/response-viewer/search-utils.ts
|
|
1409
|
+
function searchResponses(responses, query, schema) {
|
|
1410
|
+
if (!query.trim()) return responses;
|
|
1411
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
1412
|
+
const questions = getSearchableQuestions(schema);
|
|
1413
|
+
return responses.filter((response) => {
|
|
1414
|
+
if (new Date(response.submittedAt).toLocaleString().toLowerCase().includes(lowerQuery)) {
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
if (response.sessionToken?.toLowerCase().includes(lowerQuery)) {
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
for (const q of questions) {
|
|
1421
|
+
const value = response.values[q.id];
|
|
1422
|
+
if (value == null) continue;
|
|
1423
|
+
const stringified = stringifyValue(value, q.type);
|
|
1424
|
+
if (stringified.toLowerCase().includes(lowerQuery)) {
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (response.totalScore != null && String(response.totalScore).includes(lowerQuery)) {
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
return false;
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
function getSearchableQuestions(schema) {
|
|
1435
|
+
const questions = [];
|
|
1436
|
+
for (const section of schema.sections) {
|
|
1437
|
+
for (const q of section.questions) {
|
|
1438
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
questions.push(q);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return questions;
|
|
1445
|
+
}
|
|
1446
|
+
function stringifyValue(value, type) {
|
|
1447
|
+
if (value == null) return "";
|
|
1448
|
+
switch (type) {
|
|
1449
|
+
case "vitals_entry": {
|
|
1450
|
+
if (typeof value !== "object" || value === null) break;
|
|
1451
|
+
const v = value;
|
|
1452
|
+
const parts = [];
|
|
1453
|
+
if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
|
|
1454
|
+
if (v.heartRate) parts.push(`HR ${v.heartRate}`);
|
|
1455
|
+
if (v.temperature) parts.push(`${v.temperature}`);
|
|
1456
|
+
if (v.oxygenSaturation) parts.push(`SpO2 ${v.oxygenSaturation}`);
|
|
1457
|
+
return parts.join(" ");
|
|
1458
|
+
}
|
|
1459
|
+
case "medication_list":
|
|
1460
|
+
if (Array.isArray(value)) {
|
|
1461
|
+
return value.map((m) => [m.name, m.dosage, m.frequency].filter(Boolean).join(" ")).join(" ");
|
|
1462
|
+
}
|
|
1463
|
+
break;
|
|
1464
|
+
case "allergy_list":
|
|
1465
|
+
if (Array.isArray(value)) {
|
|
1466
|
+
return value.map((a) => [a.allergen, a.severity].filter(Boolean).join(" ")).join(" ");
|
|
1467
|
+
}
|
|
1468
|
+
break;
|
|
1469
|
+
case "legal_name": {
|
|
1470
|
+
if (typeof value !== "object" || value === null) break;
|
|
1471
|
+
const n = value;
|
|
1472
|
+
return [n.first, n.middle, n.last].filter(Boolean).join(" ");
|
|
1473
|
+
}
|
|
1474
|
+
case "address": {
|
|
1475
|
+
if (typeof value !== "object" || value === null) break;
|
|
1476
|
+
const a = value;
|
|
1477
|
+
return [a.street, a.city, a.state, a.zip].filter(Boolean).join(" ");
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
1481
|
+
if (Array.isArray(value)) return value.map(String).join(" ");
|
|
1482
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1483
|
+
return String(value);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
867
1486
|
// src/response-viewer/filter-utils.ts
|
|
868
1487
|
function createEmptyFilterState() {
|
|
869
1488
|
return { filters: [], dateRange: {} };
|
|
@@ -962,7 +1581,11 @@ function ResponseViewerInner({
|
|
|
962
1581
|
dateFormat,
|
|
963
1582
|
columnLabels,
|
|
964
1583
|
onExport,
|
|
965
|
-
pageSize = 25
|
|
1584
|
+
pageSize = 25,
|
|
1585
|
+
onDelete,
|
|
1586
|
+
onBulkDelete,
|
|
1587
|
+
onBulkExport,
|
|
1588
|
+
selectable = false
|
|
966
1589
|
}) {
|
|
967
1590
|
const [viewMode, setViewMode] = useState("table");
|
|
968
1591
|
const [selectedResponse, setSelectedResponse] = useState(null);
|
|
@@ -970,6 +1593,11 @@ function ResponseViewerInner({
|
|
|
970
1593
|
const [effectivePageSize, setEffectivePageSize] = useState(pageSize);
|
|
971
1594
|
const [filterState, setFilterState] = useState(createEmptyFilterState);
|
|
972
1595
|
const [showFilterPanel, setShowFilterPanel] = useState(false);
|
|
1596
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
1597
|
+
const [showStats, setShowStats] = useState(false);
|
|
1598
|
+
const [showCharts, setShowCharts] = useState(false);
|
|
1599
|
+
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
1600
|
+
const [confirmDialog, setConfirmDialog] = useState(null);
|
|
973
1601
|
const [draftFieldId, setDraftFieldId] = useState("");
|
|
974
1602
|
const [draftOperator, setDraftOperator] = useState("contains");
|
|
975
1603
|
const [draftValue, setDraftValue] = useState("");
|
|
@@ -980,9 +1608,18 @@ function ResponseViewerInner({
|
|
|
980
1608
|
setEffectivePageSize(pageSize);
|
|
981
1609
|
setCurrentPage(1);
|
|
982
1610
|
}, [pageSize]);
|
|
1611
|
+
useEffect(() => {
|
|
1612
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
1613
|
+
}, [responses]);
|
|
1614
|
+
const searchedResponses = useMemo(
|
|
1615
|
+
() => searchResponses(responses, searchQuery, schema),
|
|
1616
|
+
[responses, searchQuery, schema]
|
|
1617
|
+
);
|
|
983
1618
|
const isFiltered = hasActiveFilters(filterState);
|
|
984
|
-
const filteredResponses = applyFilters(
|
|
1619
|
+
const filteredResponses = applyFilters(searchedResponses, filterState);
|
|
985
1620
|
const pag = paginate(filteredResponses, currentPage, effectivePageSize);
|
|
1621
|
+
const questions = getAllQuestions2(schema);
|
|
1622
|
+
const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
|
|
986
1623
|
function handleSelect(response) {
|
|
987
1624
|
setSelectedResponse(response);
|
|
988
1625
|
onResponseSelect?.(response);
|
|
@@ -1029,7 +1666,59 @@ function ResponseViewerInner({
|
|
|
1029
1666
|
setFilterState(createEmptyFilterState());
|
|
1030
1667
|
setCurrentPage(1);
|
|
1031
1668
|
}
|
|
1032
|
-
|
|
1669
|
+
function handleToggleSelect(token) {
|
|
1670
|
+
setSelectedIds((prev) => {
|
|
1671
|
+
const next = new Set(prev);
|
|
1672
|
+
if (next.has(token)) {
|
|
1673
|
+
next.delete(token);
|
|
1674
|
+
} else {
|
|
1675
|
+
next.add(token);
|
|
1676
|
+
}
|
|
1677
|
+
return next;
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
function handleSelectAll() {
|
|
1681
|
+
const currentTokens = pag.pageItems.map((r) => r.sessionToken).filter(Boolean);
|
|
1682
|
+
const allSelected = currentTokens.every((t) => selectedIds.has(t));
|
|
1683
|
+
setSelectedIds((prev) => {
|
|
1684
|
+
const next = new Set(prev);
|
|
1685
|
+
if (allSelected) {
|
|
1686
|
+
currentTokens.forEach((t) => next.delete(t));
|
|
1687
|
+
} else {
|
|
1688
|
+
currentTokens.forEach((t) => next.add(t));
|
|
1689
|
+
}
|
|
1690
|
+
return next;
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
function handleBulkDelete() {
|
|
1694
|
+
if (!onBulkDelete || selectedIds.size === 0) return;
|
|
1695
|
+
setConfirmDialog({
|
|
1696
|
+
title: "Delete selected responses",
|
|
1697
|
+
message: `Are you sure you want to delete ${selectedIds.size} response${selectedIds.size !== 1 ? "s" : ""}? This cannot be undone.`,
|
|
1698
|
+
onConfirm: () => {
|
|
1699
|
+
onBulkDelete(Array.from(selectedIds));
|
|
1700
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
1701
|
+
setConfirmDialog(null);
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
function handleBulkExport() {
|
|
1706
|
+
if (!onBulkExport || selectedIds.size === 0) return;
|
|
1707
|
+
const selected = responses.filter((r) => r.sessionToken && selectedIds.has(r.sessionToken));
|
|
1708
|
+
onBulkExport(selected);
|
|
1709
|
+
}
|
|
1710
|
+
function handleDeleteSingle(token) {
|
|
1711
|
+
if (!onDelete) return;
|
|
1712
|
+
setConfirmDialog({
|
|
1713
|
+
title: "Delete response",
|
|
1714
|
+
message: "Are you sure you want to delete this response? This cannot be undone.",
|
|
1715
|
+
onConfirm: () => {
|
|
1716
|
+
onDelete(token);
|
|
1717
|
+
setSelectedResponse(null);
|
|
1718
|
+
setConfirmDialog(null);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1033
1722
|
function getFields(response) {
|
|
1034
1723
|
return questions.map((q) => ({
|
|
1035
1724
|
questionId: q.id,
|
|
@@ -1040,6 +1729,19 @@ function ResponseViewerInner({
|
|
|
1040
1729
|
}
|
|
1041
1730
|
const heightValue = typeof height === "number" ? `${height}px` : height;
|
|
1042
1731
|
const widthValue = typeof width === "number" ? `${width}px` : width;
|
|
1732
|
+
const countText = (() => {
|
|
1733
|
+
const hasSearch = searchQuery.trim().length > 0;
|
|
1734
|
+
if (hasSearch && isFiltered) {
|
|
1735
|
+
return `${filteredResponses.length} of ${responses.length} responses (searched + filtered)`;
|
|
1736
|
+
}
|
|
1737
|
+
if (hasSearch) {
|
|
1738
|
+
return `${searchedResponses.length} result${searchedResponses.length !== 1 ? "s" : ""} for "${searchQuery}"`;
|
|
1739
|
+
}
|
|
1740
|
+
if (isFiltered) {
|
|
1741
|
+
return `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)`;
|
|
1742
|
+
}
|
|
1743
|
+
return `${responses.length} response${responses.length !== 1 ? "s" : ""}`;
|
|
1744
|
+
})();
|
|
1043
1745
|
return /* @__PURE__ */ jsxs(
|
|
1044
1746
|
"div",
|
|
1045
1747
|
{
|
|
@@ -1047,7 +1749,20 @@ function ResponseViewerInner({
|
|
|
1047
1749
|
style: { height: heightValue, width: widthValue },
|
|
1048
1750
|
children: [
|
|
1049
1751
|
!selectedResponse && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-border", children: [
|
|
1050
|
-
/* @__PURE__ */ jsx(
|
|
1752
|
+
/* @__PURE__ */ jsx(
|
|
1753
|
+
Input,
|
|
1754
|
+
{
|
|
1755
|
+
type: "text",
|
|
1756
|
+
placeholder: "Search responses...",
|
|
1757
|
+
className: "h-7 text-xs w-40",
|
|
1758
|
+
value: searchQuery,
|
|
1759
|
+
onChange: (e) => {
|
|
1760
|
+
setSearchQuery(e.target.value);
|
|
1761
|
+
setCurrentPage(1);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
),
|
|
1765
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: countText }),
|
|
1051
1766
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1052
1767
|
/* @__PURE__ */ jsx(
|
|
1053
1768
|
Button,
|
|
@@ -1074,6 +1789,48 @@ function ResponseViewerInner({
|
|
|
1074
1789
|
onClick: () => setViewMode("card"),
|
|
1075
1790
|
children: "Cards"
|
|
1076
1791
|
}
|
|
1792
|
+
),
|
|
1793
|
+
/* @__PURE__ */ jsx(
|
|
1794
|
+
Button,
|
|
1795
|
+
{
|
|
1796
|
+
variant: viewMode === "timeline" ? "secondary" : "ghost",
|
|
1797
|
+
size: "sm",
|
|
1798
|
+
className: cn(
|
|
1799
|
+
"h-7 text-xs",
|
|
1800
|
+
viewMode === "timeline" && "font-semibold border border-ring"
|
|
1801
|
+
),
|
|
1802
|
+
onClick: () => setViewMode("timeline"),
|
|
1803
|
+
children: "Timeline"
|
|
1804
|
+
}
|
|
1805
|
+
)
|
|
1806
|
+
] }),
|
|
1807
|
+
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
1808
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1809
|
+
/* @__PURE__ */ jsx(
|
|
1810
|
+
Button,
|
|
1811
|
+
{
|
|
1812
|
+
variant: showStats ? "secondary" : "ghost",
|
|
1813
|
+
size: "sm",
|
|
1814
|
+
className: cn(
|
|
1815
|
+
"h-7 text-xs",
|
|
1816
|
+
showStats && "font-semibold border border-ring"
|
|
1817
|
+
),
|
|
1818
|
+
onClick: () => setShowStats((v) => !v),
|
|
1819
|
+
children: "Stats"
|
|
1820
|
+
}
|
|
1821
|
+
),
|
|
1822
|
+
/* @__PURE__ */ jsx(
|
|
1823
|
+
Button,
|
|
1824
|
+
{
|
|
1825
|
+
variant: showCharts ? "secondary" : "ghost",
|
|
1826
|
+
size: "sm",
|
|
1827
|
+
className: cn(
|
|
1828
|
+
"h-7 text-xs",
|
|
1829
|
+
showCharts && "font-semibold border border-ring"
|
|
1830
|
+
),
|
|
1831
|
+
onClick: () => setShowCharts((v) => !v),
|
|
1832
|
+
children: "Charts"
|
|
1833
|
+
}
|
|
1077
1834
|
)
|
|
1078
1835
|
] }),
|
|
1079
1836
|
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
@@ -1139,6 +1896,26 @@ function ResponseViewerInner({
|
|
|
1139
1896
|
)
|
|
1140
1897
|
] })
|
|
1141
1898
|
] }),
|
|
1899
|
+
!selectedResponse && isSelectable && selectedIds.size > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border bg-accent/50", children: [
|
|
1900
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-foreground", children: [
|
|
1901
|
+
selectedIds.size,
|
|
1902
|
+
" selected"
|
|
1903
|
+
] }),
|
|
1904
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
|
|
1905
|
+
onBulkExport && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "h-6 text-xs", onClick: handleBulkExport, children: "Export Selected" }),
|
|
1906
|
+
onBulkDelete && /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", className: "h-6 text-xs", onClick: handleBulkDelete, children: "Delete Selected" }),
|
|
1907
|
+
/* @__PURE__ */ jsx(
|
|
1908
|
+
Button,
|
|
1909
|
+
{
|
|
1910
|
+
variant: "ghost",
|
|
1911
|
+
size: "sm",
|
|
1912
|
+
className: "h-6 text-xs",
|
|
1913
|
+
onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
|
|
1914
|
+
children: "Clear"
|
|
1915
|
+
}
|
|
1916
|
+
)
|
|
1917
|
+
] })
|
|
1918
|
+
] }),
|
|
1142
1919
|
!selectedResponse && showFilterPanel && /* @__PURE__ */ jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
|
|
1143
1920
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
|
|
1144
1921
|
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
|
|
@@ -1267,19 +2044,47 @@ function ResponseViewerInner({
|
|
|
1267
2044
|
);
|
|
1268
2045
|
})
|
|
1269
2046
|
] }),
|
|
1270
|
-
|
|
2047
|
+
!selectedResponse && showStats && /* @__PURE__ */ jsx(
|
|
2048
|
+
StatsPanel,
|
|
2049
|
+
{
|
|
2050
|
+
schema,
|
|
2051
|
+
responses: filteredResponses,
|
|
2052
|
+
onClose: () => setShowStats(false)
|
|
2053
|
+
}
|
|
2054
|
+
),
|
|
2055
|
+
!selectedResponse && showCharts && /* @__PURE__ */ jsx(
|
|
2056
|
+
ChartPanel,
|
|
2057
|
+
{
|
|
2058
|
+
schema,
|
|
2059
|
+
responses: filteredResponses,
|
|
2060
|
+
onClose: () => setShowCharts(false)
|
|
2061
|
+
}
|
|
2062
|
+
),
|
|
2063
|
+
/* @__PURE__ */ jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
1271
2064
|
ResponseDetail,
|
|
1272
2065
|
{
|
|
1273
2066
|
response: selectedResponse,
|
|
1274
2067
|
fields: getFields(selectedResponse),
|
|
1275
|
-
onBack: handleBack
|
|
2068
|
+
onBack: handleBack,
|
|
2069
|
+
onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
|
|
2070
|
+
}
|
|
2071
|
+
) }) : viewMode === "timeline" ? /* @__PURE__ */ jsx(
|
|
2072
|
+
TimelineView,
|
|
2073
|
+
{
|
|
2074
|
+
responses: filteredResponses,
|
|
2075
|
+
questions,
|
|
2076
|
+
onSelect: handleSelect
|
|
1276
2077
|
}
|
|
1277
2078
|
) : viewMode === "table" ? /* @__PURE__ */ jsx(
|
|
1278
2079
|
ResponseTable,
|
|
1279
2080
|
{
|
|
1280
2081
|
schema,
|
|
1281
2082
|
responses: pag.pageItems,
|
|
1282
|
-
onRowClick: handleSelect
|
|
2083
|
+
onRowClick: handleSelect,
|
|
2084
|
+
selectable: isSelectable,
|
|
2085
|
+
selectedIds,
|
|
2086
|
+
onToggleSelect: handleToggleSelect,
|
|
2087
|
+
onSelectAll: handleSelectAll
|
|
1283
2088
|
}
|
|
1284
2089
|
) : /* @__PURE__ */ jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
|
|
1285
2090
|
pag.pageItems.map((response, idx) => /* @__PURE__ */ jsx(
|
|
@@ -1287,13 +2092,16 @@ function ResponseViewerInner({
|
|
|
1287
2092
|
{
|
|
1288
2093
|
response,
|
|
1289
2094
|
fields: getFields(response),
|
|
1290
|
-
onClick: () => handleSelect(response)
|
|
2095
|
+
onClick: () => handleSelect(response),
|
|
2096
|
+
selectable: isSelectable,
|
|
2097
|
+
selected: !!(response.sessionToken && selectedIds.has(response.sessionToken)),
|
|
2098
|
+
onToggleSelect: response.sessionToken ? () => handleToggleSelect(response.sessionToken) : void 0
|
|
1291
2099
|
},
|
|
1292
2100
|
response.sessionToken || idx
|
|
1293
2101
|
)),
|
|
1294
|
-
filteredResponses.length === 0 && /* @__PURE__ */ jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
|
|
2102
|
+
filteredResponses.length === 0 && /* @__PURE__ */ jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered || searchQuery.trim() ? "No matching responses" : "No responses yet" })
|
|
1295
2103
|
] }) }),
|
|
1296
|
-
!selectedResponse && pag.showPagination && /* @__PURE__ */ jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
|
|
2104
|
+
!selectedResponse && viewMode !== "timeline" && pag.showPagination && /* @__PURE__ */ jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
|
|
1297
2105
|
/* @__PURE__ */ jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
|
|
1298
2106
|
"Showing ",
|
|
1299
2107
|
pag.startItem,
|
|
@@ -1347,7 +2155,18 @@ function ResponseViewerInner({
|
|
|
1347
2155
|
}
|
|
1348
2156
|
)
|
|
1349
2157
|
] })
|
|
1350
|
-
] })
|
|
2158
|
+
] }),
|
|
2159
|
+
confirmDialog && /* @__PURE__ */ jsx(
|
|
2160
|
+
ConfirmDialog,
|
|
2161
|
+
{
|
|
2162
|
+
title: confirmDialog.title,
|
|
2163
|
+
message: confirmDialog.message,
|
|
2164
|
+
confirmLabel: "Delete",
|
|
2165
|
+
variant: "destructive",
|
|
2166
|
+
onConfirm: confirmDialog.onConfirm,
|
|
2167
|
+
onCancel: () => setConfirmDialog(null)
|
|
2168
|
+
}
|
|
2169
|
+
)
|
|
1351
2170
|
]
|
|
1352
2171
|
}
|
|
1353
2172
|
);
|