@squaredr/fieldcraft-pro 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE.txt +51 -51
  2. package/README.md +139 -139
  3. package/dist/{chunk-VEP3DOCQ.mjs → chunk-5QXKDYWY.mjs} +103 -19
  4. package/dist/{chunk-GZIV2XAD.mjs → chunk-7GPX3YIB.mjs} +146 -183
  5. package/dist/{chunk-J5V6OVZO.mjs → chunk-DWRLPGX5.mjs} +690 -211
  6. package/dist/{chunk-4TN6YJDF.mjs → chunk-KTIU7VCP.mjs} +971 -214
  7. package/dist/default-schema-BGxf9Jrg.d.ts +174 -0
  8. package/dist/default-schema-Bwq8FATF.d.mts +174 -0
  9. package/dist/form-builder/index.d.mts +14 -170
  10. package/dist/form-builder/index.d.ts +14 -170
  11. package/dist/form-builder/index.js +827 -326
  12. package/dist/form-builder/index.mjs +1 -1
  13. package/dist/index.d.mts +111 -10
  14. package/dist/index.d.ts +111 -10
  15. package/dist/index.js +3525 -971
  16. package/dist/index.mjs +1424 -124
  17. package/dist/response-viewer/index.d.mts +1 -1
  18. package/dist/response-viewer/index.d.ts +1 -1
  19. package/dist/response-viewer/index.js +146 -183
  20. package/dist/response-viewer/index.mjs +1 -1
  21. package/dist/styles.css +2 -2
  22. package/dist/templates/index.d.mts +1 -1
  23. package/dist/templates/index.d.ts +1 -1
  24. package/dist/templates/index.js +103 -19
  25. package/dist/templates/index.mjs +1 -1
  26. package/dist/theme-editor/index.d.mts +12 -7
  27. package/dist/theme-editor/index.d.ts +12 -7
  28. package/dist/theme-editor/index.js +971 -214
  29. package/dist/theme-editor/index.mjs +1 -1
  30. package/dist/{types-bFHsZT1n.d.mts → types-BQOeI3LX.d.mts} +1 -1
  31. package/dist/{types-bFHsZT1n.d.ts → types-BQOeI3LX.d.ts} +1 -1
  32. package/package.json +29 -18
  33. package/theme-editor/styles.css +497 -466
@@ -4,6 +4,119 @@ import { useState, useEffect, useMemo } from 'react';
4
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
+ // src/response-viewer/constants.ts
8
+ var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
9
+ "info-block",
10
+ "info_block",
11
+ "section-header",
12
+ "page-break",
13
+ "image",
14
+ "divider",
15
+ "spacer",
16
+ "video",
17
+ "rich-text",
18
+ "welcome-screen",
19
+ "thank-you-screen",
20
+ "hidden",
21
+ "calculated"
22
+ ]);
23
+ function formatDuration(ms) {
24
+ if (ms == null || ms < 0) return "\u2014";
25
+ const totalSeconds = Math.round(ms / 1e3);
26
+ if (totalSeconds < 60) return `${totalSeconds}s`;
27
+ const minutes = Math.floor(totalSeconds / 60);
28
+ const seconds = totalSeconds % 60;
29
+ return `${minutes}m ${seconds}s`;
30
+ }
31
+ function countAnswered(values) {
32
+ let count = 0;
33
+ for (const v of Object.values(values)) {
34
+ if (v !== void 0 && v !== null && v !== "") count++;
35
+ }
36
+ return count;
37
+ }
38
+ function getTotalFieldCount(schema) {
39
+ let count = 0;
40
+ for (const section of schema.sections) {
41
+ for (const q of section.questions) {
42
+ if (!DISPLAY_ONLY_TYPES.has(q.type)) count++;
43
+ }
44
+ }
45
+ return count;
46
+ }
47
+ function ResponseTable({
48
+ schema,
49
+ responses,
50
+ onRowClick,
51
+ selectable,
52
+ selectedIds,
53
+ onToggleSelect,
54
+ onSelectAll
55
+ }) {
56
+ const totalFields = getTotalFieldCount(schema);
57
+ const allPageSelected = selectable && responses.length > 0 && responses.every(
58
+ (r) => r.sessionToken && selectedIds?.has(r.sessionToken)
59
+ );
60
+ const colCount = 4 + (selectable ? 1 : 0);
61
+ return /* @__PURE__ */ jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
62
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-muted", children: [
63
+ selectable && /* @__PURE__ */ jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsx(
64
+ "input",
65
+ {
66
+ type: "checkbox",
67
+ checked: allPageSelected,
68
+ onChange: () => onSelectAll?.(),
69
+ className: "accent-primary"
70
+ }
71
+ ) }),
72
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
73
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Completion Time" }),
74
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Fields Answered" }),
75
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Score" })
76
+ ] }) }),
77
+ /* @__PURE__ */ jsxs("tbody", { children: [
78
+ responses.map((response, idx) => {
79
+ const answered = countAnswered(response.values);
80
+ return /* @__PURE__ */ jsxs(
81
+ "tr",
82
+ {
83
+ onClick: () => onRowClick?.(response),
84
+ className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
85
+ children: [
86
+ selectable && /* @__PURE__ */ jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsx(
87
+ "input",
88
+ {
89
+ type: "checkbox",
90
+ checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
91
+ onChange: (e) => {
92
+ e.stopPropagation();
93
+ if (response.sessionToken) onToggleSelect?.(response.sessionToken);
94
+ },
95
+ onClick: (e) => e.stopPropagation(),
96
+ className: "accent-primary"
97
+ }
98
+ ) }),
99
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
100
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: formatDuration(response.completionTimeMs) }),
101
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: totalFields > 0 ? `${answered}/${totalFields}` : "\u2014" }),
102
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: response.totalScore ?? "\u2014" })
103
+ ]
104
+ },
105
+ response.sessionToken || idx
106
+ );
107
+ }),
108
+ responses.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
109
+ "td",
110
+ {
111
+ colSpan: colCount,
112
+ className: "px-3 py-8 text-center text-muted-foreground",
113
+ children: "No responses yet"
114
+ }
115
+ ) })
116
+ ] })
117
+ ] }) });
118
+ }
119
+
7
120
  // src/response-viewer/clinical-display-data.ts
