formanitor 0.0.48 → 0.0.49

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/index.cjs CHANGED
@@ -67,8 +67,25 @@ function fieldMetaRequestsMarMedicationOrdersButton(meta) {
67
67
 
68
68
  // src/core/validate.ts
69
69
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
70
- function validateField(value, field) {
70
+ function validateField(value, field, allValues) {
71
71
  if (field.hidden || field.disabled) return null;
72
+ if (field.type === "otp_input") {
73
+ if (field.required) {
74
+ if (!value || typeof value !== "object" || value.verified !== true) {
75
+ return "Please verify your mobile number with OTP";
76
+ }
77
+ const otpField = field;
78
+ const phoneFieldId = otpField.meta?.phoneFieldId ?? "phone";
79
+ if (allValues) {
80
+ const phoneRaw = allValues[phoneFieldId] ?? "";
81
+ const e164 = phoneRaw.startsWith("+") ? phoneRaw : `+91${phoneRaw}`;
82
+ if (value.phone && value.phone !== e164) {
83
+ return "Mobile number changed \u2014 please re-verify";
84
+ }
85
+ }
86
+ }
87
+ return null;
88
+ }
72
89
  if (field.required) {
73
90
  if (value === null || value === void 0 || value === "") {
74
91
  return "This field is required";
@@ -129,7 +146,7 @@ function validateForm(values, fields) {
129
146
  const errors = {};
130
147
  for (const [key, field] of Object.entries(fields)) {
131
148
  if (field.type === "static_text") continue;
132
- const error = validateField(values[key], field);
149
+ const error = validateField(values[key], field, values);
133
150
  if (error) {
134
151
  errors[key] = error;
135
152
  }
@@ -1206,6 +1223,9 @@ var FormStore = class {
1206
1223
  persistence;
1207
1224
  prefillData = {};
1208
1225
  config;
1226
+ // Widget-owned errors that schema validation cannot express (e.g. "OTP not sent yet").
1227
+ // Merged on top of validateForm output in evaluate().
1228
+ externalErrors = {};
1209
1229
  // Cache for debounce
1210
1230
  saveTimeout = null;
1211
1231
  draftCallbackTimeout = null;
@@ -1325,6 +1345,9 @@ var FormStore = class {
1325
1345
  getSchema() {
1326
1346
  return this.schema;
1327
1347
  }
1348
+ getConfig() {
1349
+ return this.config;
1350
+ }
1328
1351
  getFieldDef(fieldId) {
1329
1352
  return this.fieldMap[fieldId];
1330
1353
  }
@@ -1372,6 +1395,17 @@ var FormStore = class {
1372
1395
  this.notify();
1373
1396
  this.scheduleSave();
1374
1397
  }
1398
+ /**
1399
+ * Let a widget report an error that schema validation cannot express
1400
+ * (e.g. "Send OTP to continue" until the API call succeeds).
1401
+ * Pass `null` to clear a previously set external error.
1402
+ */
1403
+ setExternalError(fieldId, message) {
1404
+ if ((this.externalErrors[fieldId] ?? null) === message) return;
1405
+ this.externalErrors = { ...this.externalErrors, [fieldId]: message };
1406
+ this.evaluate();
1407
+ this.notify();
1408
+ }
1375
1409
  setTouched(fieldId) {
1376
1410
  if (this.state.touched[fieldId]) return;
1377
1411
  this.state.touched = { ...this.state.touched, [fieldId]: true };
@@ -1500,6 +1534,20 @@ var FormStore = class {
1500
1534
  result[field.id] = parts.length > 0 ? parts.join("\n\n") : null;
1501
1535
  return;
1502
1536
  }
1537
+ if (field.type === "otp_input") {
1538
+ const raw = values[field.id];
1539
+ if (typeof raw === "string") {
1540
+ result[field.id] = raw;
1541
+ return;
1542
+ }
1543
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && raw.verified === true) {
1544
+ const code = raw.code;
1545
+ result[field.id] = typeof code === "string" ? code : null;
1546
+ return;
1547
+ }
1548
+ result[field.id] = null;
1549
+ return;
1550
+ }
1503
1551
  if (field.type === "image_upload" || field.type === "media_upload" || field.type === "signature") {
1504
1552
  const raw = values[field.id];
1505
1553
  if (raw !== void 0 && raw !== null && raw !== "") {
@@ -1616,8 +1664,12 @@ var FormStore = class {
1616
1664
  };
1617
1665
  });
1618
1666
  const errors = validateForm(this.state.values, effectiveFields);
1619
- this.state.errors = errors;
1620
- this.state.isValid = Object.keys(errors).length === 0;
1667
+ const merged = { ...errors };
1668
+ for (const [id, msg] of Object.entries(this.externalErrors)) {
1669
+ if (msg) merged[id] = msg;
1670
+ }
1671
+ this.state.errors = merged;
1672
+ this.state.isValid = Object.keys(merged).length === 0;
1621
1673
  }
1622
1674
  // --- Workflow ---
1623
1675
  getAvailableTransitions() {
@@ -13810,6 +13862,347 @@ var OBGPathwayWidget = ({ fieldId }) => {
13810
13862
  ] })
13811
13863
  ] });
13812
13864
  };
13865
+ var RESEND_COOLDOWN_SECONDS = 30;
13866
+ var PhoneInputWidget = ({ fieldId }) => {
13867
+ const { fieldDef, error, touched, disabled, value: fieldValue } = useField(fieldId);
13868
+ const { state } = useForm();
13869
+ const store = useFormStore();
13870
+ const rawPhone = fieldValue ?? "";
13871
+ const wasAlreadySent = rawPhone.length === 10 && !error;
13872
+ const [status, setStatus] = React15.useState(wasAlreadySent ? "sent" : "idle");
13873
+ const [resendCooldown, setResendCooldown] = React15.useState(0);
13874
+ const [resendError, setResendError] = React15.useState("");
13875
+ const hasSentRef = React15.useRef(wasAlreadySent);
13876
+ const sentPhoneRef = React15.useRef(wasAlreadySent ? rawPhone : "");
13877
+ const isSendingRef = React15.useRef(false);
13878
+ const isResendingRef = React15.useRef(false);
13879
+ const showError = !!error && (touched || state.submitAttempted) && status !== "sending" && status !== "sent";
13880
+ React15.useEffect(() => {
13881
+ if (resendCooldown <= 0) return;
13882
+ const t = setTimeout(() => setResendCooldown((c) => Math.max(0, c - 1)), 1e3);
13883
+ return () => clearTimeout(t);
13884
+ }, [resendCooldown]);
13885
+ if (!fieldDef) return null;
13886
+ const placeholder = typeof fieldDef.placeholder === "string" && fieldDef.placeholder.trim() ? fieldDef.placeholder : "Enter 10-digit mobile number";
13887
+ const handleChange = (e) => {
13888
+ const digits = e.target.value.replace(/\D/g, "").slice(0, 10);
13889
+ if (hasSentRef.current && digits !== sentPhoneRef.current) {
13890
+ hasSentRef.current = false;
13891
+ sentPhoneRef.current = "";
13892
+ setStatus("idle");
13893
+ setResendCooldown(0);
13894
+ setResendError("");
13895
+ store.setExternalError(fieldId, "Send OTP to continue");
13896
+ const otpFieldId = fieldDef.meta?.otpFieldId;
13897
+ if (otpFieldId) {
13898
+ store.setValue(otpFieldId, "");
13899
+ store.setExternalError(otpFieldId, "Verify OTP to continue");
13900
+ }
13901
+ }
13902
+ store.setValue(fieldId, digits);
13903
+ if (digits.length === 10 && !hasSentRef.current && !isSendingRef.current) {
13904
+ sendOtp(digits);
13905
+ }
13906
+ };
13907
+ const sendOtp = async (phone) => {
13908
+ if (isSendingRef.current) return;
13909
+ isSendingRef.current = true;
13910
+ setStatus("sending");
13911
+ store.setExternalError(fieldId, "Sending OTP...");
13912
+ try {
13913
+ const authCfg = store.getConfig().auth;
13914
+ const base = authCfg?.baseUrl ?? ((typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_URL) ?? "");
13915
+ const sendPath = authCfg?.sendPath ?? "/api/auth/otp/send/";
13916
+ const countryCode = authCfg?.defaultCountryCode ?? "+91";
13917
+ const res = await fetch(`${base}${sendPath}`, {
13918
+ method: "POST",
13919
+ headers: { "Content-Type": "application/json" },
13920
+ body: JSON.stringify({ mobile_number: `${countryCode}${phone}` })
13921
+ });
13922
+ const data = await res.json().catch(() => ({}));
13923
+ if (res.ok) {
13924
+ hasSentRef.current = true;
13925
+ sentPhoneRef.current = phone;
13926
+ setStatus("sent");
13927
+ setResendCooldown(RESEND_COOLDOWN_SECONDS);
13928
+ store.setExternalError(fieldId, null);
13929
+ } else {
13930
+ setStatus("error");
13931
+ store.setExternalError(fieldId, data.error ?? "Failed to send OTP. Please try again.");
13932
+ }
13933
+ } catch {
13934
+ setStatus("error");
13935
+ store.setExternalError(fieldId, "Network error. Please try again.");
13936
+ } finally {
13937
+ isSendingRef.current = false;
13938
+ }
13939
+ };
13940
+ const handleResend = async () => {
13941
+ if (isResendingRef.current || resendCooldown > 0) return;
13942
+ isResendingRef.current = true;
13943
+ setResendError("");
13944
+ try {
13945
+ const authCfg = store.getConfig().auth;
13946
+ const base = authCfg?.baseUrl ?? ((typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_URL) ?? "");
13947
+ const resendPath = authCfg?.resendPath ?? "/api/auth/otp/resend/";
13948
+ const countryCode = authCfg?.defaultCountryCode ?? "+91";
13949
+ const res = await fetch(`${base}${resendPath}`, {
13950
+ method: "POST",
13951
+ headers: { "Content-Type": "application/json" },
13952
+ body: JSON.stringify({ mobile_number: `${countryCode}${rawPhone}`, retrytype: "text" })
13953
+ });
13954
+ const data = await res.json().catch(() => ({}));
13955
+ if (res.ok) {
13956
+ setResendCooldown(RESEND_COOLDOWN_SECONDS);
13957
+ setResendError("");
13958
+ } else {
13959
+ setResendError(data.error ?? "Failed to resend OTP. Please try again.");
13960
+ }
13961
+ } catch {
13962
+ setResendError("Network error. Please try again.");
13963
+ } finally {
13964
+ isResendingRef.current = false;
13965
+ }
13966
+ };
13967
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
13968
+ /* @__PURE__ */ jsxRuntime.jsxs(Label, { htmlFor: fieldId, children: [
13969
+ fieldDef.label,
13970
+ " ",
13971
+ fieldDef.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500", children: "*" })
13972
+ ] }),
13973
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
13974
+ /* @__PURE__ */ jsxRuntime.jsx(
13975
+ Input,
13976
+ {
13977
+ id: fieldId,
13978
+ type: "tel",
13979
+ inputMode: "numeric",
13980
+ value: rawPhone,
13981
+ onChange: handleChange,
13982
+ onBlur: () => store.setTouched(fieldId),
13983
+ disabled: disabled || status === "sending",
13984
+ placeholder,
13985
+ maxLength: 10,
13986
+ className: cn(
13987
+ showError && "border-red-500",
13988
+ status === "sent" && "border-green-500 pr-9",
13989
+ status === "sending" && "pr-9"
13990
+ )
13991
+ }
13992
+ ),
13993
+ status === "sending" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsxs(
13994
+ "svg",
13995
+ {
13996
+ className: "animate-spin h-4 w-4 text-muted-foreground",
13997
+ viewBox: "0 0 24 24",
13998
+ fill: "none",
13999
+ children: [
14000
+ /* @__PURE__ */ jsxRuntime.jsx(
14001
+ "circle",
14002
+ {
14003
+ className: "opacity-25",
14004
+ cx: "12",
14005
+ cy: "12",
14006
+ r: "10",
14007
+ stroke: "currentColor",
14008
+ strokeWidth: "4"
14009
+ }
14010
+ ),
14011
+ /* @__PURE__ */ jsxRuntime.jsx(
14012
+ "path",
14013
+ {
14014
+ className: "opacity-75",
14015
+ fill: "currentColor",
14016
+ d: "M4 12a8 8 0 018-8v8H4z"
14017
+ }
14018
+ )
14019
+ ]
14020
+ }
14021
+ ) }),
14022
+ status === "sent" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx(
14023
+ "svg",
14024
+ {
14025
+ className: "h-4 w-4 text-green-500",
14026
+ viewBox: "0 0 20 20",
14027
+ fill: "currentColor",
14028
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14029
+ "path",
14030
+ {
14031
+ fillRule: "evenodd",
14032
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
14033
+ clipRule: "evenodd"
14034
+ }
14035
+ )
14036
+ }
14037
+ ) })
14038
+ ] }),
14039
+ status === "sent" && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-green-600", children: [
14040
+ "OTP sent to +91",
14041
+ rawPhone
14042
+ ] }),
14043
+ showError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500", children: error }),
14044
+ status === "sent" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
14045
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Didn't receive it?" }),
14046
+ /* @__PURE__ */ jsxRuntime.jsx(
14047
+ "button",
14048
+ {
14049
+ type: "button",
14050
+ onClick: handleResend,
14051
+ disabled: resendCooldown > 0,
14052
+ className: cn(
14053
+ "underline transition-colors",
14054
+ resendCooldown > 0 ? "opacity-40 cursor-not-allowed" : "text-primary hover:text-primary/80 cursor-pointer"
14055
+ ),
14056
+ children: resendCooldown > 0 ? `Resend in ${resendCooldown}s` : "Resend OTP"
14057
+ }
14058
+ )
14059
+ ] }),
14060
+ resendError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500", children: resendError })
14061
+ ] });
14062
+ };
14063
+ var OtpInputWidget = ({ fieldId }) => {
14064
+ const { fieldDef, error, touched, disabled } = useField(fieldId);
14065
+ const { state } = useForm();
14066
+ const store = useFormStore();
14067
+ const phoneFieldId = fieldDef?.meta?.phoneFieldId ?? "phone";
14068
+ const { value: phoneValue } = useField(phoneFieldId);
14069
+ const phone = phoneValue ?? "";
14070
+ const e164Phone = phone.startsWith("+") ? phone : `+91${phone}`;
14071
+ const [otp, setOtp] = React15.useState("");
14072
+ const [status, setStatus] = React15.useState("idle");
14073
+ const isVerifyingRef = React15.useRef(false);
14074
+ const showError = !!error && (touched || state.submitAttempted) && status !== "verifying" && status !== "verified";
14075
+ if (!fieldDef) return null;
14076
+ const placeholder = typeof fieldDef.placeholder === "string" && fieldDef.placeholder.trim() ? fieldDef.placeholder : "Enter 4-digit OTP";
14077
+ const handleChange = (e) => {
14078
+ const digits = e.target.value.replace(/\D/g, "").slice(0, 4);
14079
+ if (status === "failed") {
14080
+ setStatus("idle");
14081
+ store.setExternalError(fieldId, null);
14082
+ }
14083
+ setOtp(digits);
14084
+ store.setValue(fieldId, digits);
14085
+ if (digits.length === 4 && !isVerifyingRef.current) {
14086
+ verifyOtp(digits);
14087
+ }
14088
+ };
14089
+ const verifyOtp = async (code) => {
14090
+ if (isVerifyingRef.current) return;
14091
+ isVerifyingRef.current = true;
14092
+ setStatus("verifying");
14093
+ store.setExternalError(fieldId, "Verifying...");
14094
+ try {
14095
+ const authCfg = store.getConfig().auth;
14096
+ const base = authCfg?.baseUrl ?? ((typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_URL) ?? "");
14097
+ const verifyPath = authCfg?.verifyPath ?? "/api/auth/otp/verify/";
14098
+ const res = await fetch(`${base}${verifyPath}`, {
14099
+ method: "POST",
14100
+ headers: { "Content-Type": "application/json" },
14101
+ body: JSON.stringify({ mobile_number: e164Phone, otp_code: code })
14102
+ });
14103
+ const data = await res.json().catch(() => ({}));
14104
+ if (res.ok) {
14105
+ setStatus("verified");
14106
+ store.setExternalError(fieldId, null);
14107
+ store.setValue(fieldId, {
14108
+ verified: true,
14109
+ phone: e164Phone,
14110
+ code,
14111
+ ...data?.verification_token ? { token: data.verification_token } : {}
14112
+ });
14113
+ } else {
14114
+ setStatus("failed");
14115
+ const msg = data.error ?? "Invalid OTP. Please try again.";
14116
+ store.setExternalError(fieldId, msg);
14117
+ setOtp("");
14118
+ store.setValue(fieldId, "");
14119
+ }
14120
+ } catch {
14121
+ setStatus("failed");
14122
+ store.setExternalError(fieldId, "Network error. Please try again.");
14123
+ setOtp("");
14124
+ store.setValue(fieldId, "");
14125
+ } finally {
14126
+ isVerifyingRef.current = false;
14127
+ }
14128
+ };
14129
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
14130
+ /* @__PURE__ */ jsxRuntime.jsxs(Label, { htmlFor: fieldId, children: [
14131
+ fieldDef.label,
14132
+ " ",
14133
+ fieldDef.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500", children: "*" })
14134
+ ] }),
14135
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
14136
+ /* @__PURE__ */ jsxRuntime.jsx(
14137
+ Input,
14138
+ {
14139
+ id: fieldId,
14140
+ type: "tel",
14141
+ inputMode: "numeric",
14142
+ value: otp,
14143
+ onChange: handleChange,
14144
+ onBlur: () => store.setTouched(fieldId),
14145
+ disabled: disabled || status === "verifying" || status === "verified",
14146
+ placeholder,
14147
+ maxLength: 4,
14148
+ className: cn(
14149
+ "tracking-widest text-center text-lg",
14150
+ showError && "border-red-500",
14151
+ status === "verified" && "border-green-500 pr-9",
14152
+ status === "verifying" && "pr-9"
14153
+ )
14154
+ }
14155
+ ),
14156
+ status === "verifying" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsxs(
14157
+ "svg",
14158
+ {
14159
+ className: "animate-spin h-4 w-4 text-muted-foreground",
14160
+ viewBox: "0 0 24 24",
14161
+ fill: "none",
14162
+ children: [
14163
+ /* @__PURE__ */ jsxRuntime.jsx(
14164
+ "circle",
14165
+ {
14166
+ className: "opacity-25",
14167
+ cx: "12",
14168
+ cy: "12",
14169
+ r: "10",
14170
+ stroke: "currentColor",
14171
+ strokeWidth: "4"
14172
+ }
14173
+ ),
14174
+ /* @__PURE__ */ jsxRuntime.jsx(
14175
+ "path",
14176
+ {
14177
+ className: "opacity-75",
14178
+ fill: "currentColor",
14179
+ d: "M4 12a8 8 0 018-8v8H4z"
14180
+ }
14181
+ )
14182
+ ]
14183
+ }
14184
+ ) }),
14185
+ status === "verified" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx(
14186
+ "svg",
14187
+ {
14188
+ className: "h-4 w-4 text-green-500",
14189
+ viewBox: "0 0 20 20",
14190
+ fill: "currentColor",
14191
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14192
+ "path",
14193
+ {
14194
+ fillRule: "evenodd",
14195
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
14196
+ clipRule: "evenodd"
14197
+ }
14198
+ )
14199
+ }
14200
+ ) })
14201
+ ] }),
14202
+ status === "verified" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-green-600", children: "Mobile number verified successfully" }),
14203
+ showError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500", children: error })
14204
+ ] });
14205
+ };
13813
14206
  var FieldRenderer = ({ fieldId }) => {
13814
14207
  const { fieldDef, visible } = useField(fieldId);
13815
14208
  if (!visible || !fieldDef) {
@@ -13901,6 +14294,10 @@ function renderWidget(fieldId, fieldDef) {
13901
14294
  return /* @__PURE__ */ jsxRuntime.jsx(OBGExaminationWidget, { fieldId });
13902
14295
  case "obg_pathway":
13903
14296
  return /* @__PURE__ */ jsxRuntime.jsx(OBGPathwayWidget, { fieldId });
14297
+ case "phone_input":
14298
+ return /* @__PURE__ */ jsxRuntime.jsx(PhoneInputWidget, { fieldId });
14299
+ case "otp_input":
14300
+ return /* @__PURE__ */ jsxRuntime.jsx(OtpInputWidget, { fieldId });
13904
14301
  case "toggle": {
13905
14302
  const { value, setValue, setTouched, disabled } = useField(fieldId);
13906
14303
  const store = useFormStore();
@@ -14021,16 +14418,6 @@ var DynamicForm = () => {
14021
14418
  })
14022
14419
  ] });
