@squaredr/fieldcraft-pro 1.7.0 → 1.8.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.
@@ -1,3 +1,5 @@
1
+ import { TIMEZONES } from '@squaredr/fieldcraft-core';
2
+
1
3
  // src/templates/consultation-booking.ts
2
4
  var consultationBookingSchema = {
3
5
  id: "consultation-booking",
@@ -364,17 +366,10 @@ var consultationBookingSchema = {
364
366
  type: "dropdown",
365
367
  label: "Your Timezone",
366
368
  required: true,
367
- options: [
368
- { label: "US Eastern (ET)", value: "America/New_York" },
369
- { label: "US Central (CT)", value: "America/Chicago" },
370
- { label: "US Mountain (MT)", value: "America/Denver" },
371
- { label: "US Pacific (PT)", value: "America/Los_Angeles" },
372
- { label: "UK (GMT/BST)", value: "Europe/London" },
373
- { label: "Central Europe (CET)", value: "Europe/Berlin" },
374
- { label: "India (IST)", value: "Asia/Kolkata" },
375
- { label: "Japan (JST)", value: "Asia/Tokyo" },
376
- { label: "Australia Eastern (AEST)", value: "Australia/Sydney" }
377
- ]
369
+ options: TIMEZONES.map((tz) => ({
370
+ label: `${tz.label} (${tz.offset})`,
371
+ value: tz.value
372
+ }))
378
373
  },
379
374
  {
380
375
  id: "appointment_slot",
@@ -405,7 +400,7 @@ var consultationBookingSchema = {
405
400
  times: ["09:00", "11:00", "13:00", "14:00", "16:00"]
406
401
  }
407
402
  ],
408
- timezone: "America/New_York",
403
+ timezoneField: "timezone",
409
404
  duration: 60
410
405
  }
411
406
  },
@@ -501,10 +496,11 @@ var consultationBookingSchema = {
501
496
  type: "payment",
502
497
  provider: "stripe",
503
498
  publicKey: "",
499
+ // Set your Stripe publishable key (pk_test_... or pk_live_...)
504
500
  amount: 2e4,
505
501
  currency: "USD",
506
- description: "Consultation Session",
507
- serverUrl: "/api/payment-intents"
502
+ description: "Consultation Session"
503
+ // No serverUrl — developer must configure their own endpoint via customProps or serverUrl
508
504
  }
509
505
  }
510
506
  ]
@@ -1028,10 +1024,11 @@ var ecommerceCheckoutSchema = {
1028
1024
  type: "payment",
1029
1025
  provider: "stripe",
1030
1026
  publicKey: "",
1027
+ // Set your Stripe publishable key (pk_test_... or pk_live_...)
1031
1028
  amount: 1e4,
1032
1029
  currency: "USD",
1033
- description: "E-commerce Order",
1034
- serverUrl: "/api/payment-intents"
1030
+ description: "E-commerce Order"
1031
+ // No serverUrl — developer must configure their own endpoint via customProps or serverUrl
1035
1032
  }
1036
1033
  }
1037
1034
  ]