8
121
  var BODY_REGION_LABELS = {
9
122
  head: "Head",
@@ -140,166 +253,6 @@ function getScoreSeverity(instrumentKey, score) {
140
253
  if (!thresholds) return void 0;
141
254
  return thresholds.find((t) => score >= t.min && score <= t.max);
142
255
  }
143
- function ResponseTable({
144
- schema,
145
- responses,
146
- onRowClick,
147
- selectable,
148
- selectedIds,
149
- onToggleSelect,
150
- onSelectAll
151
- }) {
152
- const questions = getAllQuestions(schema);
153
- const allPageSelected = selectable && responses.length > 0 && responses.every(
154
- (r) => r.sessionToken && selectedIds?.has(r.sessionToken)
155
- );
156
- return /* @__PURE__ */ jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
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
- ) }),
167
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
168
- questions.map((q) => /* @__PURE__ */ jsx(
169
- "th",
170
- {
171
- className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap",
172
- children: q.label
173
- },
174
- q.id
175
- )),
176
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Score" })
177
- ] }) }),
178
- /* @__PURE__ */ jsxs("tbody", { children: [
179
- responses.map((response, idx) => /* @__PURE__ */ jsxs(
180
- "tr",
181
- {
182
- onClick: () => onRowClick?.(response),
183
- className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
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
- ) }),
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() }),
199
- questions.map((q) => /* @__PURE__ */ jsx(
200
- "td",
201
- {
202
- className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap",
203
- children: formatCellValue(response.values[q.id], q.type)
204
- },
205
- q.id
206
- )),
207
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap", children: response.totalScore ?? "\u2014" })
208
- ]
209
- },
210
- response.sessionToken || idx
211
- )),
212
- responses.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
213
- "td",
214
- {
215
- colSpan: questions.length + 2 + (selectable ? 1 : 0),
216
- className: "px-3 py-8 text-center text-muted-foreground",
217
- children: "No responses yet"
218
- }
219
- ) })
220
- ] })
221
- ] }) });
222
- }
223
- function getAllQuestions(schema) {
224
- const questions = [];
225
- for (const section of schema.sections) {
226
- for (const q of section.questions) {
227
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
228
- continue;
229
- }
230
- questions.push(q);
231
- }
232
- }
233
- return questions;
234
- }
235
- function formatCellValue(value, type) {
236
- if (value == null) return "\u2014";
237
- if (type) {
238
- switch (type) {
239
- case "vitals_entry": {
240
- if (typeof value !== "object" || value === null) break;
241
- const v = value;
242
- const parts = [];
243
- if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
244
- if (v.heartRate) parts.push(`HR ${v.heartRate}`);
245
- if (v.temperature) parts.push(`${v.temperature}\xB0F`);
246
- if (v.oxygenSaturation) parts.push(`SpO\u2082 ${v.oxygenSaturation}%`);
247
- return parts.length > 0 ? parts.join(", ") : "\u2014";
248
- }
249
- case "medication_list":
250
- if (Array.isArray(value)) return `${value.length} medication${value.length !== 1 ? "s" : ""}`;
251
- break;
252
- case "allergy_list":
253
- if (Array.isArray(value)) return `${value.length} allerg${value.length !== 1 ? "ies" : "y"}`;
254
- break;
255
- case "body_diagram":
256
- if (Array.isArray(value)) {
257
- const labels = value.map((id) => BODY_REGION_LABELS[id] ?? id);
258
- return labels.join(", ");
259
- }
260
- break;
261
- case "pain_scale":
262
- if (typeof value === "number") return `${value}/10`;
263
- break;
264
- case "bmi_calculator": {
265
- if (typeof value !== "object" || value === null) break;
266
- const d = value;
267
- if (d.bmi != null) return `BMI ${d.bmi}`;
268
- break;
269
- }
270
- case "payment": {
271
- if (typeof value !== "object" || value === null) break;
272
- const p = value;
273
- const status = p.status;
274
- return status ? status.charAt(0).toUpperCase() + status.slice(1) : "\u2014";
275
- }
276
- case "insurance_card": {
277
- if (typeof value !== "object" || value === null) break;
278
- const ins = value;
279
- const parts = [ins.carrierId, ins.planName].filter(Boolean);
280
- return parts.length > 0 ? parts.join(" \u2014 ") : "Card uploaded";
281
- }
282
- case "legal_name": {
283
- if (typeof value !== "object" || value === null) break;
284
- const n = value;
285
- return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
286
- }
287
- case "address": {
288
- if (typeof value !== "object" || value === null) break;
289
- const a = value;
290
- return [a.city, a.state].filter(Boolean).join(", ") || "\u2014";
291
- }
292
- case "consent":
293
- return value === true || value === "true" || value === "agreed" ? "Agreed" : "Not agreed";
294
- case "signature":
295
- return typeof value === "string" && value.startsWith("data:image") ? "Signed" : "\u2014";
296
- }
297
- }
298
- if (typeof value === "boolean") return value ? "Yes" : "No";
299
- if (Array.isArray(value)) return value.join(", ");
300
- if (typeof value === "object") return JSON.stringify(value);
301
- return String(value);
302
- }
303
256
  function ResponseCard({
304
257
  response,
305
258
  fields,
@@ -783,7 +736,13 @@ function formatFallbackValue(value) {
783
736
  if (value == null) return "\u2014";
784
737
  if (typeof value === "boolean") return value ? "Yes" : "No";
785
738
  if (Array.isArray(value)) return value.map(formatFallbackValue).join(", ");
786
- if (typeof value === "object") return JSON.stringify(value, null, 2);
739
+ if (typeof value === "object") {
740
+ try {
741
+ return JSON.stringify(value, null, 2);
742
+ } catch {
743
+ return "[Object]";
744
+ }
745
+ }
787
746
  return String(value);
788
747
  }
789
748
  function TimelineView({ responses, questions, onSelect }) {
@@ -1301,9 +1260,7 @@ function getExportableQuestions(schema) {
1301
1260
  const questions = [];
1302
1261
  for (const section of schema.sections) {
1303
1262
  for (const q of section.questions) {
1304
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
1305
- continue;
1306
- }
1263
+ if (DISPLAY_ONLY_TYPES.has(q.type)) continue;
1307
1264
  questions.push(q);
1308
1265
  }
1309
1266
  }
@@ -1369,6 +1326,10 @@ function formatExportValue(value, type) {
1369
1326
  return String(value);
1370
1327
  }
1371
1328
  function escapeCsvField(field) {
1329
+ const first = field.charAt(0);
1330
+ if (first === "=" || first === "+" || first === "-" || first === "@" || first === " " || first === "\r") {
1331
+ field = `'${field}`;
1332
+ }
1372
1333
  if (field.includes(",") || field.includes('"') || field.includes("\n")) {
1373
1334
  return `"${field.replace(/"/g, '""')}"`;
1374
1335
  }
@@ -1383,7 +1344,7 @@ function downloadBlob(content, filename, mimeType) {
1383
1344
  document.body.appendChild(link);
1384
1345
  link.click();
1385
1346
  document.body.removeChild(link);
1386
- URL.revokeObjectURL(url);
1347
+ setTimeout(() => URL.revokeObjectURL(url), 100);
1387
1348
  }
1388
1349
 
1389
1350
  // src/response-viewer/pagination-utils.ts
@@ -1503,14 +1464,15 @@ function applyFilters(responses, state) {
1503
1464
  function matchesDateRange(submittedAt, range) {
1504
1465
  if (!range.from && !range.to) return true;
1505
1466
  const submitted = new Date(submittedAt);
1467
+ if (isNaN(submitted.getTime())) return false;
1506
1468
  if (range.from) {
1507
- const from = new Date(range.from);
1508
- from.setHours(0, 0, 0, 0);
1469
+ const from = /* @__PURE__ */ new Date(range.from + "T00:00:00");
1470
+ if (isNaN(from.getTime())) return false;
1509
1471
  if (submitted < from) return false;
1510
1472
  }
1511
1473
  if (range.to) {
1512
- const to = new Date(range.to);
1513
- to.setHours(23, 59, 59, 999);
1474
+ const to = /* @__PURE__ */ new Date(range.to + "T23:59:59.999");
1475
+ if (isNaN(to.getTime())) return false;
1514
1476
  if (submitted > to) return false;
1515
1477
  }
1516
1478
  return true;
@@ -1616,9 +1578,12 @@ function ResponseViewerInner({
1616
1578
  [responses, searchQuery, schema]
1617
1579
  );
1618
1580
  const isFiltered = hasActiveFilters(filterState);
1619
- const filteredResponses = applyFilters(searchedResponses, filterState);
1581
+ const filteredResponses = useMemo(
1582
+ () => applyFilters(searchedResponses, filterState),
1583
+ [searchedResponses, filterState]
1584
+ );
1620
1585
  const pag = paginate(filteredResponses, currentPage, effectivePageSize);
1621
- const questions = getAllQuestions2(schema);
1586
+ const questions = useMemo(() => getAllQuestions(schema), [schema]);
1622
1587
  const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
1623
1588
  function handleSelect(response) {
1624
1589
  setSelectedResponse(response);
@@ -1720,7 +1685,7 @@ function ResponseViewerInner({
1720
1685
  });
1721
1686
  }
1722
1687
  function getFields(response) {
1723
- return questions.map((q) => ({
1688
+ return questions.filter((q) => response.values[q.id] !== void 0).map((q) => ({
1724
1689
  questionId: q.id,
1725
1690
  label: q.label,
1726
1691
  type: q.type,
@@ -1873,10 +1838,10 @@ function ResponseViewerInner({
1873
1838
  className: "h-7 text-xs",
1874
1839
  onClick: () => {
1875
1840
  const csvFilename = filename ? filename.replace(/\.\w+$/, ".csv") : "responses.csv";
1876
- exportToCsv(schema, responses, csvFilename, { dateFormat, columnLabels });
1877
- onExport?.("csv", responses.length);
1841
+ exportToCsv(schema, filteredResponses, csvFilename, { dateFormat, columnLabels });
1842
+ onExport?.("csv", filteredResponses.length);
1878
1843
  },
1879
- disabled: responses.length === 0,
1844
+ disabled: filteredResponses.length === 0,
1880
1845
  children: "Export CSV"
1881
1846
  }
1882
1847
  ),
@@ -1888,10 +1853,10 @@ function ResponseViewerInner({
1888
1853
  className: "h-7 text-xs",
1889
1854
  onClick: () => {
1890
1855
  const jsonFilename = filename ? filename.replace(/\.\w+$/, ".json") : "responses.json";
1891
- exportToJson(responses, jsonFilename);
1892
- onExport?.("json", responses.length);
1856
+ exportToJson(filteredResponses, jsonFilename);
1857
+ onExport?.("json", filteredResponses.length);
1893
1858
  },
1894
- disabled: responses.length === 0,
1859
+ disabled: filteredResponses.length === 0,
1895
1860
  children: "Export JSON"
1896
1861
  }
1897
1862
  )
@@ -2067,7 +2032,7 @@ function ResponseViewerInner({
2067
2032
  response: selectedResponse,
2068
2033
  fields: getFields(selectedResponse),
2069
2034
  onBack: handleBack,
2070
- onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
2035
+ onDelete: onDelete && selectedResponse.sessionToken != null ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
2071
2036
  }
2072
2037
  ) }) : viewMode === "timeline" ? /* @__PURE__ */ jsx(
2073
2038
  TimelineView,
@@ -2172,13 +2137,11 @@ function ResponseViewerInner({
2172
2137
  }
2173
2138
  );
2174
2139
  }
2175
- function getAllQuestions2(schema) {
2140
+ function getAllQuestions(schema) {
2176
2141
  const questions = [];
2177
2142
  for (const section of schema.sections) {
2178
2143
  for (const q of section.questions) {
2179
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
2180
- continue;
2181
- }
2144
+ if (DISPLAY_ONLY_TYPES.has(q.type)) continue;
2182
2145
  questions.push(q);
2183
2146
  }
2184
2147
  }