14023
14420
  };
14024
- function PlainSection({
14025
- title,
14026
- showHeading,
14027
- children
14028
- }) {
14029
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 space-y-4", children: [
14030
- showHeading ? /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-medium text-foreground", children: title }) : null,
14031
- children
14032
- ] });
14033
- }
14034
14421
  function renderSectionChildren(children) {
14035
14422
  return children.map((child, index) => {
14036
14423
  if (typeof child === "string") {
@@ -14050,6 +14437,16 @@ function renderSectionChildren(children) {
14050
14437
  return /* @__PURE__ */ jsxRuntime.jsx(React15__namespace.default.Fragment, {}, index);
14051
14438
  });
14052
14439
  }
14440
+ function PlainSection({
14441
+ title,
14442
+ showHeading,
14443
+ children
14444
+ }) {
14445
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 space-y-4", children: [
14446
+ showHeading ? /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-medium text-foreground", children: title }) : null,
14447
+ children
14448
+ ] });
14449
+ }
14053
14450
  var DynamicFormV2 = ({
14054
14451
  showTitle = true,
14055
14452
  sectionMode = "plain",
@@ -14090,6 +14487,247 @@ var DynamicFormV2 = ({
14090
14487
  })
14091
14488
  ] });
14092
14489
  };
14490
+ function flattenStepFieldIds(step) {
14491
+ const ids = [];
14492
+ for (const child of step.children) {
14493
+ if (typeof child === "string") {
14494
+ ids.push(child);
14495
+ } else if (child.type === "column_layout") {
14496
+ ids.push(...child.children);
14497
+ }
14498
+ }
14499
+ return ids;
14500
+ }
14501
+ function isFieldComplete(id, state, fieldMap) {
14502
+ if (state.visibility[id] === false) return true;
14503
+ if (state.errors[id]) return false;
14504
+ const def = fieldMap[id];
14505
+ if (def?.required) {
14506
+ const v = state.values[id];
14507
+ if (v === null || v === void 0 || v === "") return false;
14508
+ if (Array.isArray(v) && v.length === 0) return false;
14509
+ }
14510
+ return true;
14511
+ }
14512
+ function isStepComplete(step, state, fieldMap) {
14513
+ return flattenStepFieldIds(step).every(
14514
+ (id) => isFieldComplete(id, state, fieldMap)
14515
+ );
14516
+ }
14517
+ var Stepper2 = ({ steps, statuses, variant }) => {
14518
+ if (variant === "dots") {
14519
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-6", children: steps.map((step, i) => {
14520
+ const status = statuses[i];
14521
+ return /* @__PURE__ */ jsxRuntime.jsxs(React15__namespace.default.Fragment, { children: [
14522
+ /* @__PURE__ */ jsxRuntime.jsx(
14523
+ "div",
14524
+ {
14525
+ className: cn(
14526
+ "h-2.5 w-2.5 rounded-full transition-colors",
14527
+ status === "complete" && "bg-primary",
14528
+ status === "active" && "bg-primary ring-2 ring-primary/30",
14529
+ status === "locked" && "bg-muted"
14530
+ ),
14531
+ title: step.title
14532
+ }
14533
+ ),
14534
+ i < steps.length - 1 && /* @__PURE__ */ jsxRuntime.jsx(
14535
+ "div",
14536
+ {
14537
+ className: cn(
14538
+ "flex-1 h-px transition-colors",
14539
+ status === "complete" ? "bg-primary" : "bg-border"
14540
+ )
14541
+ }
14542
+ )
14543
+ ] }, step.id);
14544
+ }) });
14545
+ }
14546
+ if (variant === "labels") {
14547
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-0 mb-6", children: steps.map((step, i) => {
14548
+ const status = statuses[i];
14549
+ return /* @__PURE__ */ jsxRuntime.jsxs(React15__namespace.default.Fragment, { children: [
14550
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
14551
+ /* @__PURE__ */ jsxRuntime.jsx(
14552
+ "div",
14553
+ {
14554
+ className: cn(
14555
+ "h-2 w-2 rounded-full",
14556
+ status === "complete" && "bg-primary",
14557
+ status === "active" && "bg-primary",
14558
+ status === "locked" && "bg-muted"
14559
+ )
14560
+ }
14561
+ ),
14562
+ /* @__PURE__ */ jsxRuntime.jsx(
14563
+ "span",
14564
+ {
14565
+ className: cn(
14566
+ "text-xs whitespace-nowrap",
14567
+ status === "locked" ? "text-muted-foreground" : "text-foreground"
14568
+ ),
14569
+ children: step.title
14570
+ }
14571
+ )
14572
+ ] }),
14573
+ i < steps.length - 1 && /* @__PURE__ */ jsxRuntime.jsx(
14574
+ "div",
14575
+ {
14576
+ className: cn(
14577
+ "flex-1 h-px mb-4 transition-colors",
14578
+ status === "complete" ? "bg-primary" : "bg-border"
14579
+ )
14580
+ }
14581
+ )
14582
+ ] }, step.id);
14583
+ }) });
14584
+ }
14585
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-0 mb-6", children: steps.map((step, i) => {
14586
+ const status = statuses[i];
14587
+ return /* @__PURE__ */ jsxRuntime.jsxs(React15__namespace.default.Fragment, { children: [
14588
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
14589
+ /* @__PURE__ */ jsxRuntime.jsx(
14590
+ "div",
14591
+ {
14592
+ className: cn(
14593
+ "flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold border-2 transition-colors",
14594
+ status === "complete" && "border-primary bg-primary text-primary-foreground",
14595
+ status === "active" && "border-primary bg-background text-primary",
14596
+ status === "locked" && "border-border bg-background text-muted-foreground"
14597
+ ),
14598
+ children: status === "complete" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5" }) : i + 1
14599
+ }
14600
+ ),
14601
+ /* @__PURE__ */ jsxRuntime.jsx(
14602
+ "span",
14603
+ {
14604
+ className: cn(
14605
+ "text-sm font-medium hidden sm:block",
14606
+ status === "locked" ? "text-muted-foreground" : "text-foreground"
14607
+ ),
14608
+ children: step.title
14609
+ }
14610
+ )
14611
+ ] }),
14612
+ i < steps.length - 1 && /* @__PURE__ */ jsxRuntime.jsx(
14613
+ "div",
14614
+ {
14615
+ className: cn(
14616
+ "flex-1 h-px mx-3 transition-colors",
14617
+ status === "complete" ? "bg-primary" : "bg-border"
14618
+ )
14619
+ }
14620
+ )
14621
+ ] }, step.id);
14622
+ }) });
14623
+ };
14624
+ var ActiveStep = ({
14625
+ title,
14626
+ children
14627
+ }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 space-y-4", children: [
14628
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-medium text-foreground", children: title }),
14629
+ children
14630
+ ] });
14631
+ var Wizard = ({ node }) => {
14632
+ const store = useFormStore();
14633
+ const [formState, setFormState] = React15.useState(() => ({
14634
+ ...store.getState()
14635
+ }));
14636
+ React15.useEffect(() => {
14637
+ return store.subscribe((state) => {
14638
+ console.log("[MultiStepForm] store update \u2192", {
14639
+ values: state.values,
14640
+ errors: state.errors,
14641
+ visibility: state.visibility
14642
+ });
14643
+ setFormState({ ...state });
14644
+ });
14645
+ }, [store]);
14646
+ const schema = store.getSchema();
14647
+ const fieldMap = {};
14648
+ schema.fields.forEach((f) => {
14649
+ fieldMap[f.id] = f;
14650
+ });
14651
+ const { steps, meta } = node;
14652
+ const showStepper = meta?.showStepper !== false;
14653
+ const variant = meta?.stepperVariant ?? "numbered";
14654
+ const collapseCompleted = meta?.collapseCompleted !== false;
14655
+ const stepComplete = steps.map((s) => {
14656
+ const fieldIds = flattenStepFieldIds(s);
14657
+ const complete = isStepComplete(s, formState, fieldMap);
14658
+ console.log(`[MultiStepForm] step "${s.id}" fieldIds=${JSON.stringify(fieldIds)} complete=${complete}`, {
14659
+ errors: fieldIds.map((id) => ({ id, error: formState.errors[id] })),
14660
+ values: fieldIds.map((id) => ({ id, value: formState.values[id] }))
14661
+ });
14662
+ return complete;
14663
+ });
14664
+ const activeIndex = stepComplete.findIndex((c) => !c) === -1 ? steps.length : stepComplete.findIndex((c) => !c);
14665
+ console.log(`[MultiStepForm] activeIndex=${activeIndex} stepComplete=${JSON.stringify(stepComplete)}`);
14666
+ const statuses = steps.map((_, i) => {
14667
+ if (i < activeIndex) return "complete";
14668
+ if (i === activeIndex) return "active";
14669
+ return "locked";
14670
+ });
14671
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
14672
+ node.title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold mb-4", children: node.title }),
14673
+ showStepper && /* @__PURE__ */ jsxRuntime.jsx(Stepper2, { steps, statuses, variant }),
14674
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: steps.map((step, i) => {
14675
+ const status = statuses[i];
14676
+ if (status === "locked") return null;
14677
+ const children = renderSectionChildren(step.children);
14678
+ if (status === "complete") {
14679
+ if (collapseCompleted) {
14680
+ return /* @__PURE__ */ jsxRuntime.jsx(Section, { title: step.title, children }, step.id);
14681
+ }
14682
+ return /* @__PURE__ */ jsxRuntime.jsxs(
14683
+ "div",
14684
+ {
14685
+ className: "mb-4 opacity-60 pointer-events-none select-none space-y-4",
14686
+ children: [
14687
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-medium text-foreground", children: step.title }),
14688
+ children
14689
+ ]
14690
+ },
14691
+ step.id
14692
+ );
14693
+ }
14694
+ return /* @__PURE__ */ jsxRuntime.jsx(ActiveStep, { title: step.title, children }, step.id);
14695
+ }) })
14696
+ ] });
14697
+ };
14698
+ var MultiStepForm = ({
14699
+ showTitle = true,
14700
+ className
14701
+ }) => {
14702
+ const store = useFormStore();
14703
+ const schema = store.getSchema();
14704
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-6", className), children: [
14705
+ showTitle && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold", children: schema.title }) }),
14706
+ schema.layout.map((node) => {
14707
+ if (node.type === "multi_step") {
14708
+ return /* @__PURE__ */ jsxRuntime.jsx(Wizard, { node }, node.id);
14709
+ }
14710
+ if (node.type === "section") {
14711
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 space-y-4", children: [
14712
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-medium text-foreground", children: node.title }),
14713
+ renderSectionChildren(node.children)
14714
+ ] }, node.id);
14715
+ }
14716
+ if (node.type === "column_layout") {
14717
+ return /* @__PURE__ */ jsxRuntime.jsx(
14718
+ ColumnLayout,
14719
+ {
14720
+ columns: node.columns,
14721
+ label: node.label,
14722
+ children: node.children.map((fieldId) => /* @__PURE__ */ jsxRuntime.jsx(FieldRenderer, { fieldId }, fieldId))
14723
+ },
14724
+ node.id
14725
+ );
14726
+ }
14727
+ return null;
14728
+ })
14729
+ ] });
14730
+ };
14093
14731
  var SUGGESTION_KEYS = /* @__PURE__ */ new Set(["medications", "investigations", "procedures"]);
