@squaredr/fieldcraft-pro 1.6.4 → 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.
@@ -7,27 +7,6 @@ var clsx = require('clsx');
7
7
  var tailwindMerge = require('tailwind-merge');
8
8
 
9
9
  // ../license/dist/index.mjs
10
- var PRO_FEATURES = [
11
- "SchemaEditor",
12
- "ResponseViewer",
13
- "ThemeEditor",
14
- "FormBuilder",
15
- "ApiManager",
16
- "Analytics"
17
- ];
18
- var ALL_FEATURES = [
19
- ...PRO_FEATURES,
20
- "Telehealth"
21
- ];
22
- var TIER_FEATURES = {
23
- starter: ["SchemaEditor", "ResponseViewer"],
24
- pro: PRO_FEATURES,
25
- business: ALL_FEATURES,
26
- enterprise: ALL_FEATURES
27
- };
28
- function tierHasFeature(tier, featureName) {
29
- return TIER_FEATURES[tier]?.includes(featureName) ?? false;
30
- }
31
10
  var DEV_HOSTNAMES = /* @__PURE__ */ new Set([
32
11
  "localhost",
33
12
  "127.0.0.1",
@@ -99,7 +78,8 @@ function isProductionEnvironment() {
99
78
  }
100
79
  return false;
101
80
  }
102
- var defaultContext = { status: "validating", tier: null };
81
+ typeof process !== "undefined" && process.env?.NEXT_PUBLIC_FCPRO_PING_URL || "https://fieldcraft.dev/api/license/ping";
82
+ var defaultContext = { status: "validating" };
103
83
  var LicenseCtx = react.createContext(defaultContext);
104
84
  function useLicense() {
105
85
  return react.useContext(LicenseCtx);
@@ -299,7 +279,7 @@ function UnlicensedOverlay({ featureName, reason, children }) {
299
279
  }
300
280
  function requireLicense(Component, featureName) {
301
281
  function LicenseGated(props) {
302
- const { status, tier } = useLicense();
282
+ const { status } = useLicense();
303
283
  const isProduction = isProductionEnvironment();
304
284
  if (!isProduction) {
305
285
  return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props });
@@ -307,8 +287,7 @@ function requireLicense(Component, featureName) {
307
287
  if (status === "validating") {
308
288
  return null;
309
289
  }
310
- const isLicensed = status === "valid" && tier != null && tierHasFeature(tier, featureName);
311
- if (isLicensed) {
290
+ if (status === "valid") {
312
291
  return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props });
313
292
  }
314
293
  let reason = "A valid license is required for production use.";
@@ -320,14 +299,127 @@ function requireLicense(Component, featureName) {
320
299
  reason = "This license key has been revoked. Please contact support or purchase a new license.";
321
300
  } else if (status === "domain_mismatch") {
322
301
  reason = "This license key is registered to a different domain. Each key can only be used on one production domain. Please purchase a new license for this domain.";
323
- } else if (!tier || !tierHasFeature(tier, featureName)) {
324
- reason = "Your license does not include this feature. Please upgrade your plan.";
325
302
  }
326
303
  return /* @__PURE__ */ jsxRuntime.jsx(UnlicensedOverlay, { featureName, reason, children: /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props }) });
327
304
  }
328
305
  LicenseGated.displayName = `requireLicense(${Component.displayName || Component.name || "Component"})`;
329
306
  return LicenseGated;
330
307
  }
