@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
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardExecutiveDashboardPage.js","names":[],"sources":["../../../src/crewfoundry/pages/DashboardExecutiveDashboardPage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { Building2, Percent, Target } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useCrewFoundryStore, type Department } from '../store/crewfoundryStore';\n\nexport function DashboardExecutiveDashboardPage() {\n const { employees, departments, projects, jobPostings } = useCrewFoundryStore();\n\n const riskIndicators = useMemo(\n () => [\n {\n metric: 'Flight Risk Employees',\n value: employees.filter((employee) => employee.performanceScore < 3.6).length,\n severity: 'warning' as const,\n detail: `${Math.round((employees.filter((employee) => employee.performanceScore < 3.6).length / Math.max(employees.length, 1)) * 100)}% of workforce`,\n },\n {\n metric: 'Employees On Leave',\n value: employees.filter((employee) => employee.status === 'on_leave').length,\n severity: 'error' as const,\n detail: 'Coverage follow-ups required',\n },\n {\n metric: 'Unfilled Lead Roles',\n value: jobPostings.filter(\n (posting) => posting.status === 'active' && posting.experienceLevel === 'lead'\n ).length,\n severity: 'warning' as const,\n detail: 'Open lead-level positions',\n },\n {\n metric: 'Contract Employees',\n value: employees.filter((employee) => employee.type === 'contract').length,\n severity: 'info' as const,\n detail: 'Contract renewals in focus',\n },\n ],\n [employees, jobPostings]\n );\n\n const totalSalaryBudget = employees.reduce((sum, e) => sum + e.salary, 0);\n const totalDeptBudget = departments.reduce((sum, d) => sum + d.budget, 0);\n const totalProjectBudget = projects.reduce((sum, p) => sum + p.budget, 0);\n const totalProjectSpent = projects.reduce((sum, p) => sum + p.spent, 0);\n const budgetUtilization =\n totalProjectBudget > 0 ? Math.round((totalProjectSpent / totalProjectBudget) * 100) : 0;\n\n const terminatedCount = employees.filter((e) => e.status === 'terminated').length;\n const retentionRate =\n employees.length > 0 ? Math.round((1 - terminatedCount / employees.length) * 100) : 0;\n\n const totalHeadcount = departments.reduce((sum, d) => sum + d.employeeCount, 0);\n const avgCostPerEmployee = totalHeadcount > 0 ? totalDeptBudget / totalHeadcount : 0;\n\n const departmentColumns: ResponsiveTableColumn<Department>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (dept) => <span className=\"font-medium\">{dept.name}</span>,\n },\n {\n key: 'head',\n header: 'Head',\n priority: 'secondary',\n cell: (dept) => <span className=\"text-text-secondary\">{dept.headName}</span>,\n },\n {\n key: 'headcount',\n header: 'Headcount',\n priority: 'secondary',\n cell: (dept) => <span className=\"md:block md:text-right\">{dept.employeeCount}</span>,\n },\n {\n key: 'budget',\n header: 'Budget',\n priority: 'secondary',\n cell: (dept) => (\n <span className=\"md:block md:text-right\">${(dept.budget / 1000000).toFixed(1)}M</span>\n ),\n },\n {\n key: 'costPerEmployee',\n header: 'Cost/Employee',\n priority: 'primary',\n cell: (dept) => {\n const costPerEmp =\n dept.employeeCount > 0 ? Math.round(dept.budget / dept.employeeCount) : 0;\n return <span className=\"md:block md:text-right\">${(costPerEmp / 1000).toFixed(0)}K</span>;\n },\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Executive Dashboard</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Strategic workforce insights for leadership</p>\n <PagePurpose className=\"mt-1\">\n The executive snapshot — top-line workforce, financial, and hiring KPIs in one view so\n leaders can make decisions fast.\n </PagePurpose>\n </div>\n <Badge variant=\"outline\">Q1 2026</Badge>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total Workforce Cost</CardTitle>\n <Building2 className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalSalaryBudget / 1000000).toFixed(1)}M\n </div>\n <p className=\"text-xs text-text-secondary\">Annual salary expenditure</p>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Budget Utilization</CardTitle>\n <Percent className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{budgetUtilization}%</div>\n <p className=\"text-xs text-text-secondary\">\n ${(totalProjectSpent / 1000000).toFixed(1)}M of $\n {(totalProjectBudget / 1000000).toFixed(1)}M project budget\n </p>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Retention Rate</CardTitle>\n <Target className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{retentionRate}%</div>\n <p className=\"text-xs text-text-secondary\">\n {terminatedCount} of {employees.length} employees no longer active\n </p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Risk Indicators</CardTitle>\n <CardDescription>Items requiring executive attention</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-3 md:grid-cols-2\">\n {riskIndicators.map((risk) => (\n <div key={risk.metric} className=\"flex items-center justify-between rounded border p-3\">\n <div className=\"flex items-center gap-3\">\n <div\n className={`h-3 w-3 rounded-full ${\n risk.severity === 'error'\n ? 'bg-status-error-text'\n : risk.severity === 'warning'\n ? 'bg-status-warning-text'\n : 'bg-status-info-text'\n }`}\n />\n <div>\n <p className=\"text-sm font-medium\">{risk.metric}</p>\n <p className=\"text-xs text-text-secondary\">{risk.detail}</p>\n </div>\n </div>\n <span className=\"text-lg font-bold\">{risk.value}</span>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Department Budget Status</CardTitle>\n <CardDescription>Annual budget allocation and headcount</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={departmentColumns}\n data={departments}\n rowKey={(dept) => dept.id}\n />\n <Separator className=\"my-3\" />\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm font-bold\">\n <span>Total</span>\n <span>{totalHeadcount} employees</span>\n <span>${(totalDeptBudget / 1000000).toFixed(1)}M budget</span>\n <span>${(avgCostPerEmployee / 1000).toFixed(0)}K / employee</span>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardExecutiveDashboardPage.js","names":[],"sources":["../../../src/crewfoundry/pages/DashboardExecutiveDashboardPage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Separator,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { Building2, Percent, RefreshCw, Target, UserPlus, Users } from 'lucide-react';\nimport { type ReactNode, useCallback, useEffect, useMemo } from 'react';\nimport {\n useCrewFoundryStore,\n type CrewAttritionRisk,\n type CrewAttritionRiskLevel,\n type CrewCostProjectionRow,\n type CrewExecutiveDashboard,\n type CrewHeadcountGapRow,\n type CrewSkillsGapRow,\n type Department,\n} from '../store/crewfoundryStore';\n\nconst MAX_SKILL_SHORTAGE_ROWS = 8;\n\nconst RISK_BADGE_VARIANT: Record<CrewAttritionRiskLevel, 'destructive' | 'warning' | 'secondary'> =\n {\n HIGH: 'destructive',\n MEDIUM: 'warning',\n LOW: 'secondary',\n };\n\nfunction formatMoney(value: number): string {\n if (Math.abs(value) >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;\n if (Math.abs(value) >= 1_000) return `$${(value / 1_000).toFixed(0)}K`;\n return `$${Math.round(value)}`;\n}\n\nfunction formatGeneratedAt(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '';\n return date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });\n}\n\nconst attritionColumns: ResponsiveTableColumn<CrewAttritionRisk>[] = [\n {\n key: 'employee',\n header: 'Employee',\n priority: 'primary',\n cell: (risk) => (\n <div>\n <p className=\"font-medium\">{risk.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">{risk.department}</p>\n </div>\n ),\n },\n {\n key: 'level',\n header: 'Risk',\n priority: 'primary',\n cell: (risk) => (\n <Badge variant={RISK_BADGE_VARIANT[risk.riskLevel]}>\n {risk.riskLevel.charAt(0)}\n {risk.riskLevel.slice(1).toLowerCase()} · {risk.riskScore.toFixed(2)}\n </Badge>\n ),\n },\n {\n key: 'drivers',\n header: 'Drivers',\n priority: 'secondary',\n cell: (risk) => (\n <span className=\"text-sm text-text-secondary\">\n {risk.drivers.length > 0 ? risk.drivers.join(', ') : 'No risk signals'}\n </span>\n ),\n },\n];\n\nconst headcountColumns: ResponsiveTableColumn<CrewHeadcountGapRow>[] = [\n {\n key: 'department',\n header: 'Department',\n priority: 'primary',\n cell: (row) => <span className=\"font-medium\">{row.department}</span>,\n },\n {\n key: 'current',\n header: 'Active',\n priority: 'secondary',\n cell: (row) => <span className=\"tabular-nums md:block md:text-right\">{row.current}</span>,\n },\n {\n key: 'open',\n header: 'Open roles',\n priority: 'primary',\n cell: (row) => <span className=\"tabular-nums md:block md:text-right\">{row.openPostings}</span>,\n },\n {\n key: 'projected',\n header: 'Projected',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"tabular-nums md:block md:text-right\">{row.projectedHeadcount}</span>\n ),\n },\n];\n\nconst skillShortageColumns: ResponsiveTableColumn<CrewSkillsGapRow>[] = [\n {\n key: 'skill',\n header: 'Skill',\n priority: 'primary',\n cell: (row) => <span className=\"font-medium\">{row.skill}</span>,\n },\n {\n key: 'supply',\n header: 'People',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"tabular-nums md:block md:text-right\">{row.employeesWithSkill}</span>\n ),\n },\n {\n key: 'demand',\n header: 'Open roles',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"tabular-nums md:block md:text-right\">{row.openJobsRequiringSkill}</span>\n ),\n },\n {\n key: 'shortage',\n header: 'Shortage',\n priority: 'primary',\n cell: (row) => (\n <span className=\"font-semibold tabular-nums text-status-warning-text md:block md:text-right\">\n {row.shortage}\n </span>\n ),\n },\n];\n\nconst costColumns: ResponsiveTableColumn<CrewCostProjectionRow>[] = [\n {\n key: 'department',\n header: 'Department',\n priority: 'primary',\n cell: (row) => <span className=\"font-medium\">{row.department}</span>,\n },\n {\n key: 'headcount',\n header: 'Active',\n priority: 'secondary',\n cell: (row) => <span className=\"tabular-nums md:block md:text-right\">{row.headcount}</span>,\n },\n {\n key: 'monthly',\n header: 'Monthly',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"tabular-nums md:block md:text-right\">{formatMoney(row.monthly)}</span>\n ),\n },\n {\n key: 'annual',\n header: 'Annual',\n priority: 'primary',\n cell: (row) => (\n <span className=\"tabular-nums md:block md:text-right\">{formatMoney(row.annual)}</span>\n ),\n },\n];\n\nconst departmentColumns: ResponsiveTableColumn<Department>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (dept) => <span className=\"font-medium\">{dept.name}</span>,\n },\n {\n key: 'head',\n header: 'Head',\n priority: 'secondary',\n cell: (dept) => <span className=\"text-text-secondary\">{dept.headName}</span>,\n },\n {\n key: 'headcount',\n header: 'Headcount',\n priority: 'secondary',\n cell: (dept) => <span className=\"md:block md:text-right\">{dept.employeeCount}</span>,\n },\n {\n key: 'budget',\n header: 'Budget',\n priority: 'secondary',\n cell: (dept) => <span className=\"md:block md:text-right\">{formatMoney(dept.budget)}</span>,\n },\n {\n key: 'costPerEmployee',\n header: 'Cost/Employee',\n priority: 'primary',\n cell: (dept) => {\n const costPerEmp = dept.employeeCount > 0 ? Math.round(dept.budget / dept.employeeCount) : 0;\n return <span className=\"md:block md:text-right\">{formatMoney(costPerEmp)}</span>;\n },\n },\n];\n\ninterface KpiTileProps {\n label: string;\n value: ReactNode;\n caption: string;\n icon?: ReactNode;\n testId?: string;\n}\n\n/** Metric tile whose caption always renders (StatCard hides it without a delta). */\nfunction KpiTile({ label, value, caption, icon, testId }: KpiTileProps) {\n return (\n <GlassCard treatment=\"metric\" className=\"border-border-seam\" data-testid={testId}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">{label}</CardTitle>\n {icon}\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{value}</div>\n <p className=\"text-xs text-text-secondary\">{caption}</p>\n </CardContent>\n </GlassCard>\n );\n}\n\nfunction WorkforceKpis({\n analytics,\n budget,\n retention,\n}: {\n analytics: CrewExecutiveDashboard;\n budget: { spent: number; total: number };\n retention: { retained: number; total: number };\n}) {\n const annualRunRate = analytics.costProjection.reduce((sum, row) => sum + row.annual, 0);\n const openRoles = analytics.headcountGap.reduce((sum, row) => sum + row.openPostings, 0);\n const budgetUtilization = budget.total > 0 ? Math.round((budget.spent / budget.total) * 100) : 0;\n // Retention counts everyone still employed (on leave and inactive\n // included); only terminated people have left.\n const retentionRate =\n retention.total > 0 ? `${Math.round((retention.retained / retention.total) * 100)}%` : '—';\n const iconClass = 'h-4 w-4 text-text-secondary';\n\n return (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <KpiTile\n label=\"Active headcount\"\n value={analytics.activeHeadcount}\n caption={`${analytics.totalHeadcount} people on record`}\n icon={<Users className={iconClass} />}\n testId=\"kpi-active-headcount\"\n />\n <KpiTile\n label=\"Hires and exits (90 days)\"\n value={`${analytics.newHiresLast90Days} / ${analytics.exitsLast90Days}`}\n caption={`${analytics.newHiresLast90Days} joined, ${analytics.exitsLast90Days} left`}\n icon={<UserPlus className={iconClass} />}\n />\n <KpiTile\n label=\"Average tenure\"\n value={`${analytics.averageTenureMonths.toFixed(1)} mo`}\n caption={`Average performance ${analytics.averagePerformanceScore.toFixed(2)} of 5`}\n icon={<Target className={iconClass} />}\n />\n <KpiTile\n label=\"Retention rate\"\n value={retentionRate}\n caption={`${retention.total - retention.retained} of ${retention.total} people on record have left`}\n icon={<Target className={iconClass} />}\n />\n <KpiTile\n label=\"Payroll run-rate\"\n value={formatMoney(annualRunRate)}\n caption={`Annual salary of active employees; ${openRoles} open role${openRoles === 1 ? '' : 's'} not included`}\n icon={<Building2 className={iconClass} />}\n testId=\"kpi-payroll-run-rate\"\n />\n <KpiTile\n label=\"Budget utilization\"\n value={`${budgetUtilization}%`}\n caption={`${formatMoney(budget.spent)} of ${formatMoney(budget.total)} project budget`}\n icon={<Percent className={iconClass} />}\n />\n </div>\n );\n}\n\nconst RISK_BANDS = [\n { key: 'high', label: 'High', dot: 'bg-status-error-text' },\n { key: 'medium', label: 'Medium', dot: 'bg-status-warning-text' },\n { key: 'low', label: 'Low', dot: 'bg-status-success-text' },\n] as const;\n\nfunction AttritionRiskCard({ risk }: { risk: CrewExecutiveDashboard['attritionRisk'] }) {\n const counts = { high: risk.highCount, medium: risk.mediumCount, low: risk.lowCount };\n return (\n <Card>\n <CardHeader>\n <CardTitle>Attrition risk</CardTitle>\n <CardDescription>\n Heuristic risk bands from performance, tenure, contract type and recent leave. A signal to\n start a conversation, not a prediction.\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-3 sm:grid-cols-3\">\n {RISK_BANDS.map((band) => (\n <div\n key={band.key}\n className=\"flex items-center justify-between rounded border p-3\"\n data-testid={`attrition-${band.key}`}\n >\n <div className=\"flex items-center gap-3\">\n <div className={`h-3 w-3 rounded-full ${band.dot}`} />\n <p className=\"text-sm font-medium\">{band.label} risk</p>\n </div>\n <span className=\"text-lg font-bold tabular-nums\">{counts[band.key]}</span>\n </div>\n ))}\n </div>\n {risk.totalAssessed === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No active employees to assess\"\n description=\"Add employees to see where retention attention is needed.\"\n />\n ) : (\n <ResponsiveTable\n columns={attritionColumns}\n data={risk.topRisks}\n rowKey={(row) => row.employeeId}\n />\n )}\n </CardContent>\n </Card>\n );\n}\n\ninterface TableCardProps<T> {\n title: string;\n description: string;\n columns: ResponsiveTableColumn<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n emptyTitle: string;\n emptyDescription: string;\n}\n\nfunction TableCard<T>({\n title,\n description,\n columns,\n rows,\n rowKey,\n emptyTitle,\n emptyDescription,\n}: TableCardProps<T>) {\n return (\n <Card>\n <CardHeader>\n <CardTitle>{title}</CardTitle>\n <CardDescription>{description}</CardDescription>\n </CardHeader>\n <CardContent>\n {rows.length > 0 ? (\n <ResponsiveTable columns={columns} data={rows} rowKey={rowKey} />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={emptyTitle}\n description={emptyDescription}\n />\n )}\n </CardContent>\n </Card>\n );\n}\n\nfunction WorkforceAnalyticsSections({\n analytics,\n budget,\n retention,\n}: {\n analytics: CrewExecutiveDashboard;\n budget: { spent: number; total: number };\n retention: { retained: number; total: number };\n}) {\n const skillShortages = analytics.skillsGap\n .filter((row) => row.shortage > 0)\n .slice(0, MAX_SKILL_SHORTAGE_ROWS);\n return (\n <>\n <WorkforceKpis analytics={analytics} budget={budget} retention={retention} />\n <AttritionRiskCard risk={analytics.attritionRisk} />\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <TableCard\n title=\"Headcount plan\"\n description=\"Active people per department plus open (active or paused) roles.\"\n columns={headcountColumns}\n rows={analytics.headcountGap}\n rowKey={(row) => row.department}\n emptyTitle=\"No headcount yet\"\n emptyDescription=\"Departments appear here once they have active people or open roles.\"\n />\n <TableCard\n title=\"Skills shortages\"\n description=\"Skills open roles ask for that more roles need than people have.\"\n columns={skillShortageColumns}\n rows={skillShortages}\n rowKey={(row) => row.skill}\n emptyTitle=\"No skills shortages\"\n emptyDescription=\"Every skill listed on an open role is covered by at least as many active employees.\"\n />\n </div>\n <TableCard\n title=\"Payroll run-rate by department\"\n description=\"Annualised salary of active employees\"\n columns={costColumns}\n rows={analytics.costProjection}\n rowKey={(row) => row.department}\n emptyTitle=\"No salaried employees yet\"\n emptyDescription=\"Record salaries on active employees to see the run-rate.\"\n />\n </>\n );\n}\n\nfunction DepartmentBudgetCard({ departments }: { departments: Department[] }) {\n const totalDeptBudget = departments.reduce((sum, d) => sum + d.budget, 0);\n const totalHeadcount = departments.reduce((sum, d) => sum + d.employeeCount, 0);\n const avgCostPerEmployee = totalHeadcount > 0 ? totalDeptBudget / totalHeadcount : 0;\n return (\n <Card>\n <CardHeader>\n <CardTitle>Department Budget Status</CardTitle>\n <CardDescription>Annual budget allocation and headcount</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={departmentColumns}\n data={departments}\n rowKey={(dept) => dept.id}\n />\n <Separator className=\"my-3\" />\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm font-bold\">\n <span>Total</span>\n <span>{totalHeadcount} employees</span>\n <span>{formatMoney(totalDeptBudget)} budget</span>\n <span>{formatMoney(avgCostPerEmployee)} / employee</span>\n </div>\n </CardContent>\n </Card>\n );\n}\n\n/**\n * Executive dashboard (BOFF-4208). Workforce KPIs, attrition risk, headcount\n * plan, skills shortages and payroll run-rate come from the server-side\n * `crewExecutiveDashboard` rollup, so every figure is computed from the\n * workspace's real rows by one deterministic implementation rather than\n * re-derived in the browser. Project budget and department budget figures\n * still come from the bootstrap store, which already owns those rows.\n */\nexport function DashboardExecutiveDashboardPage() {\n const initialized = useCrewFoundryStore((state) => state.initialized);\n const workspaceId = useCrewFoundryStore((state) => state.initParams.workspaceId);\n const departments = useCrewFoundryStore((state) => state.departments);\n const projects = useCrewFoundryStore((state) => state.projects);\n const employees = useCrewFoundryStore((state) => state.employees);\n const analytics = useCrewFoundryStore((state) => state.executiveDashboard);\n const analyticsLoading = useCrewFoundryStore((state) => state.executiveDashboardLoading);\n const analyticsError = useCrewFoundryStore((state) => state.executiveDashboardError);\n const fetchExecutiveDashboard = useCrewFoundryStore((state) => state.fetchExecutiveDashboard);\n\n const loadAnalytics = useCallback(() => {\n void fetchExecutiveDashboard();\n }, [fetchExecutiveDashboard]);\n\n // Refresh on every visit and on every workspace switch: the rollup reflects\n // writes made elsewhere in the module, so a cached copy would go stale.\n useEffect(() => {\n if (!initialized || !workspaceId) return;\n loadAnalytics();\n }, [initialized, workspaceId, loadAnalytics]);\n\n const budget = useMemo(\n () => ({\n spent: projects.reduce((sum, p) => sum + p.spent, 0),\n total: projects.reduce((sum, p) => sum + p.budget, 0),\n }),\n [projects]\n );\n const retention = useMemo(\n () => ({\n retained: employees.filter((employee) => employee.status !== 'terminated').length,\n total: employees.length,\n }),\n [employees]\n );\n\n if (!initialized) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Executive Dashboard</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Strategic workforce insights for leadership</p>\n <PagePurpose className=\"mt-1\">\n The executive snapshot — headcount, attrition risk, hiring plan, skills shortages and\n payroll run-rate in one view, computed from your workspace&apos;s own records.\n </PagePurpose>\n </div>\n <div className=\"flex items-center gap-2\">\n {analytics ? (\n <Badge variant=\"outline\">Updated {formatGeneratedAt(analytics.generatedAt)}</Badge>\n ) : null}\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={loadAnalytics}\n disabled={analyticsLoading}\n aria-label=\"Refresh workforce analytics\"\n >\n <RefreshCw className={`mr-2 h-4 w-4 ${analyticsLoading ? 'animate-spin' : ''}`} />\n Refresh\n </Button>\n </div>\n </div>\n\n {analyticsError ? (\n <Card className=\"border-status-error-border\">\n <CardContent className=\"flex flex-wrap items-center justify-between gap-3 py-4\">\n <p className=\"text-sm text-status-error-text\">{analyticsError}</p>\n <Button variant=\"outline\" size=\"sm\" onClick={loadAnalytics}>\n Try again\n </Button>\n </CardContent>\n </Card>\n ) : null}\n\n {analytics ? (\n <WorkforceAnalyticsSections analytics={analytics} budget={budget} retention={retention} />\n ) : analyticsLoading ? (\n <div className=\"flex min-h-[12rem] items-center justify-center\">\n <Spinner size=\"md\" />\n </div>\n ) : null}\n\n <DepartmentBudgetCard departments={departments} />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * My field work (BOFF-4205) — the field worker's phone view. Shows the
3
+ * signed-in worker's own shifts and work orders, checks in and out with the
4
+ * device's location (validated against the site geofence by the service),
5
+ * and advances work orders en route → on site → completed. Opened directly
6
+ * by a shift-reminder or new-work-order push (`?shift=` / `?dispatch=`).
7
+ */
8
+ export declare function FieldMyWorkPagePage(): import("react").JSX.Element;
9
+ //# sourceMappingURL=FieldMyWorkPagePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldMyWorkPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/FieldMyWorkPagePage.tsx"],"names":[],"mappings":"AAoCA;;;;;;GAMG;AACH,wBAAgB,mBAAmB,gCAwKlC"}
@@ -0,0 +1,141 @@
1
+ import { useFieldOpsStore as e } from "../store/fieldOpsStore.js";
2
+ import { ShiftStatusBadge as t } from "../components/field/FieldBadges.js";
3
+ import { STATUS_ACTION_LABEL as n, formatDistance as r, readDevicePosition as i } from "../lib/fieldOps.js";
4
+ import { formatWhen as a } from "../components/field/fieldFormat.js";
5
+ import { MyShiftItem as o } from "../components/field/MyShiftItem.js";
6
+ import { MyDispatchItem as s } from "../components/field/MyDispatchItem.js";
7
+ import { useFieldOpsData as c } from "../lib/useFieldOpsData.js";
8
+ import { useCallback as l, useEffect as u, useMemo as d, useState as f } from "react";
9
+ import { RefreshCw as p } from "lucide-react";
10
+ import { useSearchParams as m } from "react-router";
11
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
12
+ import { Button as _, Card as v, CardContent as y, CardDescription as b, CardHeader as x, CardTitle as S, GradientText as C, IllustratedEmptyState as w, PagePurpose as T, Spinner as E } from "@burdenoff/fe-libs/ui";
13
+ import { useGlobalUi as D } from "@burdenoff/fe-libs/shared/providers/shell";
14
+ //#region src/crewfoundry/pages/FieldMyWorkPagePage.tsx
15
+ var O = (e) => e.status === "SCHEDULED" || e.status === "IN_PROGRESS", k = (e) => e.status !== "COMPLETED" && e.status !== "CANCELLED";
16
+ function A() {
17
+ let { showToast: A } = D(), [j] = m(), M = j.get("shift"), N = j.get("dispatch"), { myShifts: P, myDispatches: F, myWorkKey: I, fetchMyWork: L, checkIn: R, checkOut: z, advanceMyDispatch: B } = e(), [V, H] = f(null), [U, W] = f(() => /* @__PURE__ */ new Date()), { ready: G, loadFailed: K, refresh: q } = c(L, I, l((e) => A(e, "error"), [A]));
18
+ u(() => {
19
+ let e = setInterval(() => W(/* @__PURE__ */ new Date()), 6e4);
20
+ return () => clearInterval(e);
21
+ }, []);
22
+ let J = d(() => P.filter(O).sort((e, t) => new Date(e.startsAt).getTime() - new Date(t.startsAt).getTime()), [P]), Y = d(() => P.filter((e) => !O(e)), [P]), X = d(() => F.filter(k), [F]), Z = async (e, t) => {
23
+ H(e.id);
24
+ try {
25
+ let n = await i();
26
+ if (t === "check-in") {
27
+ let t = (await R(e.id, n)).checkInDistanceMeters;
28
+ A(`Checked in${t === null ? "" : ` — ${r(t)} from the site`}`, "success");
29
+ } else {
30
+ let t = await z(e.id, n);
31
+ A(`Checked out${t.timeEntry ? ` — ${t.timeEntry.totalHours} h recorded` : ""}`, "success");
32
+ }
33
+ } catch (e) {
34
+ A(e instanceof Error ? e.message : "Unable to record your location", "error");
35
+ } finally {
36
+ H(null);
37
+ }
38
+ }, Q = (e, t) => {
39
+ H(e.id), B(e.id, t).then(() => A(`Marked ${n[t].toLowerCase()}`, "success")).catch((e) => A(e instanceof Error ? e.message : "Unable to update", "error")).finally(() => H(null));
40
+ };
41
+ return !G && !K ? /* @__PURE__ */ h("div", {
42
+ className: "flex min-h-[60vh] items-center justify-center p-6",
43
+ children: /* @__PURE__ */ h(E, { size: "lg" })
44
+ }) : /* @__PURE__ */ g("div", {
45
+ className: "mx-auto max-w-3xl space-y-5 p-4 sm:p-6",
46
+ "data-testid": "field-my-work-page",
47
+ children: [
48
+ /* @__PURE__ */ g("div", {
49
+ className: "flex items-end justify-between gap-3",
50
+ children: [/* @__PURE__ */ g("div", { children: [
51
+ /* @__PURE__ */ h("p", {
52
+ className: "text-sm text-text-secondary",
53
+ children: "Field operations"
54
+ }),
55
+ /* @__PURE__ */ h("h1", {
56
+ className: "text-2xl font-semibold",
57
+ children: /* @__PURE__ */ h(C, { children: "My Field Work" })
58
+ }),
59
+ /* @__PURE__ */ h(T, {
60
+ className: "mt-1",
61
+ children: "Check in and out of your shifts on site and update your work orders. Your location is read only when you tap check in or check out."
62
+ })
63
+ ] }), /* @__PURE__ */ h(_, {
64
+ size: "sm",
65
+ variant: "outline",
66
+ "aria-label": "Refresh",
67
+ onClick: q,
68
+ children: /* @__PURE__ */ h(p, { className: "h-4 w-4" })
69
+ })]
70
+ }),
71
+ /* @__PURE__ */ g(v, {
72
+ "data-testid": "my-shifts",
73
+ children: [/* @__PURE__ */ g(x, { children: [/* @__PURE__ */ h(S, {
74
+ className: "text-base",
75
+ children: "My shifts"
76
+ }), /* @__PURE__ */ h(b, { children: "Check-in opens 30 minutes before a shift starts." })] }), /* @__PURE__ */ h(y, { children: J.length > 0 ? /* @__PURE__ */ h("ul", {
77
+ className: "space-y-3",
78
+ "aria-label": "Upcoming shifts",
79
+ children: J.map((e) => /* @__PURE__ */ h(o, {
80
+ shift: e,
81
+ now: U,
82
+ focused: M === e.id,
83
+ busy: V === e.id,
84
+ onCheckIn: (e) => void Z(e, "check-in"),
85
+ onCheckOut: (e) => void Z(e, "check-out")
86
+ }, e.id))
87
+ }) : /* @__PURE__ */ h(w, {
88
+ illustration: "empty-data",
89
+ title: "No upcoming shifts",
90
+ description: "Shifts your dispatcher schedules for you appear here."
91
+ }) })]
92
+ }),
93
+ /* @__PURE__ */ g(v, {
94
+ "data-testid": "my-work-orders",
95
+ children: [/* @__PURE__ */ h(x, { children: /* @__PURE__ */ h(S, {
96
+ className: "text-base",
97
+ children: "My work orders"
98
+ }) }), /* @__PURE__ */ h(y, { children: X.length > 0 ? /* @__PURE__ */ h("ul", {
99
+ className: "space-y-3",
100
+ "aria-label": "Work orders",
101
+ children: X.map((e) => /* @__PURE__ */ h(s, {
102
+ dispatch: e,
103
+ focused: N === e.id,
104
+ busy: V === e.id,
105
+ onAdvance: Q
106
+ }, e.id))
107
+ }) : /* @__PURE__ */ h(w, {
108
+ illustration: "empty-data",
109
+ title: "No open work orders",
110
+ description: "Work orders assigned to you appear here, and you get a push notification on your phone."
111
+ }) })]
112
+ }),
113
+ Y.length > 0 ? /* @__PURE__ */ g(v, { children: [/* @__PURE__ */ h(x, { children: /* @__PURE__ */ h(S, {
114
+ className: "text-base",
115
+ children: "Recent shifts"
116
+ }) }), /* @__PURE__ */ h(y, { children: /* @__PURE__ */ h("ul", {
117
+ className: "space-y-1 text-sm text-text-secondary",
118
+ children: Y.slice(0, 10).map((e) => /* @__PURE__ */ g("li", {
119
+ className: "flex flex-wrap items-center gap-2",
120
+ children: [
121
+ /* @__PURE__ */ h(t, { status: e.status }),
122
+ /* @__PURE__ */ h("span", {
123
+ className: "text-text-primary",
124
+ children: e.title
125
+ }),
126
+ /* @__PURE__ */ h("span", { children: a(e.startsAt) }),
127
+ e.timeEntry ? /* @__PURE__ */ g("span", { children: [
128
+ "· ",
129
+ e.timeEntry.totalHours,
130
+ " h"
131
+ ] }) : null
132
+ ]
133
+ }, e.id))
134
+ }) })] }) : null
135
+ ]
136
+ });
137
+ }
138
+ //#endregion
139
+ export { A as FieldMyWorkPagePage };
140
+
141
+ //# sourceMappingURL=FieldMyWorkPagePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldMyWorkPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/FieldMyWorkPagePage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { RefreshCw } from 'lucide-react';\nimport { useFieldOpsStore } from '../store/fieldOpsStore';\nimport { ShiftStatusBadge } from '../components/field/FieldBadges';\nimport { MyShiftItem } from '../components/field/MyShiftItem';\nimport { MyDispatchItem } from '../components/field/MyDispatchItem';\nimport { formatWhen } from '../components/field/fieldFormat';\nimport {\n type CrewDispatch,\n type CrewShift,\n type DispatchStatus,\n STATUS_ACTION_LABEL,\n formatDistance,\n readDevicePosition,\n} from '../lib/fieldOps';\nimport { useFieldOpsData } from '../lib/useFieldOpsData';\n\nconst isActive = (shift: CrewShift): boolean =>\n shift.status === 'SCHEDULED' || shift.status === 'IN_PROGRESS';\nconst isOpen = (dispatch: CrewDispatch): boolean =>\n dispatch.status !== 'COMPLETED' && dispatch.status !== 'CANCELLED';\n\n/**\n * My field work (BOFF-4205) — the field worker's phone view. Shows the\n * signed-in worker's own shifts and work orders, checks in and out with the\n * device's location (validated against the site geofence by the service),\n * and advances work orders en route → on site → completed. Opened directly\n * by a shift-reminder or new-work-order push (`?shift=` / `?dispatch=`).\n */\nexport function FieldMyWorkPagePage() {\n const { showToast } = useGlobalUi();\n const [searchParams] = useSearchParams();\n const focusShift = searchParams.get('shift');\n const focusDispatch = searchParams.get('dispatch');\n const { myShifts, myDispatches, myWorkKey, fetchMyWork, checkIn, checkOut, advanceMyDispatch } =\n useFieldOpsStore();\n const [busy, setBusy] = useState<string | null>(null);\n const [now, setNow] = useState(() => new Date());\n\n const onLoadError = useCallback((message: string) => showToast(message, 'error'), [showToast]);\n const { ready, loadFailed, refresh } = useFieldOpsData(fetchMyWork, myWorkKey, onLoadError);\n\n // Re-evaluate check-in windows once a minute.\n useEffect(() => {\n const timer = setInterval(() => setNow(new Date()), 60_000);\n return () => clearInterval(timer);\n }, []);\n\n const activeShifts = useMemo(\n () =>\n myShifts\n .filter(isActive)\n .sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime()),\n [myShifts]\n );\n const pastShifts = useMemo(() => myShifts.filter((shift) => !isActive(shift)), [myShifts]);\n const openDispatches = useMemo(() => myDispatches.filter(isOpen), [myDispatches]);\n\n const geoAction = async (shift: CrewShift, kind: 'check-in' | 'check-out') => {\n setBusy(shift.id);\n try {\n const position = await readDevicePosition();\n if (kind === 'check-in') {\n const updated = await checkIn(shift.id, position);\n const distance = updated.checkInDistanceMeters;\n showToast(\n `Checked in${distance !== null ? ` — ${formatDistance(distance)} from the site` : ''}`,\n 'success'\n );\n } else {\n const updated = await checkOut(shift.id, position);\n showToast(\n `Checked out${updated.timeEntry ? ` — ${updated.timeEntry.totalHours} h recorded` : ''}`,\n 'success'\n );\n }\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to record your location', 'error');\n } finally {\n setBusy(null);\n }\n };\n\n const advance = (dispatch: CrewDispatch, next: DispatchStatus) => {\n setBusy(dispatch.id);\n void advanceMyDispatch(dispatch.id, next)\n .then(() => showToast(`Marked ${STATUS_ACTION_LABEL[next].toLowerCase()}`, 'success'))\n .catch((error: unknown) =>\n showToast(error instanceof Error ? error.message : 'Unable to update', 'error')\n )\n .finally(() => setBusy(null));\n };\n\n if (!ready && !loadFailed) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n return (\n <div className=\"mx-auto max-w-3xl space-y-5 p-4 sm:p-6\" data-testid=\"field-my-work-page\">\n <div className=\"flex items-end justify-between gap-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">Field operations</p>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>My Field Work</GradientText>\n </h1>\n <PagePurpose className=\"mt-1\">\n Check in and out of your shifts on site and update your work orders. Your location is\n read only when you tap check in or check out.\n </PagePurpose>\n </div>\n <Button size=\"sm\" variant=\"outline\" aria-label=\"Refresh\" onClick={refresh}>\n <RefreshCw className=\"h-4 w-4\" />\n </Button>\n </div>\n\n <Card data-testid=\"my-shifts\">\n <CardHeader>\n <CardTitle className=\"text-base\">My shifts</CardTitle>\n <CardDescription>Check-in opens 30 minutes before a shift starts.</CardDescription>\n </CardHeader>\n <CardContent>\n {activeShifts.length > 0 ? (\n <ul className=\"space-y-3\" aria-label=\"Upcoming shifts\">\n {activeShifts.map((shift) => (\n <MyShiftItem\n key={shift.id}\n shift={shift}\n now={now}\n focused={focusShift === shift.id}\n busy={busy === shift.id}\n onCheckIn={(target) => void geoAction(target, 'check-in')}\n onCheckOut={(target) => void geoAction(target, 'check-out')}\n />\n ))}\n </ul>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No upcoming shifts\"\n description=\"Shifts your dispatcher schedules for you appear here.\"\n />\n )}\n </CardContent>\n </Card>\n\n <Card data-testid=\"my-work-orders\">\n <CardHeader>\n <CardTitle className=\"text-base\">My work orders</CardTitle>\n </CardHeader>\n <CardContent>\n {openDispatches.length > 0 ? (\n <ul className=\"space-y-3\" aria-label=\"Work orders\">\n {openDispatches.map((dispatch) => (\n <MyDispatchItem\n key={dispatch.id}\n dispatch={dispatch}\n focused={focusDispatch === dispatch.id}\n busy={busy === dispatch.id}\n onAdvance={advance}\n />\n ))}\n </ul>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No open work orders\"\n description=\"Work orders assigned to you appear here, and you get a push notification on your phone.\"\n />\n )}\n </CardContent>\n </Card>\n\n {pastShifts.length > 0 ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Recent shifts</CardTitle>\n </CardHeader>\n <CardContent>\n <ul className=\"space-y-1 text-sm text-text-secondary\">\n {pastShifts.slice(0, 10).map((shift) => (\n <li key={shift.id} className=\"flex flex-wrap items-center gap-2\">\n <ShiftStatusBadge status={shift.status} />\n <span className=\"text-text-primary\">{shift.title}</span>\n <span>{formatWhen(shift.startsAt)}</span>\n {shift.timeEntry ? <span>· {shift.timeEntry.totalHours} h</span> : null}\n </li>\n ))}\n </ul>\n </CardContent>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Field operations (BOFF-4205) — the dispatcher's view. Schedule shifts at a
3
+ * site with a check-in geofence, see who is on site now and who missed a
4
+ * shift, push a reminder to a worker's phone, and run work orders through
5
+ * open → assigned → en route → on site → completed. Refreshes every minute
6
+ * so worker check-ins and work-order moves show up without a reload.
7
+ */
8
+ export declare function FieldOperationsPagePage(): import("react").JSX.Element;
9
+ //# sourceMappingURL=FieldOperationsPagePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldOperationsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/FieldOperationsPagePage.tsx"],"names":[],"mappings":"AAoCA;;;;;;GAMG;AACH,wBAAgB,uBAAuB,gCA+RtC"}
@@ -0,0 +1,231 @@
1
+ import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
2
+ import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
3
+ import { StatCard as n } from "../components/viz/StatCard.js";
4
+ import "../components/viz/index.js";
5
+ import { FormModal as r } from "../components/forms/FormModal.js";
6
+ import { useFieldOpsStore as i } from "../store/fieldOpsStore.js";
7
+ import { shiftsOnDay as a } from "../lib/fieldOps.js";
8
+ import { useFieldOpsData as o } from "../lib/useFieldOpsData.js";
9
+ import { ConfirmDialog as s } from "../components/forms/ConfirmDialog.js";
10
+ import { getText as ee } from "../components/forms/formValues.js";
11
+ import { ShiftsCard as te } from "../components/field/ShiftsCard.js";
12
+ import { DispatchBoardCard as ne } from "../components/field/DispatchBoardCard.js";
13
+ import { NO_SELECTION as c, buildAssignFields as l, buildDispatchFields as re, buildShiftFields as ie, toDispatchInput as ae, toShiftInput as oe } from "../components/field/fieldForms.js";
14
+ import { useCallback as u, useMemo as d, useState as f } from "react";
15
+ import { AlarmClock as p, CalendarClock as m, ClipboardList as h, MapPin as se, Plus as ce, RefreshCw as le } from "lucide-react";
16
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
17
+ import { Button as v, GradientText as y, PagePurpose as b, Spinner as x } from "@burdenoff/fe-libs/ui";
18
+ import { useGlobalUi as S } from "@burdenoff/fe-libs/shared/providers/shell";
19
+ //#region src/crewfoundry/pages/FieldOperationsPagePage.tsx
20
+ var C = (e, t) => e instanceof Error ? e.message : t;
21
+ function w(e) {
22
+ return e.totalDevices > 0 ? `Reminder sent to ${e.delivered} of ${e.totalDevices} devices` : e.dispatched ? "Reminder handed to push — the worker has no registered phone yet" : "Reminder could not be delivered";
23
+ }
24
+ function T() {
25
+ let { showToast: T } = S(), E = e((e) => e.employees), D = i(), { shifts: O, dispatches: k, members: A, boardKey: ue } = D, [j, M] = f(!1), [N, P] = f(!1), [F, I] = f(null), [L, R] = f(null), [z, B] = f(null), [V, H] = f(null), [U, W] = f(), { fetchBoard: G, fetchMembers: K } = D, { ready: de, loadFailed: q, refresh: fe } = o(u(async () => {
26
+ await G(), await K().catch(() => void 0);
27
+ }, [G, K]), ue, u((e) => T(e, "error"), [T])), J = d(() => O.filter((e) => e.status === "SCHEDULED" || e.status === "IN_PROGRESS"), [O]), pe = d(() => ie(E.filter((e) => e.status !== "terminated").map((e) => ({
28
+ label: `${e.firstName} ${e.lastName} — ${e.position}`,
29
+ value: e.id
30
+ })), A.map((e) => ({
31
+ label: e.label,
32
+ value: e.userId
33
+ }))), [E, A]), me = d(() => re(J), [J]), he = d(() => l(J), [J]), Y = d(() => ({
34
+ today: a(O, /* @__PURE__ */ new Date()).filter((e) => e.status !== "CANCELLED").length,
35
+ onSite: O.filter((e) => e.status === "IN_PROGRESS").length,
36
+ missed: O.filter((e) => e.status === "MISSED").length,
37
+ open: k.filter((e) => e.status !== "COMPLETED" && e.status !== "CANCELLED").length
38
+ }), [O, k]), X = async (e, t) => {
39
+ try {
40
+ await e(), T(t, "success");
41
+ } catch (e) {
42
+ T(C(e, "Something went wrong"), "error");
43
+ }
44
+ }, Z = async (e, t, n) => {
45
+ try {
46
+ await e(), W(void 0), T(t, "success");
47
+ } catch (e) {
48
+ throw W(C(e, n)), e;
49
+ }
50
+ }, Q = (e) => {
51
+ if (typeof e == "string") throw W(e), Error(e);
52
+ return e;
53
+ }, $ = (e) => {
54
+ W(void 0), e(!0);
55
+ };
56
+ return !de && !q ? /* @__PURE__ */ g("div", {
57
+ className: "flex min-h-[60vh] items-center justify-center p-6",
58
+ children: /* @__PURE__ */ g(x, { size: "lg" })
59
+ }) : /* @__PURE__ */ _("div", {
60
+ className: "space-y-6 p-4 sm:p-6",
61
+ "data-testid": "field-operations-page",
62
+ children: [
63
+ /* @__PURE__ */ _("div", {
64
+ className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between",
65
+ children: [/* @__PURE__ */ _("div", { children: [
66
+ /* @__PURE__ */ g("p", {
67
+ className: "text-sm text-text-secondary",
68
+ children: "Field operations"
69
+ }),
70
+ /* @__PURE__ */ g("h1", {
71
+ className: "text-2xl font-semibold",
72
+ children: /* @__PURE__ */ g(y, { children: "Dispatch Board" })
73
+ }),
74
+ /* @__PURE__ */ g(b, {
75
+ className: "mt-1",
76
+ children: "Schedule shifts at a site, see who has checked in on location, and move work orders from open to done. Workers check in from the My Field Work page on their phone."
77
+ })
78
+ ] }), /* @__PURE__ */ _("div", {
79
+ className: "flex flex-wrap gap-2",
80
+ children: [
81
+ /* @__PURE__ */ g(v, {
82
+ size: "sm",
83
+ variant: "outline",
84
+ "aria-label": "Refresh",
85
+ onClick: fe,
86
+ children: /* @__PURE__ */ g(le, { className: "h-4 w-4" })
87
+ }),
88
+ /* @__PURE__ */ _(v, {
89
+ size: "sm",
90
+ variant: "outline",
91
+ onClick: () => $(P),
92
+ children: [/* @__PURE__ */ g(h, { className: "mr-1 h-4 w-4" }), "New work order"]
93
+ }),
94
+ /* @__PURE__ */ _(v, {
95
+ size: "sm",
96
+ onClick: () => $(M),
97
+ disabled: E.length === 0,
98
+ children: [/* @__PURE__ */ g(ce, { className: "mr-1 h-4 w-4" }), "Schedule shift"]
99
+ })
100
+ ]
101
+ })]
102
+ }),
103
+ /* @__PURE__ */ _("div", {
104
+ className: "grid grid-cols-2 gap-3 lg:grid-cols-4",
105
+ children: [
106
+ /* @__PURE__ */ g(n, {
107
+ label: "Shifts today",
108
+ value: Y.today,
109
+ icon: /* @__PURE__ */ g(m, { className: "h-4 w-4" })
110
+ }),
111
+ /* @__PURE__ */ g(n, {
112
+ label: "On site now",
113
+ value: Y.onSite,
114
+ icon: /* @__PURE__ */ g(se, { className: "h-4 w-4" })
115
+ }),
116
+ /* @__PURE__ */ g(n, {
117
+ label: "Missed",
118
+ value: Y.missed,
119
+ icon: /* @__PURE__ */ g(p, { className: "h-4 w-4" })
120
+ }),
121
+ /* @__PURE__ */ g(n, {
122
+ label: "Open work orders",
123
+ value: Y.open,
124
+ icon: /* @__PURE__ */ g(h, { className: "h-4 w-4" })
125
+ })
126
+ ]
127
+ }),
128
+ /* @__PURE__ */ g(te, {
129
+ shifts: O,
130
+ onRemind: (e) => void D.sendReminder(e.id).then((e) => T(w(e), e.dispatched ? "success" : "error")).catch((e) => T(C(e, "Unable to send"), "error")),
131
+ onCancel: R,
132
+ onRemove: B
133
+ }),
134
+ /* @__PURE__ */ g(ne, {
135
+ dispatches: k,
136
+ canAssign: J.length > 0,
137
+ onAssign: (e) => {
138
+ W(void 0), I(e);
139
+ },
140
+ onTransition: (e, n) => void X(() => D.transitionDispatch(e.id, n), `Work order moved to ${t(n)}`),
141
+ onRemove: H
142
+ }),
143
+ /* @__PURE__ */ g(r, {
144
+ open: j,
145
+ onOpenChange: M,
146
+ title: "Schedule shift",
147
+ description: "Workers can check in from 30 minutes before the start, inside the site's check-in radius.",
148
+ errorMessage: U,
149
+ onValidationError: W,
150
+ fields: pe,
151
+ initialValues: {
152
+ geofenceRadiusMeters: 150,
153
+ reminderMinutesBefore: 60,
154
+ assigneeUserId: c
155
+ },
156
+ submitLabel: "Schedule",
157
+ onSubmit: (e) => Z(() => D.createShift(Q(oe(e))), "Shift scheduled", "Unable to schedule this shift")
158
+ }),
159
+ /* @__PURE__ */ g(r, {
160
+ open: N,
161
+ onOpenChange: P,
162
+ title: "New work order",
163
+ errorMessage: U,
164
+ onValidationError: W,
165
+ fields: me,
166
+ initialValues: {
167
+ priority: "NORMAL",
168
+ shiftId: c
169
+ },
170
+ submitLabel: "Create",
171
+ onSubmit: (e) => Z(() => D.createDispatch(Q(ae(e))), "Work order created", "Unable to create this work order")
172
+ }),
173
+ /* @__PURE__ */ g(r, {
174
+ open: F !== null,
175
+ onOpenChange: (e) => {
176
+ e || I(null);
177
+ },
178
+ title: F ? `Assign “${F.title}”` : "Assign",
179
+ description: "The worker on the chosen shift gets the work order and a push notification.",
180
+ errorMessage: U,
181
+ onValidationError: W,
182
+ fields: he,
183
+ submitLabel: "Assign",
184
+ onSubmit: async (e) => {
185
+ F && (await Z(() => D.assignDispatch(F.id, { shiftId: ee(e, "shiftId") }), "Work order assigned", "Unable to assign"), I(null));
186
+ }
187
+ }),
188
+ /* @__PURE__ */ g(s, {
189
+ open: L !== null,
190
+ onOpenChange: (e) => {
191
+ e || R(null);
192
+ },
193
+ title: "Cancel this shift?",
194
+ description: L ? `${L.title} for ${L.employeeName}` : void 0,
195
+ confirmLabel: "Cancel shift",
196
+ destructive: !0,
197
+ onConfirm: async () => {
198
+ L && await X(() => D.cancelShift(L.id, null), "Shift cancelled"), R(null);
199
+ }
200
+ }),
201
+ /* @__PURE__ */ g(s, {
202
+ open: z !== null,
203
+ onOpenChange: (e) => {
204
+ e || B(null);
205
+ },
206
+ title: "Remove this shift?",
207
+ confirmLabel: "Remove",
208
+ destructive: !0,
209
+ onConfirm: async () => {
210
+ z && await X(() => D.removeShift(z.id), "Shift removed"), B(null);
211
+ }
212
+ }),
213
+ /* @__PURE__ */ g(s, {
214
+ open: V !== null,
215
+ onOpenChange: (e) => {
216
+ e || H(null);
217
+ },
218
+ title: "Remove this work order?",
219
+ confirmLabel: "Remove",
220
+ destructive: !0,
221
+ onConfirm: async () => {
222
+ V && await X(() => D.removeDispatch(V.id), "Work order removed"), H(null);
223
+ }
224
+ })
225
+ ]
226
+ });
227
+ }
228
+ //#endregion
229
+ export { T as FieldOperationsPagePage };
230
+
231
+ //# sourceMappingURL=FieldOperationsPagePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldOperationsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/FieldOperationsPagePage.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport { Button, GradientText, PagePurpose, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { AlarmClock, CalendarClock, ClipboardList, MapPin, Plus, RefreshCw } from 'lucide-react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { type ReminderOutcome, useFieldOpsStore } from '../store/fieldOpsStore';\nimport { FormModal, type FormValues } from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getText } from '../components/forms/formValues';\nimport { StatCard } from '../components/viz';\nimport { ShiftsCard } from '../components/field/ShiftsCard';\nimport { DispatchBoardCard } from '../components/field/DispatchBoardCard';\nimport {\n NO_SELECTION,\n buildAssignFields,\n buildDispatchFields,\n buildShiftFields,\n toDispatchInput,\n toShiftInput,\n} from '../components/field/fieldForms';\nimport { type CrewDispatch, type CrewShift, shiftsOnDay } from '../lib/fieldOps';\nimport { useFieldOpsData } from '../lib/useFieldOpsData';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst errorMessage = (error: unknown, fallback: string): string =>\n error instanceof Error ? error.message : fallback;\n\nfunction reminderToast(outcome: ReminderOutcome): string {\n if (outcome.totalDevices > 0) {\n return `Reminder sent to ${outcome.delivered} of ${outcome.totalDevices} devices`;\n }\n return outcome.dispatched\n ? 'Reminder handed to push — the worker has no registered phone yet'\n : 'Reminder could not be delivered';\n}\n\n/**\n * Field operations (BOFF-4205) — the dispatcher's view. Schedule shifts at a\n * site with a check-in geofence, see who is on site now and who missed a\n * shift, push a reminder to a worker's phone, and run work orders through\n * open → assigned → en route → on site → completed. Refreshes every minute\n * so worker check-ins and work-order moves show up without a reload.\n */\nexport function FieldOperationsPagePage() {\n const { showToast } = useGlobalUi();\n const employees = useCrewFoundryStore((state) => state.employees);\n const store = useFieldOpsStore();\n const { shifts, dispatches, members, boardKey } = store;\n\n const [shiftOpen, setShiftOpen] = useState(false);\n const [dispatchOpen, setDispatchOpen] = useState(false);\n const [assignTarget, setAssignTarget] = useState<CrewDispatch | null>(null);\n const [cancelTarget, setCancelTarget] = useState<CrewShift | null>(null);\n const [removeShiftTarget, setRemoveShiftTarget] = useState<CrewShift | null>(null);\n const [removeDispatchTarget, setRemoveDispatchTarget] = useState<CrewDispatch | null>(null);\n const [formError, setFormError] = useState<string | undefined>();\n\n const { fetchBoard, fetchMembers } = store;\n const load = useCallback(async () => {\n await fetchBoard();\n // The member directory only enriches the worker-login picker.\n await fetchMembers().catch(() => undefined);\n }, [fetchBoard, fetchMembers]);\n const onLoadError = useCallback((message: string) => showToast(message, 'error'), [showToast]);\n const { ready, loadFailed, refresh } = useFieldOpsData(load, boardKey, onLoadError);\n\n const upcoming = useMemo(\n () => shifts.filter((shift) => shift.status === 'SCHEDULED' || shift.status === 'IN_PROGRESS'),\n [shifts]\n );\n const shiftFields = useMemo(\n () =>\n buildShiftFields(\n employees\n .filter((employee) => employee.status !== 'terminated')\n .map((employee) => ({\n label: `${employee.firstName} ${employee.lastName} — ${employee.position}`,\n value: employee.id,\n })),\n members.map((member) => ({ label: member.label, value: member.userId }))\n ),\n [employees, members]\n );\n const dispatchFields = useMemo(() => buildDispatchFields(upcoming), [upcoming]);\n const assignFields = useMemo(() => buildAssignFields(upcoming), [upcoming]);\n\n const stats = useMemo(\n () => ({\n today: shiftsOnDay(shifts, new Date()).filter((shift) => shift.status !== 'CANCELLED').length,\n onSite: shifts.filter((shift) => shift.status === 'IN_PROGRESS').length,\n missed: shifts.filter((shift) => shift.status === 'MISSED').length,\n open: dispatches.filter(\n (dispatch) => dispatch.status !== 'COMPLETED' && dispatch.status !== 'CANCELLED'\n ).length,\n }),\n [shifts, dispatches]\n );\n\n const run = async (action: () => Promise<unknown>, success: string) => {\n try {\n await action();\n showToast(success, 'success');\n } catch (error) {\n showToast(errorMessage(error, 'Something went wrong'), 'error');\n }\n };\n\n /** Shared submit for the three forms: validate, call, surface errors inside the modal. */\n const submit = async (action: () => Promise<void>, success: string, fallback: string) => {\n try {\n await action();\n setFormError(undefined);\n showToast(success, 'success');\n } catch (error) {\n setFormError(errorMessage(error, fallback));\n throw error;\n }\n };\n\n const mapped = <T,>(result: T | string): T => {\n if (typeof result === 'string') {\n setFormError(result);\n throw new Error(result);\n }\n return result;\n };\n\n const openForm = (open: (value: boolean) => void) => {\n setFormError(undefined);\n open(true);\n };\n\n if (!ready && !loadFailed) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\" data-testid=\"field-operations-page\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between\">\n <div>\n <p className=\"text-sm text-text-secondary\">Field operations</p>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Dispatch Board</GradientText>\n </h1>\n <PagePurpose className=\"mt-1\">\n Schedule shifts at a site, see who has checked in on location, and move work orders from\n open to done. Workers check in from the My Field Work page on their phone.\n </PagePurpose>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"outline\" aria-label=\"Refresh\" onClick={refresh}>\n <RefreshCw className=\"h-4 w-4\" />\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => openForm(setDispatchOpen)}>\n <ClipboardList className=\"mr-1 h-4 w-4\" />\n New work order\n </Button>\n <Button\n size=\"sm\"\n onClick={() => openForm(setShiftOpen)}\n disabled={employees.length === 0}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Schedule shift\n </Button>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <StatCard\n label=\"Shifts today\"\n value={stats.today}\n icon={<CalendarClock className=\"h-4 w-4\" />}\n />\n <StatCard label=\"On site now\" value={stats.onSite} icon={<MapPin className=\"h-4 w-4\" />} />\n <StatCard label=\"Missed\" value={stats.missed} icon={<AlarmClock className=\"h-4 w-4\" />} />\n <StatCard\n label=\"Open work orders\"\n value={stats.open}\n icon={<ClipboardList className=\"h-4 w-4\" />}\n />\n </div>\n\n <ShiftsCard\n shifts={shifts}\n onRemind={(shift) =>\n void store\n .sendReminder(shift.id)\n .then((outcome) =>\n showToast(reminderToast(outcome), outcome.dispatched ? 'success' : 'error')\n )\n .catch((error: unknown) => showToast(errorMessage(error, 'Unable to send'), 'error'))\n }\n onCancel={setCancelTarget}\n onRemove={setRemoveShiftTarget}\n />\n\n <DispatchBoardCard\n dispatches={dispatches}\n canAssign={upcoming.length > 0}\n onAssign={(dispatch) => {\n setFormError(undefined);\n setAssignTarget(dispatch);\n }}\n onTransition={(dispatch, next) =>\n void run(\n () => store.transitionDispatch(dispatch.id, next),\n `Work order moved to ${formatEnumLabel(next)}`\n )\n }\n onRemove={setRemoveDispatchTarget}\n />\n\n <FormModal\n open={shiftOpen}\n onOpenChange={setShiftOpen}\n title=\"Schedule shift\"\n description=\"Workers can check in from 30 minutes before the start, inside the site's check-in radius.\"\n errorMessage={formError}\n onValidationError={setFormError}\n fields={shiftFields}\n initialValues={{\n geofenceRadiusMeters: 150,\n reminderMinutesBefore: 60,\n assigneeUserId: NO_SELECTION,\n }}\n submitLabel=\"Schedule\"\n onSubmit={(values: FormValues) =>\n submit(\n () => store.createShift(mapped(toShiftInput(values))),\n 'Shift scheduled',\n 'Unable to schedule this shift'\n )\n }\n />\n\n <FormModal\n open={dispatchOpen}\n onOpenChange={setDispatchOpen}\n title=\"New work order\"\n errorMessage={formError}\n onValidationError={setFormError}\n fields={dispatchFields}\n initialValues={{ priority: 'NORMAL', shiftId: NO_SELECTION }}\n submitLabel=\"Create\"\n onSubmit={(values: FormValues) =>\n submit(\n () => store.createDispatch(mapped(toDispatchInput(values))),\n 'Work order created',\n 'Unable to create this work order'\n )\n }\n />\n\n <FormModal\n open={assignTarget !== null}\n onOpenChange={(open) => {\n if (!open) setAssignTarget(null);\n }}\n title={assignTarget ? `Assign “${assignTarget.title}”` : 'Assign'}\n description=\"The worker on the chosen shift gets the work order and a push notification.\"\n errorMessage={formError}\n onValidationError={setFormError}\n fields={assignFields}\n submitLabel=\"Assign\"\n onSubmit={async (values: FormValues) => {\n if (!assignTarget) return;\n await submit(\n () => store.assignDispatch(assignTarget.id, { shiftId: getText(values, 'shiftId') }),\n 'Work order assigned',\n 'Unable to assign'\n );\n setAssignTarget(null);\n }}\n />\n\n <ConfirmDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => {\n if (!open) setCancelTarget(null);\n }}\n title=\"Cancel this shift?\"\n description={\n cancelTarget ? `${cancelTarget.title} for ${cancelTarget.employeeName}` : undefined\n }\n confirmLabel=\"Cancel shift\"\n destructive\n onConfirm={async () => {\n if (cancelTarget)\n await run(() => store.cancelShift(cancelTarget.id, null), 'Shift cancelled');\n setCancelTarget(null);\n }}\n />\n\n <ConfirmDialog\n open={removeShiftTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveShiftTarget(null);\n }}\n title=\"Remove this shift?\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={async () => {\n if (removeShiftTarget)\n await run(() => store.removeShift(removeShiftTarget.id), 'Shift removed');\n setRemoveShiftTarget(null);\n }}\n />\n\n <ConfirmDialog\n open={removeDispatchTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveDispatchTarget(null);\n }}\n title=\"Remove this work order?\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={async () => {\n if (removeDispatchTarget) {\n await run(() => store.removeDispatch(removeDispatchTarget.id), 'Work order removed');\n }\n setRemoveDispatchTarget(null);\n }}\n />\n </div>\n );\n}\n"],"mappings":"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"}