14094
14732
  function shallowEqualKeys(a, b) {
14095
14733
  const keysA = Object.keys(a);
@@ -15052,7 +15690,8 @@ var FormControls = ({
15052
15690
  actionsFullWidth = false,
15053
15691
  buttonClassName,
15054
15692
  onSaveDraft,
15055
- onSubmit
15693
+ onSubmit,
15694
+ submitLabel
15056
15695
  }) => {
15057
15696
  const {
15058
15697
  state,
@@ -15141,7 +15780,7 @@ var FormControls = ({
15141
15780
  disabled: !state.isValid || hasUploadsInFlight,
15142
15781
  size: "sm",
15143
15782
  className: buttonCn,
15144
- children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : "Submit"
15783
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : availableTransitions.length > 1 ? `Submit to ${t.to}` : submitLabel ?? "Submit"
15145
15784
  },
15146
15785
  t.to
15147
15786
  )) : onSubmit ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -15154,7 +15793,7 @@ var FormControls = ({
15154
15793
  disabled: !state.isValid || hasUploadsInFlight,
15155
15794
  size: "sm",
15156
15795
  className: buttonCn,
15157
- children: hasUploadsInFlight ? "Upload in progress\u2026" : "Submit"
15796
+ children: hasUploadsInFlight ? "Upload in progress\u2026" : submitLabel ?? "Submit"
15158
15797
  }
15159
15798
  ) : null })
15160
15799
  ]
@@ -15208,6 +15847,7 @@ exports.FormStore = FormStore;
15208
15847
  exports.ImageUploadWidget = ImageUploadWidget;
15209
15848
  exports.MAR_MEDICATION_ORDERS_META_KEY = MAR_MEDICATION_ORDERS_META_KEY;
15210
15849
  exports.MediaUploadWidget = MediaUploadWidget;
15850
+ exports.MultiStepForm = MultiStepForm;
15211
15851
  exports.ReadOnlyForm = ReadOnlyForm;
15212
15852
  exports.ReadOnlyImageUpload = ReadOnlyImageUpload;
15213
15853
  exports.ReadOnlyMediaUpload = ReadOnlyMediaUpload;