@@ -4,6 +4,122 @@ 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
+ function formatDuration(ms) {
8
+ if (ms == null) return "\u2014";
9
+ const totalSeconds = Math.round(ms / 1e3);
10
+ if (totalSeconds < 60) return `${totalSeconds}s`;
11
+ const minutes = Math.floor(totalSeconds / 60);
12
+ const seconds = totalSeconds % 60;
13
+ return `${minutes}m ${seconds}s`;
14
+ }
15
+ function countAnswered(values) {
16
+ let count = 0;
17
+ for (const v of Object.values(values)) {
18
+ if (v !== void 0 && v !== null && v !== "") count++;
19
+ }
20
+ return count;
21
+ }
22
+ function getTotalFieldCount(schema) {
23
+ let count = 0;
24
+ for (const section of schema.sections) {
25
+ for (const q of section.questions) {
26
+ if (!DISPLAY_ONLY_TYPES.has(q.type)) count++;
27
+ }
28
+ }
29
+ return count;
30
+ }
31
+ function ResponseTable({
32
+ schema,
33
+ responses,
34
+ onRowClick,
35
+ selectable,
36
+ selectedIds,
37
+ onToggleSelect,
38
+ onSelectAll
39
+ }) {
40
+ const totalFields = getTotalFieldCount(schema);
41
+ const allPageSelected = selectable && responses.length > 0 && responses.every(
42
+ (r) => r.sessionToken && selectedIds?.has(r.sessionToken)
43
+ );
44
+ const colCount = 4 + (selectable ? 1 : 0);
45
+ return /* @__PURE__ */ jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
46
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-muted", children: [
47
+ selectable && /* @__PURE__ */ jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsx(
48
+ "input",
49
+ {
50
+ type: "checkbox",
51
+ checked: allPageSelected,
52
+ onChange: () => onSelectAll?.(),
53
+ className: "accent-primary"
54
+ }
55
+ ) }),
56
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
57
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Completion Time" }),
58
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Fields Answered" }),
59
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Score" })
60
+ ] }) }),
61
+ /* @__PURE__ */ jsxs("tbody", { children: [
62
+ responses.map((response, idx) => {
63
+ const answered = countAnswered(response.values);
64
+ return /* @__PURE__ */ jsxs(
65
+ "tr",
66
+ {
67
+ onClick: () => onRowClick?.(response),
68
+ className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
69
+ children: [
70
+ selectable && /* @__PURE__ */ jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsx(
71
+ "input",
72
+ {
73
+ type: "checkbox",
74
+ checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
75
+ onChange: (e) => {
76
+ e.stopPropagation();
77
+ if (response.sessionToken) onToggleSelect?.(response.sessionToken);
78
+ },
79
+ onClick: (e) => e.stopPropagation(),
80
+ className: "accent-primary"
81
+ }
82
+ ) }),
83
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
84
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: formatDuration(response.completionTimeMs) }),
85
+ /* @__PURE__ */ jsxs("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: [
86
+ answered,
87
+ "/",
88
+ totalFields
89
+ ] }),
90
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: response.totalScore ?? "\u2014" })
91
+ ]
92
+ },
93
+ response.sessionToken || idx
94
+ );
95
+ }),
96
+ responses.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
97
+ "td",
98
+ {
99
+ colSpan: colCount,
100
+ className: "px-3 py-8 text-center text-muted-foreground",
101
+ children: "No responses yet"
102
+ }
103
+ ) })
104
+ ] })
105
+ ] }) });
106
+ }
107
+ var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
108
+ "info-block",
109
+ "info_block",
110
+ "section-header",
111
+ "page-break",
112
+ "image",
113
+ "divider",
114
+ "spacer",
115
+ "video",
116
+ "rich-text",
117
+ "welcome-screen",
118
+ "thank-you-screen",
119
+ "hidden",
120
+ "calculated"
121
+ ]);
122
+
7
123
  // src/response-viewer/clinical-display-data.ts