308
+ function formatDuration(ms) {
309
+ if (ms == null) return "\u2014";
310
+ const totalSeconds = Math.round(ms / 1e3);
311
+ if (totalSeconds < 60) return `${totalSeconds}s`;
312
+ const minutes = Math.floor(totalSeconds / 60);
313
+ const seconds = totalSeconds % 60;
314
+ return `${minutes}m ${seconds}s`;
315
+ }
316
+ function countAnswered(values) {
317
+ let count = 0;
318
+ for (const v of Object.values(values)) {
319
+ if (v !== void 0 && v !== null && v !== "") count++;
320
+ }
321
+ return count;
322
+ }
323
+ function getTotalFieldCount(schema) {
324
+ let count = 0;
325
+ for (const section of schema.sections) {
326
+ for (const q of section.questions) {
327
+ if (!DISPLAY_ONLY_TYPES.has(q.type)) count++;
328
+ }
329
+ }
330
+ return count;
331
+ }
332
+ function ResponseTable({
333
+ schema,
334
+ responses,
335
+ onRowClick,
336
+ selectable,
337
+ selectedIds,
338
+ onToggleSelect,
339
+ onSelectAll
340
+ }) {
341
+ const totalFields = getTotalFieldCount(schema);
342
+ const allPageSelected = selectable && responses.length > 0 && responses.every(
343
+ (r) => r.sessionToken && selectedIds?.has(r.sessionToken)
344
+ );
345
+ const colCount = 4 + (selectable ? 1 : 0);
346
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
347
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "bg-muted", children: [
348
+ selectable && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
349
+ "input",
350
+ {
351
+ type: "checkbox",
352
+ checked: allPageSelected,
353
+ onChange: () => onSelectAll?.(),
354
+ className: "accent-primary"
355
+ }
356
+ ) }),
357
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
358
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Completion Time" }),
359
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Fields Answered" }),
360
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Score" })
361
+ ] }) }),
362
+ /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
363
+ responses.map((response, idx) => {
364
+ const answered = countAnswered(response.values);
365
+ return /* @__PURE__ */ jsxRuntime.jsxs(
366
+ "tr",
367
+ {
368
+ onClick: () => onRowClick?.(response),
369
+ className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
370
+ children: [
371
+ selectable && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsxRuntime.jsx(
372
+ "input",
373
+ {
374
+ type: "checkbox",
375
+ checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
376
+ onChange: (e) => {
377
+ e.stopPropagation();
378
+ if (response.sessionToken) onToggleSelect?.(response.sessionToken);
379
+ },
380
+ onClick: (e) => e.stopPropagation(),
381
+ className: "accent-primary"
382
+ }
383
+ ) }),
384
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
385
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: formatDuration(response.completionTimeMs) }),
386
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: [
387
+ answered,
388
+ "/",
389
+ totalFields
390
+ ] }),
391
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: response.totalScore ?? "\u2014" })
392
+ ]
393
+ },
394
+ response.sessionToken || idx
395
+ );
396
+ }),
397
+ responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
398
+ "td",
399
+ {
400
+ colSpan: colCount,
401
+ className: "px-3 py-8 text-center text-muted-foreground",
402
+ children: "No responses yet"
403
+ }
404
+ ) })
405
+ ] })
406
+ ] }) });
407
+ }
408
+ var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
409
+ "info-block",
410
+ "info_block",
411
+ "section-header",
412
+ "page-break",
413
+ "image",
414
+ "divider",
415
+ "spacer",
416
+ "video",
417
+ "rich-text",
418
+ "welcome-screen",
419
+ "thank-you-screen",
420
+ "hidden",
421
+ "calculated"
422
+ ]);
331
423
 
332
424
  // src/response-viewer/clinical-display-data.ts
