@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
|
@@ -465,10 +465,30 @@ function getScoreSeverity(instrumentKey, score) {
|
|
|
465
465
|
if (!thresholds) return void 0;
|
|
466
466
|
return thresholds.find((t) => score >= t.min && score <= t.max);
|
|
467
467
|
}
|
|
468
|
-
function ResponseTable({
|
|
468
|
+
function ResponseTable({
|
|
469
|
+
schema,
|
|
470
|
+
responses,
|
|
471
|
+
onRowClick,
|
|
472
|
+
selectable,
|
|
473
|
+
selectedIds,
|
|
474
|
+
onToggleSelect,
|
|
475
|
+
onSelectAll
|
|
476
|
+
}) {
|
|
469
477
|
const questions = getAllQuestions(schema);
|
|
478
|
+
const allPageSelected = selectable && responses.length > 0 && responses.every(
|
|
479
|
+
(r) => r.sessionToken && selectedIds?.has(r.sessionToken)
|
|
480
|
+
);
|
|
470
481
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
|
|
471
482
|
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "bg-muted", children: [
|
|
483
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
484
|
+
"input",
|
|
485
|
+
{
|
|
486
|
+
type: "checkbox",
|
|
487
|
+
checked: allPageSelected,
|
|
488
|
+
onChange: () => onSelectAll?.(),
|
|
489
|
+
className: "accent-primary"
|
|
490
|
+
}
|
|
491
|
+
) }),
|
|
472
492
|
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
|
|
473
493
|
questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
474
494
|
"th",
|
|
@@ -487,6 +507,19 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
487
507
|
onClick: () => onRowClick?.(response),
|
|
488
508
|
className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
|
|
489
509
|
children: [
|
|
510
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
511
|
+
"input",
|
|
512
|
+
{
|
|
513
|
+
type: "checkbox",
|
|
514
|
+
checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
|
|
515
|
+
onChange: (e) => {
|
|
516
|
+
e.stopPropagation();
|
|
517
|
+
if (response.sessionToken) onToggleSelect?.(response.sessionToken);
|
|
518
|
+
},
|
|
519
|
+
onClick: (e) => e.stopPropagation(),
|
|
520
|
+
className: "accent-primary"
|
|
521
|
+
}
|
|
522
|
+
) }),
|
|
490
523
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
|
|
491
524
|
questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
492
525
|
"td",
|
|
@@ -504,7 +537,7 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
504
537
|
responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
505
538
|
"td",
|
|
506
539
|
{
|
|
507
|
-
colSpan: questions.length + 2,
|
|
540
|
+
colSpan: questions.length + 2 + (selectable ? 1 : 0),
|
|
508
541
|
className: "px-3 py-8 text-center text-muted-foreground",
|
|
509
542
|
children: "No responses yet"
|
|
510
543
|
}
|
|
@@ -592,15 +625,37 @@ function formatCellValue(value, type) {
|
|
|
592
625
|
if (typeof value === "object") return JSON.stringify(value);
|
|
593
626
|
return String(value);
|
|
594
627
|
}
|
|
595
|
-
function ResponseCard({
|
|
628
|
+
function ResponseCard({
|
|
629
|
+
response,
|
|
630
|
+
fields,
|
|
631
|
+
onClick,
|
|
632
|
+
selectable,
|
|
633
|
+
selected,
|
|
634
|
+
onToggleSelect
|
|
635
|
+
}) {
|
|
596
636
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
597
637
|
"div",
|
|
598
638
|
{
|
|
599
639
|
onClick,
|
|
600
|
-
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",
|
|
640
|
+
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" : ""),
|
|
601
641
|
children: [
|
|
602
642
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between mb-3 text-xs text-muted-foreground", children: [
|
|
603
|
-
/* @__PURE__ */ jsxRuntime.
|
|
643
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
644
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
645
|
+
"input",
|
|
646
|
+
{
|
|
647
|
+
type: "checkbox",
|
|
648
|
+
checked: !!selected,
|
|
649
|
+
onChange: (e) => {
|
|
650
|
+
e.stopPropagation();
|
|
651
|
+
onToggleSelect?.();
|
|
652
|
+
},
|
|
653
|
+
onClick: (e) => e.stopPropagation(),
|
|
654
|
+
className: "accent-primary"
|
|
655
|
+
}
|
|
656
|
+
),
|
|
657
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(response.submittedAt).toLocaleString() })
|
|
658
|
+
] }),
|
|
604
659
|
response.completionTimeMs != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
605
660
|
Math.round(response.completionTimeMs / 1e3),
|
|
606
661
|
"s"
|
|
@@ -691,18 +746,19 @@ function formatCardValue(value, type) {
|
|
|
691
746
|
if (typeof value === "object") return JSON.stringify(value);
|
|
692
747
|
return String(value);
|
|
693
748
|
}
|
|
694
|
-
function ResponseDetail({ response, fields, onBack }) {
|
|
749
|
+
function ResponseDetail({ response, fields, onBack, onDelete }) {
|
|
695
750
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
696
751
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-5 pb-3 border-b border-border", children: [
|
|
697
752
|
onBack && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onBack, children: "Back" }),
|
|
698
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
753
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
|
|
699
754
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: "Response Detail" }),
|
|
700
755
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground", children: [
|
|
701
756
|
"Submitted ",
|
|
702
757
|
new Date(response.submittedAt).toLocaleString(),
|
|
703
758
|
response.completionTimeMs != null && ` \u2014 ${Math.round(response.completionTimeMs / 1e3)}s`
|
|
704
759
|
] })
|
|
705
|
-
] })
|
|
760
|
+
] }),
|
|
761
|
+
onDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", onClick: onDelete, children: "Delete" })
|
|
706
762
|
] }),
|
|
707
763
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
708
764
|
"div",
|
|
@@ -1055,6 +1111,491 @@ function formatFallbackValue(value) {
|
|
|
1055
1111
|
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
1056
1112
|
return String(value);
|
|
1057
1113
|
}
|
|
1114
|
+
function TimelineView({ responses, questions, onSelect }) {
|
|
1115
|
+
const grouped = react.useMemo(() => groupByDay(responses), [responses]);
|
|
1116
|
+
if (responses.length === 0) {
|
|
1117
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground text-sm", children: "No responses to display." });
|
|
1118
|
+
}
|
|
1119
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6", children: grouped.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1120
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
1121
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" }),
|
|
1122
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground shrink-0", children: group.dateLabel }),
|
|
1123
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
|
|
1124
|
+
"(",
|
|
1125
|
+
group.responses.length,
|
|
1126
|
+
")"
|
|
1127
|
+
] }),
|
|
1128
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" })
|
|
1129
|
+
] }),
|
|
1130
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 ml-4 border-l-2 border-border/50 pl-4", children: group.responses.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1131
|
+
TimelineEntry,
|
|
1132
|
+
{
|
|
1133
|
+
response,
|
|
1134
|
+
questions,
|
|
1135
|
+
onView: () => onSelect(response)
|
|
1136
|
+
},
|
|
1137
|
+
response.sessionToken || idx
|
|
1138
|
+
)) })
|
|
1139
|
+
] }, group.dateLabel)) });
|
|
1140
|
+
}
|
|
1141
|
+
function TimelineEntry({
|
|
1142
|
+
response,
|
|
1143
|
+
questions,
|
|
1144
|
+
onView
|
|
1145
|
+
}) {
|
|
1146
|
+
const time = new Date(response.submittedAt).toLocaleTimeString([], {
|
|
1147
|
+
hour: "2-digit",
|
|
1148
|
+
minute: "2-digit"
|
|
1149
|
+
});
|
|
1150
|
+
const preview = [];
|
|
1151
|
+
for (const q of questions) {
|
|
1152
|
+
if (preview.length >= 3) break;
|
|
1153
|
+
const val = response.values[q.id];
|
|
1154
|
+
if (val != null && val !== "") {
|
|
1155
|
+
preview.push({ questionId: q.id, label: q.label, type: q.type, value: val });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-start gap-3 group", children: [
|
|
1159
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -left-[21px] top-2.5 w-2 h-2 rounded-full bg-border group-hover:bg-primary transition-colors" }),
|
|
1160
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 rounded border border-border bg-card p-3 hover:border-primary/40 transition-colors", children: [
|
|
1161
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
1162
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: time }),
|
|
1163
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1164
|
+
response.totalScore != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
1165
|
+
"Score: ",
|
|
1166
|
+
response.totalScore
|
|
1167
|
+
] }),
|
|
1168
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onView, children: "View" })
|
|
1169
|
+
] })
|
|
1170
|
+
] }),
|
|
1171
|
+
preview.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: preview.map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs", children: [
|
|
1172
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
1173
|
+
field.label,
|
|
1174
|
+
": "
|
|
1175
|
+
] }),
|
|
1176
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground", children: formatPreviewValue(field.value, field.type) })
|
|
1177
|
+
] }, field.questionId)) })
|
|
1178
|
+
] })
|
|
1179
|
+
] });
|
|
1180
|
+
}
|
|
1181
|
+
function groupByDay(responses) {
|
|
1182
|
+
const sorted = [...responses].sort(
|
|
1183
|
+
(a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()
|
|
1184
|
+
);
|
|
1185
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1186
|
+
for (const r of sorted) {
|
|
1187
|
+
const date = new Date(r.submittedAt).toLocaleDateString(void 0, {
|
|
1188
|
+
weekday: "long",
|
|
1189
|
+
year: "numeric",
|
|
1190
|
+
month: "long",
|
|
1191
|
+
day: "numeric"
|
|
1192
|
+
});
|
|
1193
|
+
const existing = groups.get(date);
|
|
1194
|
+
if (existing) {
|
|
1195
|
+
existing.push(r);
|
|
1196
|
+
} else {
|
|
1197
|
+
groups.set(date, [r]);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return Array.from(groups.entries()).map(([dateLabel, responses2]) => ({
|
|
1201
|
+
dateLabel,
|
|
1202
|
+
responses: responses2
|
|
1203
|
+
}));
|
|
1204
|
+
}
|
|
1205
|
+
function formatPreviewValue(value, type) {
|
|
1206
|
+
if (value == null) return "\u2014";
|
|
1207
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
1208
|
+
if (Array.isArray(value)) return value.length > 0 ? value.slice(0, 2).join(", ") + (value.length > 2 ? "..." : "") : "\u2014";
|
|
1209
|
+
if (typeof value === "object") {
|
|
1210
|
+
if (type === "legal_name") {
|
|
1211
|
+
const n = value;
|
|
1212
|
+
return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
|
|
1213
|
+
}
|
|
1214
|
+
return "...";
|
|
1215
|
+
}
|
|
1216
|
+
const str = String(value);
|
|
1217
|
+
return str.length > 40 ? str.slice(0, 40) + "..." : str;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/response-viewer/stats-utils.ts
|
|
1221
|
+
function computeStats(responses, schema) {
|
|
1222
|
+
if (responses.length === 0) {
|
|
1223
|
+
return {
|
|
1224
|
+
totalCount: 0,
|
|
1225
|
+
completionRate: 0,
|
|
1226
|
+
avgCompletionTimeMs: null,
|
|
1227
|
+
dateRange: null,
|
|
1228
|
+
fieldSummaries: []
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
const questions = getStatsQuestions(schema);
|
|
1232
|
+
const requiredIds = new Set(questions.filter((q) => q.required).map((q) => q.id));
|
|
1233
|
+
let completedCount = 0;
|
|
1234
|
+
for (const r of responses) {
|
|
1235
|
+
let allFilled = true;
|
|
1236
|
+
for (const id of requiredIds) {
|
|
1237
|
+
if (r.values[id] == null || r.values[id] === "") {
|
|
1238
|
+
allFilled = false;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (allFilled) completedCount++;
|
|
1243
|
+
}
|
|
1244
|
+
const completionRate = requiredIds.size > 0 ? completedCount / responses.length : 1;
|
|
1245
|
+
const completionTimes = responses.map((r) => r.completionTimeMs).filter((t) => t != null && t > 0);
|
|
1246
|
+
const avgCompletionTimeMs = completionTimes.length > 0 ? completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length : null;
|
|
1247
|
+
const timestamps = responses.map((r) => new Date(r.submittedAt).getTime());
|
|
1248
|
+
const earliest = new Date(Math.min(...timestamps)).toISOString();
|
|
1249
|
+
const latest = new Date(Math.max(...timestamps)).toISOString();
|
|
1250
|
+
const fieldSummaries = questions.map((q) => computeFieldSummary(q, responses));
|
|
1251
|
+
return {
|
|
1252
|
+
totalCount: responses.length,
|
|
1253
|
+
completionRate,
|
|
1254
|
+
avgCompletionTimeMs,
|
|
1255
|
+
dateRange: { earliest, latest },
|
|
1256
|
+
fieldSummaries
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function computeFieldSummary(question, responses) {
|
|
1260
|
+
const values = responses.map((r) => r.values[question.id]).filter((v) => v != null && v !== "");
|
|
1261
|
+
const filledCount = values.length;
|
|
1262
|
+
const filledRate = responses.length > 0 ? filledCount / responses.length : 0;
|
|
1263
|
+
const base = {
|
|
1264
|
+
fieldId: question.id,
|
|
1265
|
+
label: question.label,
|
|
1266
|
+
type: question.type,
|
|
1267
|
+
filledCount,
|
|
1268
|
+
filledRate
|
|
1269
|
+
};
|
|
1270
|
+
if (isNumericType(question.type)) {
|
|
1271
|
+
const nums = values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => !isNaN(n));
|
|
1272
|
+
if (nums.length > 0) {
|
|
1273
|
+
return {
|
|
1274
|
+
...base,
|
|
1275
|
+
kind: "numeric",
|
|
1276
|
+
mean: nums.reduce((a, b) => a + b, 0) / nums.length,
|
|
1277
|
+
min: Math.min(...nums),
|
|
1278
|
+
max: Math.max(...nums)
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
return { ...base, kind: "other" };
|
|
1282
|
+
}
|
|
1283
|
+
if (isCategoricalType(question.type)) {
|
|
1284
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1285
|
+
for (const v of values) {
|
|
1286
|
+
const key = String(v);
|
|
1287
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1288
|
+
}
|
|
1289
|
+
let mode = "";
|
|
1290
|
+
let modeCount = 0;
|
|
1291
|
+
for (const [key, count] of counts) {
|
|
1292
|
+
if (count > modeCount) {
|
|
1293
|
+
mode = key;
|
|
1294
|
+
modeCount = count;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
...base,
|
|
1299
|
+
kind: "categorical",
|
|
1300
|
+
mode,
|
|
1301
|
+
modeCount,
|
|
1302
|
+
uniqueCount: counts.size
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return { ...base, kind: "other" };
|
|
1306
|
+
}
|
|
1307
|
+
function isNumericType(type) {
|
|
1308
|
+
return [
|
|
1309
|
+
"number",
|
|
1310
|
+
"slider",
|
|
1311
|
+
"rating",
|
|
1312
|
+
"nps",
|
|
1313
|
+
"opinion_scale",
|
|
1314
|
+
"pain_scale"
|
|
1315
|
+
].includes(type);
|
|
1316
|
+
}
|
|
1317
|
+
function isCategoricalType(type) {
|
|
1318
|
+
return [
|
|
1319
|
+
"single_select",
|
|
1320
|
+
"dropdown",
|
|
1321
|
+
"country_select",
|
|
1322
|
+
"boolean",
|
|
1323
|
+
"consent",
|
|
1324
|
+
"multi_select",
|
|
1325
|
+
"checkbox_group"
|
|
1326
|
+
].includes(type);
|
|
1327
|
+
}
|
|
1328
|
+
function getStatsQuestions(schema) {
|
|
1329
|
+
const questions = [];
|
|
1330
|
+
for (const section of schema.sections) {
|
|
1331
|
+
for (const q of section.questions) {
|
|
1332
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
questions.push(q);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return questions;
|
|
1339
|
+
}
|
|
1340
|
+
function StatsPanel({ schema, responses, onClose }) {
|
|
1341
|
+
const stats = react.useMemo(() => computeStats(responses, schema), [responses, schema]);
|
|
1342
|
+
if (stats.totalCount === 0) {
|
|
1343
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: "No responses to compute statistics." });
|
|
1344
|
+
}
|
|
1345
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
1346
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
1347
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Statistics" }),
|
|
1348
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
1349
|
+
] }),
|
|
1350
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
1351
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-3", children: [
|
|
1352
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1353
|
+
StatCard,
|
|
1354
|
+
{
|
|
1355
|
+
label: "Total Responses",
|
|
1356
|
+
value: String(stats.totalCount)
|
|
1357
|
+
}
|
|
1358
|
+
),
|
|
1359
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1360
|
+
StatCard,
|
|
1361
|
+
{
|
|
1362
|
+
label: "Completion Rate",
|
|
1363
|
+
value: `${Math.round(stats.completionRate * 100)}%`
|
|
1364
|
+
}
|
|
1365
|
+
),
|
|
1366
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1367
|
+
StatCard,
|
|
1368
|
+
{
|
|
1369
|
+
label: "Avg. Completion Time",
|
|
1370
|
+
value: formatTime(stats.avgCompletionTimeMs)
|
|
1371
|
+
}
|
|
1372
|
+
),
|
|
1373
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1374
|
+
StatCard,
|
|
1375
|
+
{
|
|
1376
|
+
label: "Date Range",
|
|
1377
|
+
value: formatDateRange(stats.dateRange)
|
|
1378
|
+
}
|
|
1379
|
+
)
|
|
1380
|
+
] }),
|
|
1381
|
+
stats.fieldSummaries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2", children: stats.fieldSummaries.slice(0, 6).map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1382
|
+
"div",
|
|
1383
|
+
{
|
|
1384
|
+
className: "px-2.5 py-2 rounded border border-border bg-background text-xs",
|
|
1385
|
+
children: [
|
|
1386
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-muted-foreground truncate mb-1", children: f.label }),
|
|
1387
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
1388
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-foreground font-medium", children: [
|
|
1389
|
+
Math.round(f.filledRate * 100),
|
|
1390
|
+
"% filled"
|
|
1391
|
+
] }),
|
|
1392
|
+
f.kind === "numeric" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
1393
|
+
"avg ",
|
|
1394
|
+
f.mean.toFixed(1),
|
|
1395
|
+
" (",
|
|
1396
|
+
f.min,
|
|
1397
|
+
"\u2013",
|
|
1398
|
+
f.max,
|
|
1399
|
+
")"
|
|
1400
|
+
] }),
|
|
1401
|
+
f.kind === "categorical" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground truncate", children: [
|
|
1402
|
+
"top: ",
|
|
1403
|
+
f.mode,
|
|
1404
|
+
" (",
|
|
1405
|
+
f.modeCount,
|
|
1406
|
+
")"
|
|
1407
|
+
] })
|
|
1408
|
+
] })
|
|
1409
|
+
]
|
|
1410
|
+
},
|
|
1411
|
+
f.fieldId
|
|
1412
|
+
)) })
|
|
1413
|
+
] })
|
|
1414
|
+
] });
|
|
1415
|
+
}
|
|
1416
|
+
function StatCard({ label, value }) {
|
|
1417
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2 rounded border border-border bg-background", children: [
|
|
1418
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[11px] text-muted-foreground mb-0.5", children: label }),
|
|
1419
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: value })
|
|
1420
|
+
] });
|
|
1421
|
+
}
|
|
1422
|
+
function formatTime(ms) {
|
|
1423
|
+
if (ms == null) return "\u2014";
|
|
1424
|
+
const seconds = Math.round(ms / 1e3);
|
|
1425
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1426
|
+
const minutes = Math.floor(seconds / 60);
|
|
1427
|
+
const remainingSeconds = seconds % 60;
|
|
1428
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
1429
|
+
}
|
|
1430
|
+
function formatDateRange(range) {
|
|
1431
|
+
if (!range) return "\u2014";
|
|
1432
|
+
const fmt = (iso) => new Date(iso).toLocaleDateString();
|
|
1433
|
+
return `${fmt(range.earliest)} \u2013 ${fmt(range.latest)}`;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/response-viewer/chart-utils.ts
|
|
1437
|
+
function computeFieldDistribution(responses, fieldId) {
|
|
1438
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1439
|
+
let total = 0;
|
|
1440
|
+
for (const r of responses) {
|
|
1441
|
+
const raw = r.values[fieldId];
|
|
1442
|
+
if (raw == null || raw === "") continue;
|
|
1443
|
+
total++;
|
|
1444
|
+
if (typeof raw === "boolean") {
|
|
1445
|
+
const key = raw ? "Yes" : "No";
|
|
1446
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1447
|
+
} else if (Array.isArray(raw)) {
|
|
1448
|
+
for (const item of raw) {
|
|
1449
|
+
const key = String(item);
|
|
1450
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1451
|
+
}
|
|
1452
|
+
} else {
|
|
1453
|
+
const key = String(raw);
|
|
1454
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (total === 0) return [];
|
|
1458
|
+
const entries = [];
|
|
1459
|
+
for (const [value, count] of counts) {
|
|
1460
|
+
entries.push({
|
|
1461
|
+
value,
|
|
1462
|
+
count,
|
|
1463
|
+
percentage: count / total * 100
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
entries.sort((a, b) => b.count - a.count);
|
|
1467
|
+
return entries;
|
|
1468
|
+
}
|
|
1469
|
+
function getChartableFieldIds(schema) {
|
|
1470
|
+
const chartable = [];
|
|
1471
|
+
const chartTypes = /* @__PURE__ */ new Set([
|
|
1472
|
+
"single_select",
|
|
1473
|
+
"dropdown",
|
|
1474
|
+
"country_select",
|
|
1475
|
+
"boolean",
|
|
1476
|
+
"consent",
|
|
1477
|
+
"multi_select",
|
|
1478
|
+
"checkbox_group",
|
|
1479
|
+
"rating",
|
|
1480
|
+
"nps",
|
|
1481
|
+
"opinion_scale",
|
|
1482
|
+
"pain_scale"
|
|
1483
|
+
]);
|
|
1484
|
+
for (const section of schema.sections) {
|
|
1485
|
+
for (const q of section.questions) {
|
|
1486
|
+
if (chartTypes.has(q.type)) {
|
|
1487
|
+
chartable.push({ id: q.id, label: q.label, type: q.type });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return chartable;
|
|
1492
|
+
}
|
|
1493
|
+
function ChartPanel({ schema, responses, onClose }) {
|
|
1494
|
+
const chartableFields = react.useMemo(() => getChartableFieldIds(schema), [schema]);
|
|
1495
|
+
const [selectedFieldId, setSelectedFieldId] = react.useState(chartableFields[0]?.id ?? "");
|
|
1496
|
+
const distribution = react.useMemo(() => {
|
|
1497
|
+
if (!selectedFieldId) return [];
|
|
1498
|
+
return computeFieldDistribution(responses, selectedFieldId);
|
|
1499
|
+
}, [responses, selectedFieldId]);
|
|
1500
|
+
const selectedField = chartableFields.find((f) => f.id === selectedFieldId);
|
|
1501
|
+
const maxCount = distribution.length > 0 ? Math.max(...distribution.map((d) => d.count)) : 1;
|
|
1502
|
+
if (chartableFields.length === 0) {
|
|
1503
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-6 border-b border-border bg-muted/30 text-center", children: [
|
|
1504
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "No chartable fields found in this schema." }),
|
|
1505
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "Charts work with select, dropdown, boolean, rating, and similar field types." })
|
|
1506
|
+
] });
|
|
1507
|
+
}
|
|
1508
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
1509
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
1510
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1511
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Field Distribution" }),
|
|
1512
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1513
|
+
"select",
|
|
1514
|
+
{
|
|
1515
|
+
value: selectedFieldId,
|
|
1516
|
+
onChange: (e) => setSelectedFieldId(e.target.value),
|
|
1517
|
+
className: "text-xs bg-background border border-border rounded px-2 py-1 text-foreground",
|
|
1518
|
+
children: chartableFields.map((f) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: f.id, children: f.label }, f.id))
|
|
1519
|
+
}
|
|
1520
|
+
)
|
|
1521
|
+
] }),
|
|
1522
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
1523
|
+
] }),
|
|
1524
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
1525
|
+
distribution.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground text-center py-4", children: "No data for this field." }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
1526
|
+
distribution.slice(0, 10).map((entry) => /* @__PURE__ */ jsxRuntime.jsx(BarRow, { entry, maxCount }, entry.value)),
|
|
1527
|
+
distribution.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground mt-2", children: [
|
|
1528
|
+
"+",
|
|
1529
|
+
distribution.length - 10,
|
|
1530
|
+
" more values"
|
|
1531
|
+
] })
|
|
1532
|
+
] }),
|
|
1533
|
+
selectedField && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 text-[11px] text-muted-foreground", children: [
|
|
1534
|
+
responses.length,
|
|
1535
|
+
" response",
|
|
1536
|
+
responses.length !== 1 ? "s" : "",
|
|
1537
|
+
" \xB7 ",
|
|
1538
|
+
distribution.length,
|
|
1539
|
+
" unique value",
|
|
1540
|
+
distribution.length !== 1 ? "s" : ""
|
|
1541
|
+
] })
|
|
1542
|
+
] })
|
|
1543
|
+
] });
|
|
1544
|
+
}
|
|
1545
|
+
function BarRow({ entry, maxCount }) {
|
|
1546
|
+
const widthPct = maxCount > 0 ? entry.count / maxCount * 100 : 0;
|
|
1547
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1548
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24 shrink-0 text-xs text-foreground truncate", title: entry.value, children: entry.value }),
|
|
1549
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 h-5 bg-background rounded overflow-hidden border border-border/50", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1550
|
+
"div",
|
|
1551
|
+
{
|
|
1552
|
+
className: "h-full bg-primary/60 rounded transition-all",
|
|
1553
|
+
style: { width: `${widthPct}%` }
|
|
1554
|
+
}
|
|
1555
|
+
) }),
|
|
1556
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-16 shrink-0 text-xs text-muted-foreground text-right", children: [
|
|
1557
|
+
entry.count,
|
|
1558
|
+
" (",
|
|
1559
|
+
Math.round(entry.percentage),
|
|
1560
|
+
"%)"
|
|
1561
|
+
] })
|
|
1562
|
+
] });
|
|
1563
|
+
}
|
|
1564
|
+
function ConfirmDialog({
|
|
1565
|
+
title,
|
|
1566
|
+
message,
|
|
1567
|
+
confirmLabel = "Confirm",
|
|
1568
|
+
cancelLabel = "Cancel",
|
|
1569
|
+
variant = "default",
|
|
1570
|
+
onConfirm,
|
|
1571
|
+
onCancel
|
|
1572
|
+
}) {
|
|
1573
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1574
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1575
|
+
"div",
|
|
1576
|
+
{
|
|
1577
|
+
className: "absolute inset-0 bg-black/50",
|
|
1578
|
+
onClick: onCancel
|
|
1579
|
+
}
|
|
1580
|
+
),
|
|
1581
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative bg-background border border-border rounded-lg shadow-lg p-6 max-w-sm w-full mx-4", children: [
|
|
1582
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-foreground mb-2", children: title }),
|
|
1583
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mb-4", children: message }),
|
|
1584
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
1585
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onCancel, children: cancelLabel }),
|
|
1586
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1587
|
+
fieldcraftReact.Button,
|
|
1588
|
+
{
|
|
1589
|
+
variant: variant === "destructive" ? "destructive" : "default",
|
|
1590
|
+
size: "sm",
|
|
1591
|
+
onClick: onConfirm,
|
|
1592
|
+
children: confirmLabel
|
|
1593
|
+
}
|
|
1594
|
+
)
|
|
1595
|
+
] })
|
|
1596
|
+
] })
|
|
1597
|
+
] });
|
|
1598
|
+
}
|
|
1058
1599
|
|
|
1059
1600
|
// src/response-viewer/export-utils.ts
|
|
1060
1601
|
function buildCsvContent(schema, responses, options = {}) {
|
|
@@ -1189,6 +1730,84 @@ function paginate(items, currentPage, pageSize) {
|
|
|
1189
1730
|
};
|
|
1190
1731
|
}
|
|
1191
1732
|
|
|
1733
|
+
// src/response-viewer/search-utils.ts
|
|
1734
|
+
function searchResponses(responses, query, schema) {
|
|
1735
|
+
if (!query.trim()) return responses;
|
|
1736
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
1737
|
+
const questions = getSearchableQuestions(schema);
|
|
1738
|
+
return responses.filter((response) => {
|
|
1739
|
+
if (new Date(response.submittedAt).toLocaleString().toLowerCase().includes(lowerQuery)) {
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
if (response.sessionToken?.toLowerCase().includes(lowerQuery)) {
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
for (const q of questions) {
|
|
1746
|
+
const value = response.values[q.id];
|
|
1747
|
+
if (value == null) continue;
|
|
1748
|
+
const stringified = stringifyValue(value, q.type);
|
|
1749
|
+
if (stringified.toLowerCase().includes(lowerQuery)) {
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (response.totalScore != null && String(response.totalScore).includes(lowerQuery)) {
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
return false;
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
function getSearchableQuestions(schema) {
|
|
1760
|
+
const questions = [];
|
|
1761
|
+
for (const section of schema.sections) {
|
|
1762
|
+
for (const q of section.questions) {
|
|
1763
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
questions.push(q);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return questions;
|
|
1770
|
+
}
|
|
1771
|
+
function stringifyValue(value, type) {
|
|
1772
|
+
if (value == null) return "";
|
|
1773
|
+
switch (type) {
|
|
1774
|
+
case "vitals_entry": {
|
|
1775
|
+
if (typeof value !== "object" || value === null) break;
|
|
1776
|
+
const v = value;
|
|
1777
|
+
const parts = [];
|
|
1778
|
+
if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
|
|
1779
|
+
if (v.heartRate) parts.push(`HR ${v.heartRate}`);
|
|
1780
|
+
if (v.temperature) parts.push(`${v.temperature}`);
|
|
1781
|
+
if (v.oxygenSaturation) parts.push(`SpO2 ${v.oxygenSaturation}`);
|
|
1782
|
+
return parts.join(" ");
|
|
1783
|
+
}
|
|
1784
|
+
case "medication_list":
|
|
1785
|
+
if (Array.isArray(value)) {
|
|
1786
|
+
return value.map((m) => [m.name, m.dosage, m.frequency].filter(Boolean).join(" ")).join(" ");
|
|
1787
|
+
}
|
|
1788
|
+
break;
|
|
1789
|
+
case "allergy_list":
|
|
1790
|
+
if (Array.isArray(value)) {
|
|
1791
|
+
return value.map((a) => [a.allergen, a.severity].filter(Boolean).join(" ")).join(" ");
|
|
1792
|
+
}
|
|
1793
|
+
break;
|
|
1794
|
+
case "legal_name": {
|
|
1795
|
+
if (typeof value !== "object" || value === null) break;
|
|
1796
|
+
const n = value;
|
|
1797
|
+
return [n.first, n.middle, n.last].filter(Boolean).join(" ");
|
|
1798
|
+
}
|
|
1799
|
+
case "address": {
|
|
1800
|
+
if (typeof value !== "object" || value === null) break;
|
|
1801
|
+
const a = value;
|
|
1802
|
+
return [a.street, a.city, a.state, a.zip].filter(Boolean).join(" ");
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
1806
|
+
if (Array.isArray(value)) return value.map(String).join(" ");
|
|
1807
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1808
|
+
return String(value);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1192
1811
|
// src/response-viewer/filter-utils.ts
|
|
1193
1812
|
function createEmptyFilterState() {
|
|
1194
1813
|
return { filters: [], dateRange: {} };
|
|
@@ -1290,7 +1909,11 @@ function ResponseViewerInner({
|
|
|
1290
1909
|
dateFormat,
|
|
1291
1910
|
columnLabels,
|
|
1292
1911
|
onExport,
|
|
1293
|
-
pageSize = 25
|
|
1912
|
+
pageSize = 25,
|
|
1913
|
+
onDelete,
|
|
1914
|
+
onBulkDelete,
|
|
1915
|
+
onBulkExport,
|
|
1916
|
+
selectable = false
|
|
1294
1917
|
}) {
|
|
1295
1918
|
const [viewMode, setViewMode] = react.useState("table");
|
|
1296
1919
|
const [selectedResponse, setSelectedResponse] = react.useState(null);
|
|
@@ -1298,6 +1921,11 @@ function ResponseViewerInner({
|
|
|
1298
1921
|
const [effectivePageSize, setEffectivePageSize] = react.useState(pageSize);
|
|
1299
1922
|
const [filterState, setFilterState] = react.useState(createEmptyFilterState);
|
|
1300
1923
|
const [showFilterPanel, setShowFilterPanel] = react.useState(false);
|
|
1924
|
+
const [searchQuery, setSearchQuery] = react.useState("");
|
|
1925
|
+
const [showStats, setShowStats] = react.useState(false);
|
|
1926
|
+
const [showCharts, setShowCharts] = react.useState(false);
|
|
1927
|
+
const [selectedIds, setSelectedIds] = react.useState(/* @__PURE__ */ new Set());
|
|
1928
|
+
const [confirmDialog, setConfirmDialog] = react.useState(null);
|
|
1301
1929
|
const [draftFieldId, setDraftFieldId] = react.useState("");
|
|
1302
1930
|
const [draftOperator, setDraftOperator] = react.useState("contains");
|
|
1303
1931
|
const [draftValue, setDraftValue] = react.useState("");
|
|
@@ -1308,9 +1936,18 @@ function ResponseViewerInner({
|
|
|
1308
1936
|
setEffectivePageSize(pageSize);
|
|
1309
1937
|
setCurrentPage(1);
|
|
1310
1938
|
}, [pageSize]);
|
|
1939
|
+
react.useEffect(() => {
|
|
1940
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
1941
|
+
}, [responses]);
|
|
1942
|
+
const searchedResponses = react.useMemo(
|
|
1943
|
+
() => searchResponses(responses, searchQuery, schema),
|
|
1944
|
+
[responses, searchQuery, schema]
|
|
1945
|
+
);
|
|
1311
1946
|
const isFiltered = hasActiveFilters(filterState);
|
|
1312
|
-
const filteredResponses = applyFilters(
|
|
1947
|
+
const filteredResponses = applyFilters(searchedResponses, filterState);
|
|
1313
1948
|
const pag = paginate(filteredResponses, currentPage, effectivePageSize);
|
|
1949
|
+
const questions = getAllQuestions2(schema);
|
|
1950
|
+
const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
|
|
1314
1951
|
function handleSelect(response) {
|
|
1315
1952
|
setSelectedResponse(response);
|
|
1316
1953
|
onResponseSelect?.(response);
|
|
@@ -1357,7 +1994,59 @@ function ResponseViewerInner({
|
|
|
1357
1994
|
setFilterState(createEmptyFilterState());
|
|
1358
1995
|
setCurrentPage(1);
|
|
1359
1996
|
}
|
|
1360
|
-
|
|
1997
|
+
function handleToggleSelect(token) {
|
|
1998
|
+
setSelectedIds((prev) => {
|
|
1999
|
+
const next = new Set(prev);
|
|
2000
|
+
if (next.has(token)) {
|
|
2001
|
+
next.delete(token);
|
|
2002
|
+
} else {
|
|
2003
|
+
next.add(token);
|
|
2004
|
+
}
|
|
2005
|
+
return next;
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
function handleSelectAll() {
|
|
2009
|
+
const currentTokens = pag.pageItems.map((r) => r.sessionToken).filter(Boolean);
|
|
2010
|
+
const allSelected = currentTokens.every((t) => selectedIds.has(t));
|
|
2011
|
+
setSelectedIds((prev) => {
|
|
2012
|
+
const next = new Set(prev);
|
|
2013
|
+
if (allSelected) {
|
|
2014
|
+
currentTokens.forEach((t) => next.delete(t));
|
|
2015
|
+
} else {
|
|
2016
|
+
currentTokens.forEach((t) => next.add(t));
|
|
2017
|
+
}
|
|
2018
|
+
return next;
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
function handleBulkDelete() {
|
|
2022
|
+
if (!onBulkDelete || selectedIds.size === 0) return;
|
|
2023
|
+
setConfirmDialog({
|
|
2024
|
+
title: "Delete selected responses",
|
|
2025
|
+
message: `Are you sure you want to delete ${selectedIds.size} response${selectedIds.size !== 1 ? "s" : ""}? This cannot be undone.`,
|
|
2026
|
+
onConfirm: () => {
|
|
2027
|
+
onBulkDelete(Array.from(selectedIds));
|
|
2028
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
2029
|
+
setConfirmDialog(null);
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
function handleBulkExport() {
|
|
2034
|
+
if (!onBulkExport || selectedIds.size === 0) return;
|
|
2035
|
+
const selected = responses.filter((r) => r.sessionToken && selectedIds.has(r.sessionToken));
|
|
2036
|
+
onBulkExport(selected);
|
|
2037
|
+
}
|
|
2038
|
+
function handleDeleteSingle(token) {
|
|
2039
|
+
if (!onDelete) return;
|
|
2040
|
+
setConfirmDialog({
|
|
2041
|
+
title: "Delete response",
|
|
2042
|
+
message: "Are you sure you want to delete this response? This cannot be undone.",
|
|
2043
|
+
onConfirm: () => {
|
|
2044
|
+
onDelete(token);
|
|
2045
|
+
setSelectedResponse(null);
|
|
2046
|
+
setConfirmDialog(null);
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
1361
2050
|
function getFields(response) {
|
|
1362
2051
|
return questions.map((q) => ({
|
|
1363
2052
|
questionId: q.id,
|
|
@@ -1368,6 +2057,19 @@ function ResponseViewerInner({
|
|
|
1368
2057
|
}
|
|
1369
2058
|
const heightValue = typeof height === "number" ? `${height}px` : height;
|
|
1370
2059
|
const widthValue = typeof width === "number" ? `${width}px` : width;
|
|
2060
|
+
const countText = (() => {
|
|
2061
|
+
const hasSearch = searchQuery.trim().length > 0;
|
|
2062
|
+
if (hasSearch && isFiltered) {
|
|
2063
|
+
return `${filteredResponses.length} of ${responses.length} responses (searched + filtered)`;
|
|
2064
|
+
}
|
|
2065
|
+
if (hasSearch) {
|
|
2066
|
+
return `${searchedResponses.length} result${searchedResponses.length !== 1 ? "s" : ""} for "${searchQuery}"`;
|
|
2067
|
+
}
|
|
2068
|
+
if (isFiltered) {
|
|
2069
|
+
return `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)`;
|
|
2070
|
+
}
|
|
2071
|
+
return `${responses.length} response${responses.length !== 1 ? "s" : ""}`;
|
|
2072
|
+
})();
|
|
1371
2073
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1372
2074
|
"div",
|
|
1373
2075
|
{
|
|
@@ -1375,7 +2077,20 @@ function ResponseViewerInner({
|
|
|
1375
2077
|
style: { height: heightValue, width: widthValue },
|
|
1376
2078
|
children: [
|
|
1377
2079
|
!selectedResponse && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-border", children: [
|
|
1378
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2080
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2081
|
+
fieldcraftReact.Input,
|
|
2082
|
+
{
|
|
2083
|
+
type: "text",
|
|
2084
|
+
placeholder: "Search responses...",
|
|
2085
|
+
className: "h-7 text-xs w-40",
|
|
2086
|
+
value: searchQuery,
|
|
2087
|
+
onChange: (e) => {
|
|
2088
|
+
setSearchQuery(e.target.value);
|
|
2089
|
+
setCurrentPage(1);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
),
|
|
2093
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: countText }),
|
|
1379
2094
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1380
2095
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1381
2096
|
fieldcraftReact.Button,
|
|
@@ -1402,6 +2117,48 @@ function ResponseViewerInner({
|
|
|
1402
2117
|
onClick: () => setViewMode("card"),
|
|
1403
2118
|
children: "Cards"
|
|
1404
2119
|
}
|
|
2120
|
+
),
|
|
2121
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2122
|
+
fieldcraftReact.Button,
|
|
2123
|
+
{
|
|
2124
|
+
variant: viewMode === "timeline" ? "secondary" : "ghost",
|
|
2125
|
+
size: "sm",
|
|
2126
|
+
className: cn(
|
|
2127
|
+
"h-7 text-xs",
|
|
2128
|
+
viewMode === "timeline" && "font-semibold border border-ring"
|
|
2129
|
+
),
|
|
2130
|
+
onClick: () => setViewMode("timeline"),
|
|
2131
|
+
children: "Timeline"
|
|
2132
|
+
}
|
|
2133
|
+
)
|
|
2134
|
+
] }),
|
|
2135
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
2136
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
2137
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2138
|
+
fieldcraftReact.Button,
|
|
2139
|
+
{
|
|
2140
|
+
variant: showStats ? "secondary" : "ghost",
|
|
2141
|
+
size: "sm",
|
|
2142
|
+
className: cn(
|
|
2143
|
+
"h-7 text-xs",
|
|
2144
|
+
showStats && "font-semibold border border-ring"
|
|
2145
|
+
),
|
|
2146
|
+
onClick: () => setShowStats((v) => !v),
|
|
2147
|
+
children: "Stats"
|
|
2148
|
+
}
|
|
2149
|
+
),
|
|
2150
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2151
|
+
fieldcraftReact.Button,
|
|
2152
|
+
{
|
|
2153
|
+
variant: showCharts ? "secondary" : "ghost",
|
|
2154
|
+
size: "sm",
|
|
2155
|
+
className: cn(
|
|
2156
|
+
"h-7 text-xs",
|
|
2157
|
+
showCharts && "font-semibold border border-ring"
|
|
2158
|
+
),
|
|
2159
|
+
onClick: () => setShowCharts((v) => !v),
|
|
2160
|
+
children: "Charts"
|
|
2161
|
+
}
|
|
1405
2162
|
)
|
|
1406
2163
|
] }),
|
|
1407
2164
|
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
@@ -1467,6 +2224,26 @@ function ResponseViewerInner({
|
|
|
1467
2224
|
)
|
|
1468
2225
|
] })
|
|
1469
2226
|
] }),
|
|
2227
|
+
!selectedResponse && isSelectable && selectedIds.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border bg-accent/50", children: [
|
|
2228
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-foreground", children: [
|
|
2229
|
+
selectedIds.size,
|
|
2230
|
+
" selected"
|
|
2231
|
+
] }),
|
|
2232
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
|
|
2233
|
+
onBulkExport && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", className: "h-6 text-xs", onClick: handleBulkExport, children: "Export Selected" }),
|
|
2234
|
+
onBulkDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", className: "h-6 text-xs", onClick: handleBulkDelete, children: "Delete Selected" }),
|
|
2235
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2236
|
+
fieldcraftReact.Button,
|
|
2237
|
+
{
|
|
2238
|
+
variant: "ghost",
|
|
2239
|
+
size: "sm",
|
|
2240
|
+
className: "h-6 text-xs",
|
|
2241
|
+
onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
|
|
2242
|
+
children: "Clear"
|
|
2243
|
+
}
|
|
2244
|
+
)
|
|
2245
|
+
] })
|
|
2246
|
+
] }),
|
|
1470
2247
|
!selectedResponse && showFilterPanel && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
|
|
1471
2248
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
|
|
1472
2249
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
|
|
@@ -1595,19 +2372,47 @@ function ResponseViewerInner({
|
|
|
1595
2372
|
);
|
|
1596
2373
|
})
|
|
1597
2374
|
] }),
|
|
1598
|
-
|
|
2375
|
+
!selectedResponse && showStats && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2376
|
+
StatsPanel,
|
|
2377
|
+
{
|
|
2378
|
+
schema,
|
|
2379
|
+
responses: filteredResponses,
|
|
2380
|
+
onClose: () => setShowStats(false)
|
|
2381
|
+
}
|
|
2382
|
+
),
|
|
2383
|
+
!selectedResponse && showCharts && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2384
|
+
ChartPanel,
|
|
2385
|
+
{
|
|
2386
|
+
schema,
|
|
2387
|
+
responses: filteredResponses,
|
|
2388
|
+
onClose: () => setShowCharts(false)
|
|
2389
|
+
}
|
|
2390
|
+
),
|
|
2391
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1599
2392
|
ResponseDetail,
|
|
1600
2393
|
{
|
|
1601
2394
|
response: selectedResponse,
|
|
1602
2395
|
fields: getFields(selectedResponse),
|
|
1603
|
-
onBack: handleBack
|
|
2396
|
+
onBack: handleBack,
|
|
2397
|
+
onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
|
|
2398
|
+
}
|
|
2399
|
+
) }) : viewMode === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2400
|
+
TimelineView,
|
|
2401
|
+
{
|
|
2402
|
+
responses: filteredResponses,
|
|
2403
|
+
questions,
|
|
2404
|
+
onSelect: handleSelect
|
|
1604
2405
|
}
|
|
1605
2406
|
) : viewMode === "table" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1606
2407
|
ResponseTable,
|
|
1607
2408
|
{
|
|
1608
2409
|
schema,
|
|
1609
2410
|
responses: pag.pageItems,
|
|
1610
|
-
onRowClick: handleSelect
|
|
2411
|
+
onRowClick: handleSelect,
|
|
2412
|
+
selectable: isSelectable,
|
|
2413
|
+
selectedIds,
|
|
2414
|
+
onToggleSelect: handleToggleSelect,
|
|
2415
|
+
onSelectAll: handleSelectAll
|
|
1611
2416
|
}
|
|
1612
2417
|
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
|
|
1613
2418
|
pag.pageItems.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1615,13 +2420,16 @@ function ResponseViewerInner({
|
|
|
1615
2420
|
{
|
|
1616
2421
|
response,
|
|
1617
2422
|
fields: getFields(response),
|
|
1618
|
-
onClick: () => handleSelect(response)
|
|
2423
|
+
onClick: () => handleSelect(response),
|
|
2424
|
+
selectable: isSelectable,
|
|
2425
|
+
selected: !!(response.sessionToken && selectedIds.has(response.sessionToken)),
|
|
2426
|
+
onToggleSelect: response.sessionToken ? () => handleToggleSelect(response.sessionToken) : void 0
|
|
1619
2427
|
},
|
|
1620
2428
|
response.sessionToken || idx
|
|
1621
2429
|
)),
|
|
1622
|
-
filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
|
|
2430
|
+
filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered || searchQuery.trim() ? "No matching responses" : "No responses yet" })
|
|
1623
2431
|
] }) }),
|
|
1624
|
-
!selectedResponse && pag.showPagination && /* @__PURE__ */ jsxRuntime.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: [
|
|
2432
|
+
!selectedResponse && viewMode !== "timeline" && pag.showPagination && /* @__PURE__ */ jsxRuntime.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: [
|
|
1625
2433
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
|
|
1626
2434
|
"Showing ",
|
|
1627
2435
|
pag.startItem,
|
|
@@ -1675,7 +2483,18 @@ function ResponseViewerInner({
|
|
|
1675
2483
|
}
|
|
1676
2484
|
)
|
|
1677
2485
|
] })
|
|
1678
|
-
] })
|
|
2486
|
+
] }),
|
|
2487
|
+
confirmDialog && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2488
|
+
ConfirmDialog,
|
|
2489
|
+
{
|
|
2490
|
+
title: confirmDialog.title,
|
|
2491
|
+
message: confirmDialog.message,
|
|
2492
|
+
confirmLabel: "Delete",
|
|
2493
|
+
variant: "destructive",
|
|
2494
|
+
onConfirm: confirmDialog.onConfirm,
|
|
2495
|
+
onCancel: () => setConfirmDialog(null)
|
|
2496
|
+
}
|
|
2497
|
+
)
|
|
1679
2498
|
]
|
|
1680
2499
|
}
|
|
1681
2500
|
);
|