8
124
  var BODY_REGION_LABELS = {
9
125
  head: "Head",
@@ -140,166 +256,6 @@ function getScoreSeverity(instrumentKey, score) {
140
256
  if (!thresholds) return void 0;
141
257
  return thresholds.find((t) => score >= t.min && score <= t.max);
142
258
  }
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
259
  function ResponseCard({
304
260
  response,
305
261
  fields,
@@ -1297,13 +1253,26 @@ function exportToJson(responses, filename = "responses.json") {
1297
1253
  const content = JSON.stringify(responses, null, 2);
1298
1254
  downloadBlob(content, filename, "application/json;charset=utf-8;");
1299
1255
  }
1256
+ var DISPLAY_ONLY_TYPES2 = /* @__PURE__ */ new Set([
1257
+ "info-block",
1258
+ "info_block",
1259
+ "section-header",
1260
+ "page-break",
1261
+ "image",
1262
+ "divider",
1263
+ "spacer",
1264
+ "video",
1265
+ "rich-text",
1266
+ "welcome-screen",
1267
+ "thank-you-screen",
1268
+ "hidden",
1269
+ "calculated"
1270
+ ]);
1300
1271
  function getExportableQuestions(schema) {
1301
1272
  const questions = [];
1302
1273
  for (const section of schema.sections) {
1303
1274
  for (const q of section.questions) {
1304
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
1305
- continue;
1306
- }
1275
+ if (DISPLAY_ONLY_TYPES2.has(q.type)) continue;
1307
1276
  questions.push(q);
1308
1277
  }
1309
1278
  }
@@ -1369,6 +1338,10 @@ function formatExportValue(value, type) {
1369
1338
  return String(value);
1370
1339
  }
1371
1340
  function escapeCsvField(field) {
1341
+ const first = field.charAt(0);
1342
+ if (first === "=" || first === "+" || first === "-" || first === "@" || first === " " || first === "\r") {
1343
+ field = `'${field}`;
1344
+ }
1372
1345
  if (field.includes(",") || field.includes('"') || field.includes("\n")) {
1373
1346
  return `"${field.replace(/"/g, '""')}"`;
1374
1347
  }
@@ -1383,7 +1356,7 @@ function downloadBlob(content, filename, mimeType) {
1383
1356
  document.body.appendChild(link);
1384
1357
  link.click();
1385
1358
  document.body.removeChild(link);
1386
- URL.revokeObjectURL(url);
1359
+ setTimeout(() => URL.revokeObjectURL(url), 1e4);
1387
1360
  }
1388
1361
 
1389
1362
  // src/response-viewer/pagination-utils.ts
@@ -1616,9 +1589,12 @@ function ResponseViewerInner({
1616
1589
  [responses, searchQuery, schema]
1617
1590
  );
1618
1591
  const isFiltered = hasActiveFilters(filterState);
1619
- const filteredResponses = applyFilters(searchedResponses, filterState);
1592
+ const filteredResponses = useMemo(
1593
+ () => applyFilters(searchedResponses, filterState),
1594
+ [searchedResponses, filterState]
1595
+ );
1620
1596
  const pag = paginate(filteredResponses, currentPage, effectivePageSize);
1621
- const questions = getAllQuestions2(schema);
1597
+ const questions = getAllQuestions(schema);
1622
1598
  const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
1623
1599
  function handleSelect(response) {
1624
1600
  setSelectedResponse(response);
@@ -1720,7 +1696,7 @@ function ResponseViewerInner({
1720
1696
  });
1721
1697
  }
1722
1698
  function getFields(response) {
1723
- return questions.map((q) => ({
1699
+ return questions.filter((q) => response.values[q.id] !== void 0).map((q) => ({
1724
1700
  questionId: q.id,
1725
1701
  label: q.label,
1726
1702
  type: q.type,
@@ -1873,10 +1849,10 @@ function ResponseViewerInner({
1873
1849
  className: "h-7 text-xs",
1874
1850
  onClick: () => {
1875
1851
  const csvFilename = filename ? filename.replace(/\.\w+$/, ".csv") : "responses.csv";
1876
- exportToCsv(schema, responses, csvFilename, { dateFormat, columnLabels });
1877
- onExport?.("csv", responses.length);
1852
+ exportToCsv(schema, filteredResponses, csvFilename, { dateFormat, columnLabels });
1853
+ onExport?.("csv", filteredResponses.length);
1878
1854
  },
1879
- disabled: responses.length === 0,
1855
+ disabled: filteredResponses.length === 0,
1880
1856
  children: "Export CSV"
1881
1857
  }
1882
1858
  ),
@@ -1888,10 +1864,10 @@ function ResponseViewerInner({
1888
1864
  className: "h-7 text-xs",
1889
1865
  onClick: () => {
1890
1866
  const jsonFilename = filename ? filename.replace(/\.\w+$/, ".json") : "responses.json";
1891
- exportToJson(responses, jsonFilename);
1892
- onExport?.("json", responses.length);
1867
+ exportToJson(filteredResponses, jsonFilename);
1868
+ onExport?.("json", filteredResponses.length);
1893
1869
  },
1894
- disabled: responses.length === 0,
1870
+ disabled: filteredResponses.length === 0,
1895
1871
  children: "Export JSON"
1896
1872
  }
1897
1873
  )
@@ -2172,13 +2148,26 @@ function ResponseViewerInner({
2172
2148
  }
2173
2149
  );
2174
2150
  }
2175
- function getAllQuestions2(schema) {
2151
+ var DISPLAY_ONLY_TYPES3 = /* @__PURE__ */ new Set([
2152
+ "info-block",
2153
+ "info_block",
2154
+ "section-header",
2155
+ "page-break",
2156
+ "image",
2157
+ "divider",
2158
+ "spacer",
2159
+ "video",
2160
+ "rich-text",
2161
+ "welcome-screen",
2162
+ "thank-you-screen",
2163
+ "hidden",
2164
+ "calculated"
2165
+ ]);
2166
+ function getAllQuestions(schema) {
2176
2167
  const questions = [];
2177
2168
  for (const section of schema.sections) {
2178
2169
  for (const q of section.questions) {
2179
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
2180
- continue;
2181
- }
2170
+ if (DISPLAY_ONLY_TYPES3.has(q.type)) continue;
2182
2171
  questions.push(q);
2183
2172
  }
2184
2173
  }
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { b as FormBuilderProps, d as FormBuilderTheme, f as QuestionTypeInfo, S as SelectedItem, D as DragItem } from '../types-bFHsZT1n.mjs';
4
- export { B as BuilderState, a as DropTarget, M as MutationAction, P as PaletteCategory, e as PropertyPanelTab, Q as QuestionTypeCategory } from '../types-bFHsZT1n.mjs';
4
+ export { B as BuilderState, a as DropTarget, F as FormBuilderPreviewProps, M as MutationAction, P as PaletteCategory, e as PropertyPanelTab, Q as QuestionTypeCategory } from '../types-bFHsZT1n.mjs';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
  import { FormEngineSchema, Option, Question, Section } from '@squaredr/fieldcraft-core';
7
7
  import { ClassValue } from 'clsx';
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { b as FormBuilderProps, d as FormBuilderTheme, f as QuestionTypeInfo, S as SelectedItem, D as DragItem } from '../types-bFHsZT1n.js';
4
- export { B as BuilderState, a as DropTarget, M as MutationAction, P as PaletteCategory, e as PropertyPanelTab, Q as QuestionTypeCategory } from '../types-bFHsZT1n.js';
4
+ export { B as BuilderState, a as DropTarget, F as FormBuilderPreviewProps, M as MutationAction, P as PaletteCategory, e as PropertyPanelTab, Q as QuestionTypeCategory } from '../types-bFHsZT1n.js';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
  import { FormEngineSchema, Option, Question, Section } from '@squaredr/fieldcraft-core';
7
7
  import { ClassValue } from 'clsx';