333
425
  var BODY_REGION_LABELS = {
@@ -465,166 +557,6 @@ function getScoreSeverity(instrumentKey, score) {
465
557
  if (!thresholds) return void 0;
466
558
  return thresholds.find((t) => score >= t.min && score <= t.max);
467
559
  }
468
- function ResponseTable({
469
- schema,
470
- responses,
471
- onRowClick,
472
- selectable,
473
- selectedIds,
474
- onToggleSelect,
475
- onSelectAll
476
- }) {
477
- const questions = getAllQuestions(schema);
478
- const allPageSelected = selectable && responses.length > 0 && responses.every(
479
- (r) => r.sessionToken && selectedIds?.has(r.sessionToken)
480
- );
481
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
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
- ) }),
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" }),
493
- questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
494
- "th",
495
- {
496
- className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap",
497
- children: q.label
498
- },
499
- q.id
500
- )),
501
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Score" })
502
- ] }) }),
503
- /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
504
- responses.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
505
- "tr",
506
- {
507
- onClick: () => onRowClick?.(response),
508
- className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
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
- ) }),
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() }),
524
- questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
525
- "td",
526
- {
527
- className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap",
528
- children: formatCellValue(response.values[q.id], q.type)
529
- },
530
- q.id
531
- )),
532
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap", children: response.totalScore ?? "\u2014" })
533
- ]
534
- },
535
- response.sessionToken || idx
536
- )),
537
- responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
538
- "td",
539
- {
540
- colSpan: questions.length + 2 + (selectable ? 1 : 0),
541
- className: "px-3 py-8 text-center text-muted-foreground",
542
- children: "No responses yet"
543
- }
544
- ) })
545
- ] })
546
- ] }) });
547
- }
548
- function getAllQuestions(schema) {
549
- const questions = [];
550
- for (const section of schema.sections) {
551
- for (const q of section.questions) {
552
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
553
- continue;
554
- }
555
- questions.push(q);
556
- }
557
- }
558
- return questions;
559
- }
560
- function formatCellValue(value, type) {
561
- if (value == null) return "\u2014";
562
- if (type) {
563
- switch (type) {
564
- case "vitals_entry": {
565
- if (typeof value !== "object" || value === null) break;
566
- const v = value;
567
- const parts = [];
568
- if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
569
- if (v.heartRate) parts.push(`HR ${v.heartRate}`);
570
- if (v.temperature) parts.push(`${v.temperature}\xB0F`);
571
- if (v.oxygenSaturation) parts.push(`SpO\u2082 ${v.oxygenSaturation}%`);
572
- return parts.length > 0 ? parts.join(", ") : "\u2014";
573
- }
574
- case "medication_list":
575
- if (Array.isArray(value)) return `${value.length} medication${value.length !== 1 ? "s" : ""}`;
576
- break;
577
- case "allergy_list":
578
- if (Array.isArray(value)) return `${value.length} allerg${value.length !== 1 ? "ies" : "y"}`;
579
- break;
580
- case "body_diagram":
581
- if (Array.isArray(value)) {
582
- const labels = value.map((id) => BODY_REGION_LABELS[id] ?? id);
583
- return labels.join(", ");
584
- }
585
- break;
586
- case "pain_scale":
587
- if (typeof value === "number") return `${value}/10`;
588
- break;
589
- case "bmi_calculator": {
590
- if (typeof value !== "object" || value === null) break;
591
- const d = value;
592
- if (d.bmi != null) return `BMI ${d.bmi}`;
593
- break;
594
- }
595
- case "payment": {
596
- if (typeof value !== "object" || value === null) break;
597
- const p = value;
598
- const status = p.status;
599
- return status ? status.charAt(0).toUpperCase() + status.slice(1) : "\u2014";
600
- }
601
- case "insurance_card": {
602
- if (typeof value !== "object" || value === null) break;
603
- const ins = value;
604
- const parts = [ins.carrierId, ins.planName].filter(Boolean);
605
- return parts.length > 0 ? parts.join(" \u2014 ") : "Card uploaded";
606
- }
607
- case "legal_name": {
608
- if (typeof value !== "object" || value === null) break;
609
- const n = value;
610
- return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
611
- }
612
- case "address": {
613
- if (typeof value !== "object" || value === null) break;
614
- const a = value;
615
- return [a.city, a.state].filter(Boolean).join(", ") || "\u2014";
616
- }
617
- case "consent":
618
- return value === true || value === "true" || value === "agreed" ? "Agreed" : "Not agreed";
619
- case "signature":
620
- return typeof value === "string" && value.startsWith("data:image") ? "Signed" : "\u2014";
621
- }
622
- }
623
- if (typeof value === "boolean") return value ? "Yes" : "No";
624
- if (Array.isArray(value)) return value.join(", ");
625
- if (typeof value === "object") return JSON.stringify(value);
626
- return String(value);
627
- }
628
560
  function ResponseCard({
629
561
  response,
630
562
  fields,
@@ -1622,13 +1554,26 @@ function exportToJson(responses, filename = "responses.json") {
1622
1554
  const content = JSON.stringify(responses, null, 2);
1623
1555
  downloadBlob(content, filename, "application/json;charset=utf-8;");
1624
1556
  }
1557
+ var DISPLAY_ONLY_TYPES2 = /* @__PURE__ */ new Set([
1558
+ "info-block",
1559
+ "info_block",
1560
+ "section-header",
1561
+ "page-break",
1562
+ "image",
1563
+ "divider",
1564
+ "spacer",
1565
+ "video",
1566
+ "rich-text",
1567
+ "welcome-screen",
1568
+ "thank-you-screen",
1569
+ "hidden",
1570
+ "calculated"
1571
+ ]);
1625
1572
  function getExportableQuestions(schema) {
1626
1573
  const questions = [];
1627
1574
  for (const section of schema.sections) {
1628
1575
  for (const q of section.questions) {
1629
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
1630
- continue;
1631
- }
1576
+ if (DISPLAY_ONLY_TYPES2.has(q.type)) continue;
1632
1577
  questions.push(q);
1633
1578
  }
1634
1579
  }
@@ -1694,6 +1639,10 @@ function formatExportValue(value, type) {
1694
1639
  return String(value);
1695
1640
  }
1696
1641
  function escapeCsvField(field) {
1642
+ const first = field.charAt(0);
1643
+ if (first === "=" || first === "+" || first === "-" || first === "@" || first === " " || first === "\r") {
1644
+ field = `'${field}`;
1645
+ }
1697
1646
  if (field.includes(",") || field.includes('"') || field.includes("\n")) {
1698
1647
  return `"${field.replace(/"/g, '""')}"`;
1699
1648
  }
@@ -1708,7 +1657,7 @@ function downloadBlob(content, filename, mimeType) {
1708
1657
  document.body.appendChild(link);
1709
1658
  link.click();
1710
1659
  document.body.removeChild(link);
1711
- URL.revokeObjectURL(url);
1660
+ setTimeout(() => URL.revokeObjectURL(url), 1e4);
1712
1661
  }
1713
1662
 
1714
1663
  // src/response-viewer/pagination-utils.ts
@@ -1944,9 +1893,12 @@ function ResponseViewerInner({
1944
1893
  [responses, searchQuery, schema]
1945
1894
  );
1946
1895
  const isFiltered = hasActiveFilters(filterState);
1947
- const filteredResponses = applyFilters(searchedResponses, filterState);
1896
+ const filteredResponses = react.useMemo(
1897
+ () => applyFilters(searchedResponses, filterState),
1898
+ [searchedResponses, filterState]
1899
+ );
1948
1900
  const pag = paginate(filteredResponses, currentPage, effectivePageSize);
1949
- const questions = getAllQuestions2(schema);
1901
+ const questions = getAllQuestions(schema);
1950
1902
  const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
1951
1903
  function handleSelect(response) {
1952
1904
  setSelectedResponse(response);
@@ -2048,7 +2000,7 @@ function ResponseViewerInner({
2048
2000
  });
2049
2001
  }
2050
2002
  function getFields(response) {
2051
- return questions.map((q) => ({
2003
+ return questions.filter((q) => response.values[q.id] !== void 0).map((q) => ({
2052
2004
  questionId: q.id,
2053
2005
  label: q.label,
2054
2006
  type: q.type,
@@ -2201,10 +2153,10 @@ function ResponseViewerInner({
2201
2153
  className: "h-7 text-xs",
2202
2154
  onClick: () => {
2203
2155
  const csvFilename = filename ? filename.replace(/\.\w+$/, ".csv") : "responses.csv";
2204
- exportToCsv(schema, responses, csvFilename, { dateFormat, columnLabels });
2205
- onExport?.("csv", responses.length);
2156
+ exportToCsv(schema, filteredResponses, csvFilename, { dateFormat, columnLabels });
2157
+ onExport?.("csv", filteredResponses.length);
2206
2158
  },
2207
- disabled: responses.length === 0,
2159
+ disabled: filteredResponses.length === 0,
2208
2160
  children: "Export CSV"
2209
2161
  }
2210
2162
  ),
@@ -2216,10 +2168,10 @@ function ResponseViewerInner({
2216
2168
  className: "h-7 text-xs",
2217
2169
  onClick: () => {
2218
2170
  const jsonFilename = filename ? filename.replace(/\.\w+$/, ".json") : "responses.json";
2219
- exportToJson(responses, jsonFilename);
2220
- onExport?.("json", responses.length);
2171
+ exportToJson(filteredResponses, jsonFilename);
2172
+ onExport?.("json", filteredResponses.length);
2221
2173
  },
2222
- disabled: responses.length === 0,
2174
+ disabled: filteredResponses.length === 0,
2223
2175
  children: "Export JSON"
2224
2176
  }
2225
2177
  )
@@ -2500,13 +2452,26 @@ function ResponseViewerInner({
2500
2452
  }
2501
2453
  );
2502
2454
  }
2503
- function getAllQuestions2(schema) {
2455
+ var DISPLAY_ONLY_TYPES3 = /* @__PURE__ */ new Set([
2456
+ "info-block",
2457
+ "info_block",
2458
+ "section-header",
2459
+ "page-break",
2460
+ "image",
2461
+ "divider",
2462
+ "spacer",
2463
+ "video",
2464
+ "rich-text",
2465
+ "welcome-screen",
2466
+ "thank-you-screen",
2467
+ "hidden",
2468
+ "calculated"
2469
+ ]);
2470
+ function getAllQuestions(schema) {
2504
2471
  const questions = [];
2505
2472
  for (const section of schema.sections) {
2506
2473
  for (const q of section.questions) {
2507
- if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
2508
- continue;
2509
- }
2474
+ if (DISPLAY_ONLY_TYPES3.has(q.type)) continue;
2510
2475
  questions.push(q);
2511
2476
  }
2512
2477
  }
@@ -1,3 +1,3 @@
1
- export { ResponseViewer } from '../chunk-5LKGCZAW.mjs';
1
+ export { ResponseViewer } from '../chunk-MLP5EDWP.mjs';
2
2
  import '../chunk-QQ4JZGTD.mjs';
3
- import '../chunk-VECQKSWS.mjs';
3
+ import '../chunk-4MMKB2EW.mjs';