@burdenoff/microfe-crewfoundry 2026.1030.4 → 2026.1102.2

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 (69) hide show
  1. package/dist/crewfoundry/CrewFoundryRoot.d.ts.map +1 -1
  2. package/dist/crewfoundry/CrewFoundryRoot.js +14 -0
  3. package/dist/crewfoundry/CrewFoundryRoot.js.map +1 -1
  4. package/dist/crewfoundry/CrewFoundryRoutes.d.ts.map +1 -1
  5. package/dist/crewfoundry/CrewFoundryRoutes.js +27 -19
  6. package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
  7. package/dist/crewfoundry/components/field/DispatchBoardCard.d.ts +13 -0
  8. package/dist/crewfoundry/components/field/DispatchBoardCard.d.ts.map +1 -0
  9. package/dist/crewfoundry/components/field/DispatchBoardCard.js +124 -0
  10. package/dist/crewfoundry/components/field/DispatchBoardCard.js.map +1 -0
  11. package/dist/crewfoundry/components/field/FieldBadges.d.ts +12 -0
  12. package/dist/crewfoundry/components/field/FieldBadges.d.ts.map +1 -0
  13. package/dist/crewfoundry/components/field/FieldBadges.js +45 -0
  14. package/dist/crewfoundry/components/field/FieldBadges.js.map +1 -0
  15. package/dist/crewfoundry/components/field/MyDispatchItem.d.ts +12 -0
  16. package/dist/crewfoundry/components/field/MyDispatchItem.d.ts.map +1 -0
  17. package/dist/crewfoundry/components/field/MyDispatchItem.js +46 -0
  18. package/dist/crewfoundry/components/field/MyDispatchItem.js.map +1 -0
  19. package/dist/crewfoundry/components/field/MyShiftItem.d.ts +14 -0
  20. package/dist/crewfoundry/components/field/MyShiftItem.d.ts.map +1 -0
  21. package/dist/crewfoundry/components/field/MyShiftItem.js +84 -0
  22. package/dist/crewfoundry/components/field/MyShiftItem.js.map +1 -0
  23. package/dist/crewfoundry/components/field/ShiftsCard.d.ts +12 -0
  24. package/dist/crewfoundry/components/field/ShiftsCard.d.ts.map +1 -0
  25. package/dist/crewfoundry/components/field/ShiftsCard.js +84 -0
  26. package/dist/crewfoundry/components/field/ShiftsCard.js.map +1 -0
  27. package/dist/crewfoundry/components/field/fieldFormat.d.ts +8 -0
  28. package/dist/crewfoundry/components/field/fieldFormat.d.ts.map +1 -0
  29. package/dist/crewfoundry/components/field/fieldFormat.js +15 -0
  30. package/dist/crewfoundry/components/field/fieldFormat.js.map +1 -0
  31. package/dist/crewfoundry/components/field/fieldForms.d.ts +19 -0
  32. package/dist/crewfoundry/components/field/fieldForms.d.ts.map +1 -0
  33. package/dist/crewfoundry/components/field/fieldForms.js +223 -0
  34. package/dist/crewfoundry/components/field/fieldForms.js.map +1 -0
  35. package/dist/crewfoundry/components/forms/FormModal.d.ts +1 -1
  36. package/dist/crewfoundry/components/forms/FormModal.d.ts.map +1 -1
  37. package/dist/crewfoundry/components/forms/FormModal.js +1 -1
  38. package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
  39. package/dist/crewfoundry/lib/fieldOps.d.ts +105 -0
  40. package/dist/crewfoundry/lib/fieldOps.d.ts.map +1 -0
  41. package/dist/crewfoundry/lib/fieldOps.js +85 -0
  42. package/dist/crewfoundry/lib/fieldOps.js.map +1 -0
  43. package/dist/crewfoundry/lib/useFieldOpsData.d.ts +14 -0
  44. package/dist/crewfoundry/lib/useFieldOpsData.d.ts.map +1 -0
  45. package/dist/crewfoundry/lib/useFieldOpsData.js +31 -0
  46. package/dist/crewfoundry/lib/useFieldOpsData.js.map +1 -0
  47. package/dist/crewfoundry/pages/DashboardExecutiveDashboardPage.d.ts +8 -0
  48. package/dist/crewfoundry/pages/DashboardExecutiveDashboardPage.d.ts.map +1 -1
  49. package/dist/crewfoundry/pages/DashboardExecutiveDashboardPage.js +468 -212
  50. package/dist/crewfoundry/pages/DashboardExecutiveDashboardPage.js.map +1 -1
  51. package/dist/crewfoundry/pages/FieldMyWorkPagePage.d.ts +9 -0
  52. package/dist/crewfoundry/pages/FieldMyWorkPagePage.d.ts.map +1 -0
  53. package/dist/crewfoundry/pages/FieldMyWorkPagePage.js +141 -0
  54. package/dist/crewfoundry/pages/FieldMyWorkPagePage.js.map +1 -0
  55. package/dist/crewfoundry/pages/FieldOperationsPagePage.d.ts +9 -0
  56. package/dist/crewfoundry/pages/FieldOperationsPagePage.d.ts.map +1 -0
  57. package/dist/crewfoundry/pages/FieldOperationsPagePage.js +231 -0
  58. package/dist/crewfoundry/pages/FieldOperationsPagePage.js.map +1 -0
  59. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +24 -24
  60. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +4 -4
  61. package/dist/crewfoundry/store/crewfoundryStore.d.ts +47 -1
  62. package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
  63. package/dist/crewfoundry/store/crewfoundryStore.js +93 -66
  64. package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
  65. package/dist/crewfoundry/store/fieldOpsStore.d.ts +76 -0
  66. package/dist/crewfoundry/store/fieldOpsStore.d.ts.map +1 -0
  67. package/dist/crewfoundry/store/fieldOpsStore.js +188 -0
  68. package/dist/crewfoundry/store/fieldOpsStore.js.map +1 -0
  69. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldForms.js","names":[],"sources":["../../../../src/crewfoundry/components/field/fieldForms.ts"],"sourcesContent":["/**\n * Form definitions and value mapping for the dispatch board (BOFF-4205).\n * Pure functions so the page stays a thin composition of cards and dialogs.\n */\nimport type { FieldConfig, FieldOption, FormValues } from '../forms/FormModal';\nimport { getNumber, getText } from '../forms/formValues';\nimport {\n type CrewShift,\n type DispatchPriority,\n localInputToIso,\n parseCoordinate,\n} from '../../lib/fieldOps';\nimport type { DispatchInput, ShiftInput } from '../../store/fieldOpsStore';\nimport { formatEnumLabel } from '../../utils/formatEnumLabel';\nimport { shiftOptionLabel } from './fieldFormat';\n\n/** Select value meaning \"none\" (FormModal selects cannot hold an empty value). */\nexport const NO_SELECTION = 'none';\n\nconst PRIORITIES: DispatchPriority[] = ['LOW', 'NORMAL', 'HIGH', 'URGENT'];\n\nexport function buildShiftFields(employees: FieldOption[], members: FieldOption[]): FieldConfig[] {\n return [\n { name: 'employeeId', label: 'Employee', type: 'select', required: true, options: employees },\n {\n name: 'assigneeUserId',\n label: 'Worker login',\n type: 'select',\n required: true,\n options: [\n { label: 'No login linked (no check-in or push)', value: NO_SELECTION },\n ...members,\n ],\n },\n {\n name: 'title',\n label: 'Shift title',\n type: 'text',\n required: true,\n placeholder: 'Morning install crew',\n },\n { name: 'role', label: 'Role', type: 'text', placeholder: 'Lead technician' },\n { name: 'startsAt', label: 'Starts', type: 'datetime', required: true },\n { name: 'endsAt', label: 'Ends', type: 'datetime', required: true },\n { name: 'siteName', label: 'Site name', type: 'text', placeholder: 'Depot 4' },\n { name: 'siteAddress', label: 'Site address', type: 'text' },\n { name: 'siteLatitude', label: 'Site latitude', type: 'text', placeholder: '13.0827' },\n { name: 'siteLongitude', label: 'Site longitude', type: 'text', placeholder: '80.2707' },\n {\n name: 'geofenceRadiusMeters',\n label: 'Check-in radius (m)',\n type: 'number',\n min: 25,\n max: 5000,\n },\n {\n name: 'reminderMinutesBefore',\n label: 'Reminder (minutes before)',\n type: 'number',\n min: 0,\n max: 1440,\n },\n { name: 'notes', label: 'Notes', type: 'textarea' },\n ];\n}\n\nexport function buildDispatchFields(upcoming: CrewShift[]): FieldConfig[] {\n return [\n {\n name: 'title',\n label: 'Work order',\n type: 'text',\n required: true,\n placeholder: 'Replace pump seal',\n },\n {\n name: 'priority',\n label: 'Priority',\n type: 'select',\n required: true,\n options: PRIORITIES.map((value) => ({ label: formatEnumLabel(value), value })),\n },\n { name: 'dueAt', label: 'Due', type: 'datetime' },\n {\n name: 'shiftId',\n label: 'Assign to shift',\n type: 'select',\n options: [\n { label: 'Leave unassigned', value: NO_SELECTION },\n ...upcoming.map((shift) => ({ label: shiftOptionLabel(shift), value: shift.id })),\n ],\n },\n { name: 'siteName', label: 'Site name', type: 'text' },\n { name: 'siteAddress', label: 'Site address', type: 'text' },\n { name: 'siteLatitude', label: 'Site latitude', type: 'text' },\n { name: 'siteLongitude', label: 'Site longitude', type: 'text' },\n { name: 'description', label: 'Details', type: 'textarea' },\n ];\n}\n\nexport function buildAssignFields(upcoming: CrewShift[]): FieldConfig[] {\n return [\n {\n name: 'shiftId',\n label: 'Shift',\n type: 'select',\n required: true,\n options: upcoming.map((shift) => ({ label: shiftOptionLabel(shift), value: shift.id })),\n },\n ];\n}\n\n/** Optional site coordinates from the form: both or neither, as numbers. */\nexport function readSite(\n values: FormValues\n): { siteLatitude?: number; siteLongitude?: number } | string {\n const lat = parseCoordinate(getText(values, 'siteLatitude'));\n const lng = parseCoordinate(getText(values, 'siteLongitude'));\n if (lat === null && lng === null) return {};\n if (lat === null || lng === null || Number.isNaN(lat) || Number.isNaN(lng)) {\n return 'Enter both site latitude and longitude as numbers, or leave both empty.';\n }\n return { siteLatitude: lat, siteLongitude: lng };\n}\n\nconst optional = (values: FormValues, key: string): string | null => getText(values, key) || null;\nconst selected = (values: FormValues, key: string): string | null => {\n const value = getText(values, key);\n return value && value !== NO_SELECTION ? value : null;\n};\n\n/** Map the schedule-shift form to the mutation input, or a validation message. */\nexport function toShiftInput(values: FormValues): ShiftInput | string {\n const site = readSite(values);\n if (typeof site === 'string') return site;\n const startsAt = localInputToIso(getText(values, 'startsAt'));\n const endsAt = localInputToIso(getText(values, 'endsAt'));\n if (!startsAt || !endsAt) return 'Enter a valid start and end.';\n const radius = getNumber(values, 'geofenceRadiusMeters');\n const reminder = values.reminderMinutesBefore;\n return {\n employeeId: getText(values, 'employeeId'),\n assigneeUserId: selected(values, 'assigneeUserId'),\n title: getText(values, 'title'),\n role: optional(values, 'role'),\n siteName: optional(values, 'siteName'),\n siteAddress: optional(values, 'siteAddress'),\n ...site,\n geofenceRadiusMeters: radius > 0 ? radius : undefined,\n reminderMinutesBefore: typeof reminder === 'number' ? reminder : undefined,\n startsAt,\n endsAt,\n notes: optional(values, 'notes'),\n };\n}\n\n/** Map the new-work-order form to the mutation input, or a validation message. */\nexport function toDispatchInput(values: FormValues): DispatchInput | string {\n const site = readSite(values);\n if (typeof site === 'string') return site;\n return {\n title: getText(values, 'title'),\n priority: (getText(values, 'priority') || 'NORMAL') as DispatchPriority,\n dueAt: localInputToIso(getText(values, 'dueAt')),\n shiftId: selected(values, 'shiftId'),\n siteName: optional(values, 'siteName'),\n siteAddress: optional(values, 'siteAddress'),\n ...site,\n description: optional(values, 'description'),\n };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export type FormFieldType = 'text' | 'number' | 'date' | 'email' | 'textarea' | 'select' | 'tags' | 'switch';
1
+ export type FormFieldType = 'text' | 'number' | 'date' | 'datetime' | 'email' | 'textarea' | 'select' | 'tags' | 'switch';
2
2
  export type FormValue = string | number | boolean | string[];
3
3
  export type FormValues = Record<string, FormValue>;
4
4
  export interface FieldOption {
@@ -1 +1 @@
1
- {"version":3,"file":"FormModal.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC;AAC7D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,wFAAwF;IACxF,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChB;;yCAEqC;IACrC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;iDAG6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;6EAGyE;IACzE,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AA4ED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,aAAa,EACb,WAAW,EACX,QAAQ,GACT,EAAE,cAAc,+BAgThB;AAGD,MAAM,WAAW,kBAAkB,CAAC,QAAQ;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,GAAG,IAAI,KAAK,kBAAkB,CAAC,QAAQ,CAAC,CAmB5E"}
1
+ {"version":3,"file":"FormModal.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,GACV,QAAQ,GACR,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC;AAC7D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,wFAAwF;IACxF,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChB;;yCAEqC;IACrC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;iDAG6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;6EAGyE;IACzE,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AA4ED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,aAAa,EACb,WAAW,EACX,QAAQ,GACT,EAAE,cAAc,+BAkThB;AAGD,MAAM,WAAW,kBAAkB,CAAC,QAAQ;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,GAAG,IAAI,KAAK,kBAAkB,CAAC,QAAQ,CAAC,CAmB5E"}
@@ -190,7 +190,7 @@ function D({ open: D, onOpenChange: O, title: k, description: A, errorMessage: j
190
190
  })]
191
191
  }) : /* @__PURE__ */ c(d, {
192
192
  id: t,
193
- type: e.type === "number" ? "number" : e.type === "date" ? "date" : e.type === "email" ? "email" : "text",
193
+ type: e.type === "number" ? "number" : e.type === "date" ? "date" : e.type === "datetime" ? "datetime-local" : e.type === "email" ? "email" : "text",
194
194
  min: e.min,
195
195
  max: e.max,
196
196
  value: typeof r == "string" ? r : "",
@@ -1 +1 @@
1
- {"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { AlertCircle, X } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n /**\n * Optional dynamic-required predicate. When provided, the field's `required`\n * state at runtime is `(required || requiredWhen(values))`. Both the `*`\n * marker AND the built-in required-field check (when the form is\n * submitted) honor this rule. Existing forms that don't set it see no\n * change — purely opt-in.\n */\n requiredWhen?: (values: FormValues) => boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n /** Lower bound for `date` / `number` inputs — the browser will not let the\n * user pick a value below this. Pass a YYYY-MM-DD string for date fields.\n * Ignored for other field types. */\n min?: string | number;\n /** Upper bound for `date` / `number` inputs — symmetric to `min`. */\n max?: string | number;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n /** Optional inline error banner shown above the form fields. Pages should\n * pass the first validation message so the user sees feedback inside the\n * modal — useful while `useGlobalUi().showToast` has no rendered viewport\n * (shared provider bug — see BOFF-7588). */\n errorMessage?: string;\n /** Fired when this FormModal's own required-field check rejects a submit\n * (the consumer can mirror it into `errorMessage` or its own state).\n * The FormModal still toasts (and silently, while the provider bug stands)\n * and early-returns so onSubmit is not invoked — opt-in duplication. */\n onValidationError?: (message: string) => void;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\n/** Whether this field is required at this moment, given the current values.\n * Combines the static `required` flag with the optional `requiredWhen`\n * predicate so the `*` marker and the blocking submit check stay in sync.\n */\nconst isRequiredAt = (field: FieldConfig, values: FormValues): boolean => {\n if (field.required) return true;\n if (field.requiredWhen?.(values)) return true;\n return false;\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n errorMessage,\n onValidationError,\n fields,\n initialValues,\n submitLabel,\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const tr = useTr();\n const resolvedSubmitLabel = submitLabel ?? tr('formModal.submitLabel', 'Save');\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => isRequiredAt(field, staged) && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n const message = tr('formModal.fieldRequired', '{{label}} is required', {\n label: missing.label,\n });\n showToast(message, 'warning');\n onValidationError?.(message);\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(\n error instanceof Error\n ? error.message\n : tr('formModal.submitError', 'Something went wrong'),\n 'error'\n );\n } finally {\n setSubmitting(false);\n }\n },\n [\n fields,\n onOpenChange,\n onSubmit,\n showToast,\n submitting,\n tagDrafts,\n tr,\n values,\n onValidationError,\n ]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-input border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden />\n <span>{errorMessage}</span>\n </div>\n ) : null}\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? tr('formModal.selectPlaceholder', 'Select…')}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={tr('formModal.removeTag', 'Remove {{tag}}', { tag })}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={\n field.placeholder ??\n tr('formModal.tagsPlaceholder', 'Add and press Enter')\n }\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n min={field.min}\n max={field.max}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n {tr('formModal.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? tr('formModal.saving', 'Saving…') : resolvedSubmitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":";;;;;;;AA0FA,IAAM,KAAqB,GAAuB,MAA+C;CAC/F,IAAM,IAAwB,EAAE;AAChC,MAAK,IAAM,KAAS,GAAQ;EAC1B,IAAM,IAAM,IAAgB,EAAM;AAClC,EAAI,EAAM,SAAS,WACjB,EAAM,EAAM,QAAQ,EAAQ,IACnB,EAAM,SAAS,SACpB,MAAM,QAAQ,EAAI,GACpB,EAAM,EAAM,QAAQ,EAAI,KAAK,MAAS,OAAO,EAAK,CAAC,GAC1C,OAAO,KAAQ,YAAY,EAAI,SAAS,IACjD,EAAM,EAAM,QAAQ,EACjB,MAAM,IAAI,CACV,KAAK,MAAS,EAAK,MAAM,CAAC,CAC1B,OAAO,QAAQ,GAElB,EAAM,EAAM,QAAQ,EAAE,GAGxB,EAAM,EAAM,QAAQ,KAA6B,OAAO,KAAK,OAAO,EAAI;;AAG5E,QAAO;GAGH,KAAgB,GAAoB,MACpC,EAAM,SAAS,WACV,KAEL,EAAM,SAAS,SACV,CAAC,MAAM,QAAQ,EAAM,IAAI,EAAM,WAAW,IAE5C,OAAO,KAAU,YAAY,EAAM,MAAM,KAAK,IAOjD,KAAgB,GAAoB,MAExC,GADI,EAAM,YACN,EAAM,eAAe,EAAO,GAI5B,KAAY,GAAuB,MAAsC;CAC7E,IAAM,IAAqB,EAAE;AAC7B,MAAK,IAAM,KAAS,GAAQ;EAC1B,IAAM,IAAQ,EAAM,EAAM;AAC1B,MAAI,EAAM,SAAS,SACjB,GAAO,EAAM,QAAQ,EAAQ;WACpB,EAAM,SAAS,OACxB,GAAO,EAAM,QAAQ,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE;WAC7C,EAAM,SAAS,UAAU;GAClC,IAAM,IAAM,OAAO,KAAU,WAAW,EAAM,MAAM,GAAG;AACvD,KAAO,EAAM,QAAQ,MAAQ,KAAK,KAAK,OAAO,EAAI;QAElD,GAAO,EAAM,QAAQ,OAAO,KAAU,WAAW,IAAQ;;AAG7D,QAAO;GAIH,IACJ;AAGF,SAAgB,EAAU,EACxB,SACA,iBACA,UACA,gBACA,iBACA,sBACA,WACA,kBACA,gBACA,eACiB;CACjB,IAAM,EAAE,iBAAc,GAAa,EAC7B,IAAK,GAAO,EACZ,IAAsB,KAAe,EAAG,yBAAyB,OAAO,EACxE,CAAC,GAAQ,KAAa,QAC1B,EAAkB,GAAQ,EAAc,CACzC,EACK,CAAC,GAAW,KAAgB,EAAiC,EAAE,CAAC,EAChE,CAAC,GAAY,KAAiB,EAAS,GAAM,EAK7C,IAAY,EAAO,EAAO,EAC1B,IAAmB,EAAO,EAAc,EACxC,IAAa,EAAO,GAAM;AAYhC,CAPA,QAAgB;AAEd,EADA,EAAU,UAAU,GACpB,EAAiB,UAAU;IAC1B,CAAC,GAAQ,EAAc,CAAC,EAI3B,QAAgB;AAMd,EALI,KAAQ,CAAC,EAAW,YACtB,EAAU,EAAkB,EAAU,SAAS,EAAiB,QAAQ,CAAC,EACzE,EAAa,EAAE,CAAC,EAChB,EAAc,GAAM,GAEtB,EAAW,UAAU;IACpB,CAAC,EAAK,CAAC;CAEV,IAAM,IAAgB,GAAa,GAAc,MAAyB;AACxE,KAAW,OAAU;GAAE,GAAG;IAAO,IAAO;GAAO,EAAE;IAChD,EAAE,CAAC,EAEA,IAAY,GAAa,MAAiB;AAC9C,KAAc,MAAe;GAC3B,IAAM,KAAS,EAAW,MAAS,IAAI,MAAM;AAU7C,UATI,KACF,GAAW,MAAS;IAClB,IAAM,IAAU,MAAM,QAAQ,EAAK,GAAM,GAAI,EAAK,KAAqB,EAAE;AAIzE,WAHI,EAAQ,SAAS,EAAM,GAClB,IAEF;KAAE,GAAG;MAAO,IAAO,CAAC,GAAG,GAAS,EAAM;KAAE;KAC/C,EAEG;IAAE,GAAG;KAAa,IAAO;IAAI;IACpC;IACD,EAAE,CAAC,EAEA,IAAY,GAAa,GAAc,MAAgB;AAC3D,KAAW,MAAS;GAClB,IAAM,IAAU,MAAM,QAAQ,EAAK,GAAM,GAAI,EAAK,KAAqB,EAAE;AACzE,UAAO;IAAE,GAAG;KAAO,IAAO,EAAQ,QAAQ,MAAS,MAAS,EAAI;IAAE;IAClE;IACD,EAAE,CAAC,EAEA,IAAe,EACnB,OAAO,MAA4C;AAEjD,MADA,EAAM,gBAAgB,EAClB,EACF;EAIF,IAAM,IAAyB,EAAE,GAAG,GAAQ;AAC5C,OAAK,IAAM,KAAS,EAClB,KAAI,EAAM,SAAS,QAAQ;GACzB,IAAM,KAAS,EAAU,EAAM,SAAS,IAAI,MAAM;AAClD,OAAI,GAAO;IACT,IAAM,IAAU,MAAM,QAAQ,EAAO,EAAM,MAAM,GAC5C,EAAO,EAAM,QACd,EAAE;AACN,IAAK,EAAQ,SAAS,EAAM,KAC1B,EAAO,EAAM,QAAQ,CAAC,GAAG,GAAS,EAAM;;;EAMhD,IAAM,IAAU,EAAO,MACpB,MAAU,EAAa,GAAO,EAAO,IAAI,EAAa,GAAO,EAAO,EAAM,MAAM,CAClF;AACD,MAAI,GAAS;GACX,IAAM,IAAU,EAAG,2BAA2B,yBAAyB,EACrE,OAAO,EAAQ,OAChB,CAAC;AAEF,GADA,EAAU,GAAS,UAAU,EAC7B,IAAoB,EAAQ;AAC5B;;AAGF,IAAc,GAAK;AACnB,MAAI;AAEF,GADA,MAAM,EAAS,EAAS,GAAQ,EAAO,CAAC,EACxC,EAAa,GAAM;WACZ,GAAO;AACd,KACE,aAAiB,QACb,EAAM,UACN,EAAG,yBAAyB,uBAAuB,EACvD,QACD;YACO;AACR,KAAc,GAAM;;IAGxB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,GAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD;GAAyB,WAAU;aAAnC,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAwB,GAA8B,CAAA,EACrD,IACC,kBAAC,GAAD,EAAA,UAA8B,GAA0C,CAAA,GACtE,KACmB,EAAA,CAAA,EAEzB,kBAAC,QAAD;IAAM,UAAU;IAAc,WAAU;cAAxC;KACG,IACC,kBAAC,OAAD;MACE,MAAK;MACL,WAAU;gBAFZ,CAIE,kBAAC,GAAD;OAAa,WAAU;OAA0B,eAAA;OAAc,CAAA,EAC/D,kBAAC,QAAD,EAAA,UAAO,GAAoB,CAAA,CACvB;UACJ;KACJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAO,KAAK,MAAU;OACrB,IAAM,IAAU,aAAa,EAAM,QAC7B,IACJ,EAAM,YAAY,KACjB,EAAM,YAAY,KAAA,MAChB,EAAM,SAAS,cAAc,EAAM,SAAS,SAC3C,IAAQ,EAAO,EAAM;AAE3B,cACE,kBAAC,OAAD;QAEE,WAAW,yBAAyB,IAAU,kBAAkB;kBAE/D,EAAM,SAAS,WACd,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAS;oBAAhB,CACG,EAAM,OACN,EAAa,GAAO,EAAO,GAC1B,kBAAC,QAAD;WAAM,WAAU;qBAAuB;WAAS,CAAA,GAC9C,KACE;aACR,kBAAC,GAAD;UACE,IAAI;UACJ,SAAS,EAAQ;UACjB,kBAAkB,MAAY,EAAc,EAAM,MAAM,EAAQ;UAChE,UAAU;UACV,CAAA,CACE;aAEN,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;SAAO,SAAS;mBAAhB,CACG,EAAM,OACN,EAAa,GAAO,EAAO,GAC1B,kBAAC,QAAD;UAAM,WAAU;oBAAuB;UAAS,CAAA,GAC9C,KACE;YAEP,EAAM,SAAS,aACd,kBAAC,GAAD;SACE,IAAI;SACJ,OAAO,OAAO,KAAU,WAAW,IAAQ;SAC3C,aAAa,EAAM;SACnB,UAAU;SACV,WAAW,MAAU,EAAc,EAAM,MAAM,EAAM,OAAO,MAAM;SAClE,CAAA,GACA,EAAM,SAAS,WACjB,kBAAC,UAAD;SACE,IAAI;SACJ,WAAW;SACX,OAAO,OAAO,KAAU,WAAW,IAAQ;SAC3C,UAAU;SACV,WAAW,MAAU,EAAc,EAAM,MAAM,EAAM,OAAO,MAAM;mBALpE,CAOE,kBAAC,UAAD;UAAQ,OAAM;UAAG,UAAA;oBACd,EAAM,eAAe,EAAG,+BAA+B,UAAU;UAC3D,CAAA,GACP,EAAM,WAAW,EAAE,EAAE,KAAK,MAC1B,kBAAC,UAAD;UAA2B,OAAO,EAAO;oBACtC,EAAO;UACD,EAFI,EAAO,MAEX,CACT,CACK;aACP,EAAM,SAAS,SACjB,kBAAC,OAAD;SAAK,WAAU;mBAAf,EACI,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE,EAAE,KAAK,MACxC,kBAAC,QAAD;UAEE,WAAU;oBAFZ,CAIG,GACD,kBAAC,UAAD;WACE,MAAK;WACL,cAAY,EAAG,uBAAuB,kBAAkB,EAAE,QAAK,CAAC;WAChE,WAAU;WACV,eAAe,EAAU,EAAM,MAAM,EAAI;WACzC,UAAU;qBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;WAClB,CAAA,CACJ;YAbA,EAaA,CACP,EACF,kBAAC,SAAD;UACE,IAAI;UACJ,WAAU;UACV,OAAO,EAAU,EAAM,SAAS;UAChC,aACE,EAAM,eACN,EAAG,6BAA6B,sBAAsB;UAExD,UAAU;UACV,WAAW,MACT,GAAc,OAAU;WACtB,GAAG;YACF,EAAM,OAAO,EAAM,OAAO;WAC5B,EAAE;UAEL,YAAY,MAAU;AACpB,eAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,IAEzC,CADA,EAAM,gBAAgB,EACtB,EAAU,EAAM,KAAK;oBAErB,EAAM,QAAQ,gBACb,EAAU,EAAM,SAAS,QAAQ,IAClC;YACA,IAAM,IAAU,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE;AACjD,YAAI,EAAQ,SAAS,KACnB,EAAU,EAAM,MAAM,EAAQ,EAAQ,SAAS,GAAG;;;UAIxD,cAAc,EAAU,EAAM,KAAK;UACnC,CAAA,CACE;aAEN,kBAAC,GAAD;SACE,IAAI;SACJ,MACE,EAAM,SAAS,WACX,WACA,EAAM,SAAS,SACb,SACA,EAAM,SAAS,UACb,UACA;SAEV,KAAK,EAAM;SACX,KAAK,EAAM;SACX,OAAO,OAAO,KAAU,WAAW,IAAQ;SAC3C,aAAa,EAAM;SACnB,UAAU;SACV,WAAW,MAAU,EAAc,EAAM,MAAM,EAAM,OAAO,MAAM;SAClE,CAAA,CAEH,EAAA,CAAA;QAED,EA7HC,EAAM,KA6HP;QAER;MACE,CAAA;KAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MACE,MAAK;MACL,SAAQ;MACR,eAAe,EAAa,GAAM;MAClC,UAAU;gBAET,EAAG,oBAAoB,SAAS;MAC1B,CAAA,EACT,kBAAC,GAAD;MAAQ,MAAK;MAAS,UAAU;gBAC7B,IAAa,EAAG,oBAAoB,UAAU,GAAG;MAC3C,CAAA,CACc,EAAA,CAAA;KACpB;MACiB;;EACT,CAAA;;AAkBvB,SAAgB,IAA8D;CAC5E,IAAM,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAS,KAAc,EAA0B,KAAK;AAgB7D,QAAO;EAAE;EAAM;EAAS,WAdN,GAAa,MAAoB;AAEjD,GADA,EAAW,KAAQ,KAAK,EACxB,EAAQ,GAAK;KACZ,EAAE,CAAC;EAW6B,YAThB,QAAkB,EAAQ,GAAM,EAAE,EAAE,CAAC;EAST,cAP1B,GAAa,MAAsB;AAEtD,GADA,EAAQ,EAAS,EACZ,KACH,EAAW,KAAK;KAEjB,EAAE,CAAC;EAEuD"}
1
+ {"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { AlertCircle, X } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'datetime'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n /**\n * Optional dynamic-required predicate. When provided, the field's `required`\n * state at runtime is `(required || requiredWhen(values))`. Both the `*`\n * marker AND the built-in required-field check (when the form is\n * submitted) honor this rule. Existing forms that don't set it see no\n * change — purely opt-in.\n */\n requiredWhen?: (values: FormValues) => boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n /** Lower bound for `date` / `number` inputs — the browser will not let the\n * user pick a value below this. Pass a YYYY-MM-DD string for date fields.\n * Ignored for other field types. */\n min?: string | number;\n /** Upper bound for `date` / `number` inputs — symmetric to `min`. */\n max?: string | number;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n /** Optional inline error banner shown above the form fields. Pages should\n * pass the first validation message so the user sees feedback inside the\n * modal — useful while `useGlobalUi().showToast` has no rendered viewport\n * (shared provider bug — see BOFF-7588). */\n errorMessage?: string;\n /** Fired when this FormModal's own required-field check rejects a submit\n * (the consumer can mirror it into `errorMessage` or its own state).\n * The FormModal still toasts (and silently, while the provider bug stands)\n * and early-returns so onSubmit is not invoked — opt-in duplication. */\n onValidationError?: (message: string) => void;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\n/** Whether this field is required at this moment, given the current values.\n * Combines the static `required` flag with the optional `requiredWhen`\n * predicate so the `*` marker and the blocking submit check stay in sync.\n */\nconst isRequiredAt = (field: FieldConfig, values: FormValues): boolean => {\n if (field.required) return true;\n if (field.requiredWhen?.(values)) return true;\n return false;\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n errorMessage,\n onValidationError,\n fields,\n initialValues,\n submitLabel,\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const tr = useTr();\n const resolvedSubmitLabel = submitLabel ?? tr('formModal.submitLabel', 'Save');\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => isRequiredAt(field, staged) && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n const message = tr('formModal.fieldRequired', '{{label}} is required', {\n label: missing.label,\n });\n showToast(message, 'warning');\n onValidationError?.(message);\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(\n error instanceof Error\n ? error.message\n : tr('formModal.submitError', 'Something went wrong'),\n 'error'\n );\n } finally {\n setSubmitting(false);\n }\n },\n [\n fields,\n onOpenChange,\n onSubmit,\n showToast,\n submitting,\n tagDrafts,\n tr,\n values,\n onValidationError,\n ]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-input border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden />\n <span>{errorMessage}</span>\n </div>\n ) : null}\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? tr('formModal.selectPlaceholder', 'Select…')}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={tr('formModal.removeTag', 'Remove {{tag}}', { tag })}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={\n field.placeholder ??\n tr('formModal.tagsPlaceholder', 'Add and press Enter')\n }\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'datetime'\n ? 'datetime-local'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n min={field.min}\n max={field.max}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n {tr('formModal.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? tr('formModal.saving', 'Saving…') : resolvedSubmitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Field-operations helpers (BOFF-4205): types mirroring the
3
+ * wspace-crewfoundry-svc field-ops schema, the worker-side lifecycle rules,
4
+ * and a promise wrapper over the device geolocation API.
5
+ *
6
+ * Geolocation uses the standard `navigator.geolocation`, which works in the
7
+ * browser and inside the Capacitor shell's WebView (the shell declares the
8
+ * location permission), so the MFE needs no native import.
9
+ */
10
+ export type ShiftStatus = 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED' | 'MISSED';
11
+ export type DispatchStatus = 'OPEN' | 'ASSIGNED' | 'EN_ROUTE' | 'ON_SITE' | 'COMPLETED' | 'CANCELLED';
12
+ export type DispatchPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
13
+ export interface CrewShift {
14
+ id: string;
15
+ employeeId: string;
16
+ employeeName: string;
17
+ assigneeUserId: string | null;
18
+ title: string;
19
+ role: string | null;
20
+ siteName: string | null;
21
+ siteAddress: string | null;
22
+ siteLatitude: number | null;
23
+ siteLongitude: number | null;
24
+ geofenceRadiusMeters: number;
25
+ startsAt: string;
26
+ endsAt: string;
27
+ status: ShiftStatus;
28
+ notes: string | null;
29
+ reminderMinutesBefore: number;
30
+ reminderSentAt: string | null;
31
+ checkedInAt: string | null;
32
+ checkedOutAt: string | null;
33
+ checkInDistanceMeters: number | null;
34
+ checkOutDistanceMeters: number | null;
35
+ cancelReason: string | null;
36
+ timeEntry: {
37
+ id: string;
38
+ totalHours: number;
39
+ overtime: number;
40
+ geoVerified?: boolean;
41
+ } | null;
42
+ }
43
+ export interface CrewDispatch {
44
+ id: string;
45
+ title: string;
46
+ description: string | null;
47
+ priority: DispatchPriority;
48
+ status: DispatchStatus;
49
+ siteName: string | null;
50
+ siteAddress: string | null;
51
+ siteLatitude: number | null;
52
+ siteLongitude: number | null;
53
+ shiftId: string | null;
54
+ employeeId: string | null;
55
+ employeeName: string | null;
56
+ assigneeUserId: string | null;
57
+ dueAt: string | null;
58
+ assignedAt: string | null;
59
+ enRouteAt: string | null;
60
+ arrivedAt: string | null;
61
+ completedAt: string | null;
62
+ completionNotes: string | null;
63
+ createdAt: string;
64
+ }
65
+ export interface GeoReading {
66
+ latitude: number;
67
+ longitude: number;
68
+ accuracyMeters: number | null;
69
+ }
70
+ /** Same window the backend enforces (CHECK_IN_EARLY_MINUTES). */
71
+ export declare const CHECK_IN_EARLY_MINUTES = 30;
72
+ /** Work-order moves a field worker may make on their own job (mirrors the service). */
73
+ export declare const WORKER_NEXT_STATUS: Record<DispatchStatus, DispatchStatus[]>;
74
+ /** Work-order moves a dispatcher may make (ASSIGNED is entered through Assign). */
75
+ export declare const MANAGER_NEXT_STATUS: Record<DispatchStatus, DispatchStatus[]>;
76
+ export declare const STATUS_ACTION_LABEL: Record<DispatchStatus, string>;
77
+ export type CheckInAvailability = {
78
+ kind: 'check-in';
79
+ } | {
80
+ kind: 'check-out';
81
+ } | {
82
+ kind: 'too-early';
83
+ opensAt: Date;
84
+ } | {
85
+ kind: 'closed';
86
+ };
87
+ /** What the worker can do with a shift right now. */
88
+ export declare function checkInAvailability(shift: CrewShift, now?: Date): CheckInAvailability;
89
+ /** Human-readable distance: metres under 1 km, kilometres above. */
90
+ export declare function formatDistance(meters: number | null | undefined): string;
91
+ /** Shifts that overlap the local calendar day of `day`. */
92
+ export declare function shiftsOnDay(shifts: CrewShift[], day: Date): CrewShift[];
93
+ /** `datetime-local` input value (local time, minutes) → ISO-8601 UTC string. */
94
+ export declare function localInputToIso(value: string): string | null;
95
+ /** ISO-8601 → `datetime-local` input value in the viewer's local time. */
96
+ export declare function isoToLocalInput(iso: string | null | undefined): string;
97
+ /** Parse an optional coordinate typed into a form; empty → null, junk → NaN. */
98
+ export declare function parseCoordinate(value: string): number | null;
99
+ export declare class GeolocationUnavailableError extends Error {
100
+ }
101
+ /** Translate a GeolocationPositionError code into a message a worker can act on. */
102
+ export declare function geolocationErrorMessage(code: number): string;
103
+ /** One high-accuracy position fix from the device. */
104
+ export declare function readDevicePosition(geolocation?: Geolocation | undefined, timeoutMs?: number): Promise<GeoReading>;
105
+ //# sourceMappingURL=fieldOps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldOps.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/fieldOps.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ var e = {
2
+ OPEN: [],
3
+ ASSIGNED: ["EN_ROUTE", "ON_SITE"],
4
+ EN_ROUTE: ["ON_SITE"],
5
+ ON_SITE: ["COMPLETED"],
6
+ COMPLETED: [],
7
+ CANCELLED: []
8
+ }, t = {
9
+ OPEN: ["CANCELLED"],
10
+ ASSIGNED: [
11
+ "OPEN",
12
+ "EN_ROUTE",
13
+ "ON_SITE",
14
+ "CANCELLED"
15
+ ],
16
+ EN_ROUTE: [
17
+ "ASSIGNED",
18
+ "ON_SITE",
19
+ "CANCELLED"
20
+ ],
21
+ ON_SITE: ["COMPLETED", "CANCELLED"],
22
+ COMPLETED: [],
23
+ CANCELLED: []
24
+ }, n = {
25
+ OPEN: "Unassign",
26
+ ASSIGNED: "Back to assigned",
27
+ EN_ROUTE: "On my way",
28
+ ON_SITE: "Arrived on site",
29
+ COMPLETED: "Complete",
30
+ CANCELLED: "Cancel"
31
+ };
32
+ function r(e, t = /* @__PURE__ */ new Date()) {
33
+ if (e.status === "IN_PROGRESS") return { kind: "check-out" };
34
+ if (e.status !== "SCHEDULED") return { kind: "closed" };
35
+ let n = /* @__PURE__ */ new Date(new Date(e.startsAt).getTime() - 30 * 6e4);
36
+ return t.getTime() < n.getTime() ? {
37
+ kind: "too-early",
38
+ opensAt: n
39
+ } : t.getTime() > new Date(e.endsAt).getTime() ? { kind: "closed" } : { kind: "check-in" };
40
+ }
41
+ function i(e) {
42
+ return e == null || !Number.isFinite(e) ? "—" : e < 1e3 ? `${Math.round(e)} m` : `${(e / 1e3).toFixed(1)} km`;
43
+ }
44
+ function a(e, t) {
45
+ let n = new Date(t.getFullYear(), t.getMonth(), t.getDate()).getTime(), r = n + 24 * 36e5;
46
+ return e.filter((e) => {
47
+ let t = new Date(e.startsAt).getTime(), i = new Date(e.endsAt).getTime();
48
+ return t < r && i > n;
49
+ });
50
+ }
51
+ function o(e) {
52
+ if (!e) return null;
53
+ let t = new Date(e);
54
+ return Number.isNaN(t.getTime()) ? null : t.toISOString();
55
+ }
56
+ function s(e) {
57
+ let t = e.trim();
58
+ return t === "" ? null : Number(t);
59
+ }
60
+ var c = class extends Error {};
61
+ function l(e) {
62
+ switch (e) {
63
+ case 1: return "Location permission was denied. Allow location for CrewFoundry to check in.";
64
+ case 2: return "Your position could not be determined. Move to open sky and try again.";
65
+ case 3: return "Getting your location took too long. Try again.";
66
+ default: return "Could not read your location.";
67
+ }
68
+ }
69
+ function u(e = typeof navigator < "u" ? navigator.geolocation : void 0, t = 2e4) {
70
+ return e ? new Promise((n, r) => {
71
+ e.getCurrentPosition((e) => n({
72
+ latitude: e.coords.latitude,
73
+ longitude: e.coords.longitude,
74
+ accuracyMeters: Number.isFinite(e.coords.accuracy) ? e.coords.accuracy : null
75
+ }), (e) => r(new c(l(e.code))), {
76
+ enableHighAccuracy: !0,
77
+ timeout: t,
78
+ maximumAge: 0
79
+ });
80
+ }) : Promise.reject(new c("This device does not support location."));
81
+ }
82
+ //#endregion
83
+ export { t as MANAGER_NEXT_STATUS, n as STATUS_ACTION_LABEL, e as WORKER_NEXT_STATUS, r as checkInAvailability, i as formatDistance, o as localInputToIso, s as parseCoordinate, u as readDevicePosition, a as shiftsOnDay };
84
+
85
+ //# sourceMappingURL=fieldOps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldOps.js","names":[],"sources":["../../../src/crewfoundry/lib/fieldOps.ts"],"sourcesContent":["/**\n * Field-operations helpers (BOFF-4205): types mirroring the\n * wspace-crewfoundry-svc field-ops schema, the worker-side lifecycle rules,\n * and a promise wrapper over the device geolocation API.\n *\n * Geolocation uses the standard `navigator.geolocation`, which works in the\n * browser and inside the Capacitor shell's WebView (the shell declares the\n * location permission), so the MFE needs no native import.\n */\n\nexport type ShiftStatus = 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED' | 'MISSED';\nexport type DispatchStatus =\n | 'OPEN'\n | 'ASSIGNED'\n | 'EN_ROUTE'\n | 'ON_SITE'\n | 'COMPLETED'\n | 'CANCELLED';\nexport type DispatchPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';\n\nexport interface CrewShift {\n id: string;\n employeeId: string;\n employeeName: string;\n assigneeUserId: string | null;\n title: string;\n role: string | null;\n siteName: string | null;\n siteAddress: string | null;\n siteLatitude: number | null;\n siteLongitude: number | null;\n geofenceRadiusMeters: number;\n startsAt: string;\n endsAt: string;\n status: ShiftStatus;\n notes: string | null;\n reminderMinutesBefore: number;\n reminderSentAt: string | null;\n checkedInAt: string | null;\n checkedOutAt: string | null;\n checkInDistanceMeters: number | null;\n checkOutDistanceMeters: number | null;\n cancelReason: string | null;\n timeEntry: { id: string; totalHours: number; overtime: number; geoVerified?: boolean } | null;\n}\n\nexport interface CrewDispatch {\n id: string;\n title: string;\n description: string | null;\n priority: DispatchPriority;\n status: DispatchStatus;\n siteName: string | null;\n siteAddress: string | null;\n siteLatitude: number | null;\n siteLongitude: number | null;\n shiftId: string | null;\n employeeId: string | null;\n employeeName: string | null;\n assigneeUserId: string | null;\n dueAt: string | null;\n assignedAt: string | null;\n enRouteAt: string | null;\n arrivedAt: string | null;\n completedAt: string | null;\n completionNotes: string | null;\n createdAt: string;\n}\n\nexport interface GeoReading {\n latitude: number;\n longitude: number;\n accuracyMeters: number | null;\n}\n\n/** Same window the backend enforces (CHECK_IN_EARLY_MINUTES). */\nexport const CHECK_IN_EARLY_MINUTES = 30;\n\n/** Work-order moves a field worker may make on their own job (mirrors the service). */\nexport const WORKER_NEXT_STATUS: Record<DispatchStatus, DispatchStatus[]> = {\n OPEN: [],\n ASSIGNED: ['EN_ROUTE', 'ON_SITE'],\n EN_ROUTE: ['ON_SITE'],\n ON_SITE: ['COMPLETED'],\n COMPLETED: [],\n CANCELLED: [],\n};\n\n/** Work-order moves a dispatcher may make (ASSIGNED is entered through Assign). */\nexport const MANAGER_NEXT_STATUS: Record<DispatchStatus, DispatchStatus[]> = {\n OPEN: ['CANCELLED'],\n ASSIGNED: ['OPEN', 'EN_ROUTE', 'ON_SITE', 'CANCELLED'],\n EN_ROUTE: ['ASSIGNED', 'ON_SITE', 'CANCELLED'],\n ON_SITE: ['COMPLETED', 'CANCELLED'],\n COMPLETED: [],\n CANCELLED: [],\n};\n\nexport const STATUS_ACTION_LABEL: Record<DispatchStatus, string> = {\n OPEN: 'Unassign',\n ASSIGNED: 'Back to assigned',\n EN_ROUTE: 'On my way',\n ON_SITE: 'Arrived on site',\n COMPLETED: 'Complete',\n CANCELLED: 'Cancel',\n};\n\nexport type CheckInAvailability =\n | { kind: 'check-in' }\n | { kind: 'check-out' }\n | { kind: 'too-early'; opensAt: Date }\n | { kind: 'closed' };\n\n/** What the worker can do with a shift right now. */\nexport function checkInAvailability(shift: CrewShift, now: Date = new Date()): CheckInAvailability {\n if (shift.status === 'IN_PROGRESS') return { kind: 'check-out' };\n if (shift.status !== 'SCHEDULED') return { kind: 'closed' };\n const opensAt = new Date(new Date(shift.startsAt).getTime() - CHECK_IN_EARLY_MINUTES * 60_000);\n if (now.getTime() < opensAt.getTime()) return { kind: 'too-early', opensAt };\n if (now.getTime() > new Date(shift.endsAt).getTime()) return { kind: 'closed' };\n return { kind: 'check-in' };\n}\n\n/** Human-readable distance: metres under 1 km, kilometres above. */\nexport function formatDistance(meters: number | null | undefined): string {\n if (meters === null || meters === undefined || !Number.isFinite(meters)) return '—';\n if (meters < 1000) return `${Math.round(meters)} m`;\n return `${(meters / 1000).toFixed(1)} km`;\n}\n\n/** Shifts that overlap the local calendar day of `day`. */\nexport function shiftsOnDay(shifts: CrewShift[], day: Date): CrewShift[] {\n const start = new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime();\n const end = start + 24 * 3_600_000;\n return shifts.filter((shift) => {\n const s = new Date(shift.startsAt).getTime();\n const e = new Date(shift.endsAt).getTime();\n return s < end && e > start;\n });\n}\n\n/** `datetime-local` input value (local time, minutes) → ISO-8601 UTC string. */\nexport function localInputToIso(value: string): string | null {\n if (!value) return null;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date.toISOString();\n}\n\n/** ISO-8601 → `datetime-local` input value in the viewer's local time. */\nexport function isoToLocalInput(iso: string | null | undefined): string {\n if (!iso) return '';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '';\n const pad = (n: number) => String(n).padStart(2, '0');\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(\n date.getHours()\n )}:${pad(date.getMinutes())}`;\n}\n\n/** Parse an optional coordinate typed into a form; empty → null, junk → NaN. */\nexport function parseCoordinate(value: string): number | null {\n const trimmed = value.trim();\n if (trimmed === '') return null;\n return Number(trimmed);\n}\n\nexport class GeolocationUnavailableError extends Error {}\n\n/** Translate a GeolocationPositionError code into a message a worker can act on. */\nexport function geolocationErrorMessage(code: number): string {\n switch (code) {\n case 1:\n return 'Location permission was denied. Allow location for CrewFoundry to check in.';\n case 2:\n return 'Your position could not be determined. Move to open sky and try again.';\n case 3:\n return 'Getting your location took too long. Try again.';\n default:\n return 'Could not read your location.';\n }\n}\n\n/** One high-accuracy position fix from the device. */\nexport function readDevicePosition(\n geolocation: Geolocation | undefined = typeof navigator !== 'undefined'\n ? navigator.geolocation\n : undefined,\n timeoutMs = 20_000\n): Promise<GeoReading> {\n if (!geolocation) {\n return Promise.reject(\n new GeolocationUnavailableError('This device does not support location.')\n );\n }\n return new Promise((resolve, reject) => {\n geolocation.getCurrentPosition(\n (position) =>\n resolve({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracyMeters: Number.isFinite(position.coords.accuracy)\n ? position.coords.accuracy\n : null,\n }),\n (error) => reject(new GeolocationUnavailableError(geolocationErrorMessage(error.code))),\n { enableHighAccuracy: true, timeout: timeoutMs, maximumAge: 0 }\n );\n });\n}\n"],"mappings":"AA+EA,IAAa,IAA+D;CAC1E,MAAM,EAAE;CACR,UAAU,CAAC,YAAY,UAAU;CACjC,UAAU,CAAC,UAAU;CACrB,SAAS,CAAC,YAAY;CACtB,WAAW,EAAE;CACb,WAAW,EAAE;CACd,EAGY,IAAgE;CAC3E,MAAM,CAAC,YAAY;CACnB,UAAU;EAAC;EAAQ;EAAY;EAAW;EAAY;CACtD,UAAU;EAAC;EAAY;EAAW;EAAY;CAC9C,SAAS,CAAC,aAAa,YAAY;CACnC,WAAW,EAAE;CACb,WAAW,EAAE;CACd,EAEY,IAAsD;CACjE,MAAM;CACN,UAAU;CACV,UAAU;CACV,SAAS;CACT,WAAW;CACX,WAAW;CACZ;AASD,SAAgB,EAAoB,GAAkB,oBAAY,IAAI,MAAM,EAAuB;AACjG,KAAI,EAAM,WAAW,cAAe,QAAO,EAAE,MAAM,aAAa;AAChE,KAAI,EAAM,WAAW,YAAa,QAAO,EAAE,MAAM,UAAU;CAC3D,IAAM,oBAAU,IAAI,KAAK,IAAI,KAAK,EAAM,SAAS,CAAC,SAAS,GAAA,KAA4B,IAAO;AAG9F,QAFI,EAAI,SAAS,GAAG,EAAQ,SAAS,GAAS;EAAE,MAAM;EAAa;EAAS,GACxE,EAAI,SAAS,GAAG,IAAI,KAAK,EAAM,OAAO,CAAC,SAAS,GAAS,EAAE,MAAM,UAAU,GACxE,EAAE,MAAM,YAAY;;AAI7B,SAAgB,EAAe,GAA2C;AAGxE,QAFI,KAAW,QAAgC,CAAC,OAAO,SAAS,EAAO,GAAS,MAC5E,IAAS,MAAa,GAAG,KAAK,MAAM,EAAO,CAAC,MACzC,IAAI,IAAS,KAAM,QAAQ,EAAE,CAAC;;AAIvC,SAAgB,EAAY,GAAqB,GAAwB;CACvE,IAAM,IAAQ,IAAI,KAAK,EAAI,aAAa,EAAE,EAAI,UAAU,EAAE,EAAI,SAAS,CAAC,CAAC,SAAS,EAC5E,IAAM,IAAQ,KAAK;AACzB,QAAO,EAAO,QAAQ,MAAU;EAC9B,IAAM,IAAI,IAAI,KAAK,EAAM,SAAS,CAAC,SAAS,EACtC,IAAI,IAAI,KAAK,EAAM,OAAO,CAAC,SAAS;AAC1C,SAAO,IAAI,KAAO,IAAI;GACtB;;AAIJ,SAAgB,EAAgB,GAA8B;AAC5D,KAAI,CAAC,EAAO,QAAO;CACnB,IAAM,IAAO,IAAI,KAAK,EAAM;AAC5B,QAAO,OAAO,MAAM,EAAK,SAAS,CAAC,GAAG,OAAO,EAAK,aAAa;;AAejE,SAAgB,EAAgB,GAA8B;CAC5D,IAAM,IAAU,EAAM,MAAM;AAE5B,QADI,MAAY,KAAW,OACpB,OAAO,EAAQ;;AAGxB,IAAa,IAAb,cAAiD,MAAM;AAGvD,SAAgB,EAAwB,GAAsB;AAC5D,SAAQ,GAAR;EACE,KAAK,EACH,QAAO;EACT,KAAK,EACH,QAAO;EACT,KAAK,EACH,QAAO;EACT,QACE,QAAO;;;AAKb,SAAgB,EACd,IAAuC,OAAO,YAAc,MACxD,UAAU,cACV,KAAA,GACJ,IAAY,KACS;AAMrB,QALK,IAKE,IAAI,SAAS,GAAS,MAAW;AACtC,IAAY,oBACT,MACC,EAAQ;GACN,UAAU,EAAS,OAAO;GAC1B,WAAW,EAAS,OAAO;GAC3B,gBAAgB,OAAO,SAAS,EAAS,OAAO,SAAS,GACrD,EAAS,OAAO,WAChB;GACL,CAAC,GACH,MAAU,EAAO,IAAI,EAA4B,EAAwB,EAAM,KAAK,CAAC,CAAC,EACvF;GAAE,oBAAoB;GAAM,SAAS;GAAW,YAAY;GAAG,CAChE;GACD,GAjBO,QAAQ,OACb,IAAI,EAA4B,yCAAyC,CAC1E"}
@@ -0,0 +1,14 @@
1
+ /** Field data goes stale as workers check in elsewhere; refresh this often while visible. */
2
+ export declare const FIELD_OPS_REFRESH_MS = 60000;
3
+ /**
4
+ * Load field-operations server state for the current workspace + actor:
5
+ * fetches on mount and whenever the context changes, refreshes every minute
6
+ * while the tab is visible, and reports `ready` only once rows for THIS
7
+ * context are in the store (never the previous workspace's or user's rows).
8
+ */
9
+ export declare function useFieldOpsData(load: () => Promise<void>, loadedKey: string | null, onError: (message: string) => void): {
10
+ ready: boolean;
11
+ loadFailed: boolean;
12
+ refresh: () => void;
13
+ };
14
+ //# sourceMappingURL=useFieldOpsData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFieldOpsData.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/useFieldOpsData.ts"],"names":[],"mappings":"AAGA,6FAA6F;AAC7F,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EACzB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACjC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAiC9D"}
@@ -0,0 +1,31 @@
1
+ import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
2
+ import { useCallback as t, useEffect as n, useState as r } from "react";
3
+ //#region src/crewfoundry/lib/useFieldOpsData.ts
4
+ var i = 6e4;
5
+ function a(a, o, s) {
6
+ let c = e((e) => e.initialized), l = e((e) => `${e.initParams.workspaceId ?? ""}:${e.initParams.actorId ?? ""}`), [u, d] = r(!1), f = t((e) => {
7
+ a().then(() => d(!1)).catch((t) => {
8
+ d(!0), e || s(t instanceof Error ? t.message : "Unable to load field operations");
9
+ });
10
+ }, [a, s]), p = t(() => f(!1), [f]);
11
+ return n(() => {
12
+ if (!c) return;
13
+ f(!1);
14
+ let e = setInterval(() => {
15
+ (typeof document > "u" || document.visibilityState === "visible") && f(!0);
16
+ }, i);
17
+ return () => clearInterval(e);
18
+ }, [
19
+ c,
20
+ l,
21
+ f
22
+ ]), {
23
+ ready: c && o === l,
24
+ loadFailed: u,
25
+ refresh: p
26
+ };
27
+ }
28
+ //#endregion
29
+ export { a as useFieldOpsData };
30
+
31
+ //# sourceMappingURL=useFieldOpsData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFieldOpsData.js","names":[],"sources":["../../../src/crewfoundry/lib/useFieldOpsData.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\n/** Field data goes stale as workers check in elsewhere; refresh this often while visible. */\nexport const FIELD_OPS_REFRESH_MS = 60_000;\n\n/**\n * Load field-operations server state for the current workspace + actor:\n * fetches on mount and whenever the context changes, refreshes every minute\n * while the tab is visible, and reports `ready` only once rows for THIS\n * context are in the store (never the previous workspace's or user's rows).\n */\nexport function useFieldOpsData(\n load: () => Promise<void>,\n loadedKey: string | null,\n onError: (message: string) => void\n): { ready: boolean; loadFailed: boolean; refresh: () => void } {\n const initialized = useCrewFoundryStore((state) => state.initialized);\n const contextKey = useCrewFoundryStore(\n (state) => `${state.initParams.workspaceId ?? ''}:${state.initParams.actorId ?? ''}`\n );\n const [loadFailed, setLoadFailed] = useState(false);\n\n const run = useCallback(\n (silent: boolean) => {\n load()\n .then(() => setLoadFailed(false))\n .catch((error: unknown) => {\n setLoadFailed(true);\n // Background refreshes stay quiet; the next tick retries.\n if (!silent) {\n onError(error instanceof Error ? error.message : 'Unable to load field operations');\n }\n });\n },\n [load, onError]\n );\n const refresh = useCallback(() => run(false), [run]);\n\n useEffect(() => {\n if (!initialized) return;\n run(false);\n const timer = setInterval(() => {\n if (typeof document === 'undefined' || document.visibilityState === 'visible') run(true);\n }, FIELD_OPS_REFRESH_MS);\n return () => clearInterval(timer);\n }, [initialized, contextKey, run]);\n\n return { ready: initialized && loadedKey === contextKey, loadFailed, refresh };\n}\n"],"mappings":";;;AAIA,IAAa,IAAuB;AAQpC,SAAgB,EACd,GACA,GACA,GAC8D;CAC9D,IAAM,IAAc,GAAqB,MAAU,EAAM,YAAY,EAC/D,IAAa,GAChB,MAAU,GAAG,EAAM,WAAW,eAAe,GAAG,GAAG,EAAM,WAAW,WAAW,KACjF,EACK,CAAC,GAAY,KAAiB,EAAS,GAAM,EAE7C,IAAM,GACT,MAAoB;AACnB,KAAM,CACH,WAAW,EAAc,GAAM,CAAC,CAChC,OAAO,MAAmB;AAGzB,GAFA,EAAc,GAAK,EAEd,KACH,EAAQ,aAAiB,QAAQ,EAAM,UAAU,kCAAkC;IAErF;IAEN,CAAC,GAAM,EAAQ,CAChB,EACK,IAAU,QAAkB,EAAI,GAAM,EAAE,CAAC,EAAI,CAAC;AAWpD,QATA,QAAgB;AACd,MAAI,CAAC,EAAa;AAClB,IAAI,GAAM;EACV,IAAM,IAAQ,kBAAkB;AAC9B,IAAI,OAAO,WAAa,OAAe,SAAS,oBAAoB,cAAW,EAAI,GAAK;KACvF,EAAqB;AACxB,eAAa,cAAc,EAAM;IAChC;EAAC;EAAa;EAAY;EAAI,CAAC,EAE3B;EAAE,OAAO,KAAe,MAAc;EAAY;EAAY;EAAS"}
@@ -1,2 +1,10 @@
1
+ /**
2
+ * Executive dashboard (BOFF-4208). Workforce KPIs, attrition risk, headcount
3
+ * plan, skills shortages and payroll run-rate come from the server-side
4
+ * `crewExecutiveDashboard` rollup, so every figure is computed from the
5
+ * workspace's real rows by one deterministic implementation rather than
6
+ * re-derived in the browser. Project budget and department budget figures
7
+ * still come from the bootstrap store, which already owns those rows.
8
+ */
1
9
  export declare function DashboardExecutiveDashboardPage(): import("react").JSX.Element;
2
10
  //# sourceMappingURL=DashboardExecutiveDashboardPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardExecutiveDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/DashboardExecutiveDashboardPage.tsx"],"names":[],"mappings":"AAkBA,wBAAgB,+BAA+B,gCAmM9C"}
1
+ {"version":3,"file":"DashboardExecutiveDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/DashboardExecutiveDashboardPage.tsx"],"names":[],"mappings":"AAudA;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,gCAiG9C"}