File size: 9,411 Bytes
45e997f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
'use strict';

const BOX_WORKFLOW_TEMPLATE = 'pothole_box_ops_v1';

function workflowPriorityFromSeverity(severity) {
  const normalized = String(severity || '').toLowerCase();
  if (normalized === 'critical') return 'urgent';
  if (normalized === 'high' || normalized === 'large') return 'high';
  if (normalized === 'medium') return 'medium';
  return 'normal';
}

function buildInitialWorkflow(report = {}, boxConfig = {}) {
  const aiStatus = String(report.aiReviewStatus || '').toLowerCase();
  const aiResult = String(report.aiResult || report.aiPrediction || '').toLowerCase();
  const needsManualReview = aiStatus.includes('manual') || aiStatus.includes('face');
  const needsAttention = aiStatus === 'invalid_image';

  return {
    template: BOX_WORKFLOW_TEMPLATE,
    currentStage: 'incoming_detection',
    currentStageLabel: 'Incoming Detection',
    status: needsManualReview || needsAttention ? 'Under Review' : 'Submitted',
    queue: needsManualReview || needsAttention ? 'Manual Review' : 'Ops Intake',
    nextAction: needsAttention
      ? 'Supervisor must validate the upload before enrichment'
      : needsManualReview
        ? 'Supervisor must verify the image before enrichment'
        : 'Run enrichment and create the case folder',
    priority: workflowPriorityFromSeverity(aiResult),
    needsManualReview,
    triggerFolderId: boxConfig.folderId || '',
    triggerFolderName: boxConfig.folderName || 'Incoming Detections',
    submittedAt: report.submittedAt || new Date().toISOString(),
    lastUpdatedAt: report.submittedAt || new Date().toISOString(),
  };
}

function buildEnrichedWorkflow(report = {}, enrichment = {}, context = {}) {
  const severityLevel = String(enrichment.severityLevel || '').trim() || 'Pending';
  const duplicateCount = enrichment.duplicate?.duplicateCount || 0;
  const linkedCases = Array.isArray(enrichment.duplicate?.linkedCaseIds)
    ? enrichment.duplicate.linkedCaseIds
    : [];
  const urgent = severityLevel.toLowerCase() === 'critical';

  return {
    template: BOX_WORKFLOW_TEMPLATE,
    currentStage: 'supervisor_review',
    currentStageLabel: 'Supervisor Review',
    status: 'Under Review',
    queue: urgent ? 'Emergency Dispatch' : 'Supervisor Review',
    nextAction: urgent
      ? 'Assign an emergency crew and dispatch immediately'
      : duplicateCount > 0
        ? 'Review duplicate cluster and confirm routing'
        : 'Supervisor approve the case and send it to crew routing',
    priority: workflowPriorityFromSeverity(severityLevel),
    needsManualReview: String(report.aiReviewStatus || '').toLowerCase().includes('manual'),
    severityLevel,
    severityScore: enrichment.severityScore ?? '',
    duplicateCount,
    linkedCases: linkedCases.join(', '),
    district: enrichment.district?.district || report.district || '',
    ward: enrichment.district?.ward || report.ward || '',
    maintenanceZone: enrichment.district?.maintenanceZone || report.maintenanceZone || '',
    boxPath: context.folderPath || '',
    rootFolderId: context.rootFolderId || '',
    lastUpdatedAt: enrichment.enrichedAt || new Date().toISOString(),
  };
}

function flattenWorkflowForBox(workflow = {}) {
  return {
    workflowTemplate: workflow.template || BOX_WORKFLOW_TEMPLATE,
    workflowStage: workflow.currentStage || '',
    workflowStageLabel: workflow.currentStageLabel || '',
    workflowStatus: workflow.status || '',
    workflowQueue: workflow.queue || '',
    workflowNextAction: workflow.nextAction || '',
    workflowPriority: workflow.priority || '',
    workflowNeedsManualReview: workflow.needsManualReview ? 'true' : 'false',
    workflowSeverityLevel: workflow.severityLevel || '',
    workflowSeverityScore: workflow.severityScore ?? '',
    workflowDuplicateCount: workflow.duplicateCount ?? '',
    workflowLinkedCases: workflow.linkedCases || '',
    workflowWard: workflow.ward || '',
    workflowDistrict: workflow.district || '',
    workflowMaintenanceZone: workflow.maintenanceZone || '',
    workflowBoxPath: workflow.boxPath || '',
    workflowLastUpdatedAt: workflow.lastUpdatedAt || '',
    workflowTriggerFolderId: workflow.triggerFolderId || workflow.rootFolderId || '',
    workflowTriggerFolderName: workflow.triggerFolderName || '',
  };
}

function getWorkflowInputFields() {
  return [
    { key: 'caseId', source: 'Citizen upload', purpose: 'Primary record key used across Box, ops review, and tracking.' },
    { key: 'submittedAt', source: 'Citizen upload', purpose: 'Timestamp for intake SLAs and queue sorting.' },
    { key: 'photoFileId', source: 'Box upload', purpose: 'Original pothole photo file to preview or move into the case folder.' },
    { key: 'sidecarFileId', source: 'Box upload', purpose: 'Original JSON sidecar used as the workflow payload.' },
    { key: 'address', source: 'Citizen upload', purpose: 'Road location shown to reviewers and crew dispatch.' },
    { key: 'areaLabel', source: 'Reverse geocode', purpose: 'Human-friendly locality label shown before official ward matching succeeds.' },
    { key: 'ward', source: 'GIS or reverse geocode', purpose: 'Routes the case into the correct Box subfolder and maintenance area.' },
    { key: 'district', source: 'GIS mapping', purpose: 'Maps the case to the review or engineering district.' },
    { key: 'maintenanceZone', source: 'GIS mapping', purpose: 'Directs dispatch to the correct crew zone.' },
    { key: 'duplicateStatus', source: 'Ops enrichment', purpose: 'Simple duplicate/new label used by external workflow decisions.' },
    { key: 'weatherStatus', source: 'Ops enrichment', purpose: 'Weather risk status stored with the case for triage and reporting.' },
    { key: 'potholeSize', source: 'Citizen upload AI', purpose: 'Human-readable pothole size used before and after enrichment.' },
    { key: 'aiReviewStatus', source: 'Citizen upload AI', purpose: 'Determines whether the case should enter manual review first.' },
    { key: 'aiResult', source: 'Citizen upload AI', purpose: 'Initial pothole size signal before enrichment recalculates severity.' },
    { key: 'severityScore', source: 'Ops enrichment', purpose: 'Numeric urgency score for routing, triage, and reporting.' },
    { key: 'severityLevel', source: 'Ops enrichment', purpose: 'Human-readable priority label for Box tasks and dashboards.' },
    { key: 'isDuplicate', source: 'Ops enrichment', purpose: 'Flags repeat reports so teams can merge or escalate clusters.' },
    { key: 'workflowStage', source: 'Workflow metadata', purpose: 'Stable machine field for the current lifecycle stage.' },
    { key: 'workflowNextAction', source: 'Workflow metadata', purpose: 'Plain-language instruction shown to the next owner.' },
  ];
}

function getWorkflowStages() {
  return [
    {
      id: 'incoming_detection',
      title: '1. Citizen intake and Box file storage',
      use: 'The portal uploads the photo and JSON sidecar to Box, then upserts the same case into the backend queue.',
      input: 'Folder ID from Settings, caseId, submittedAt, address, location, aiReviewStatus, potholeSize',
    },
    {
      id: 'supervisor_review',
      title: '2. Database-owned enrichment and review',
      use: 'Ops Review and backend services calculate GIS, duplicates, weather, and severity, then save enriched artifacts back into Box.',
      input: 'photoFileId, sidecarFileId, ward, district, maintenanceZone, duplicateStatus, weatherStatus, severityScore',
    },
    {
      id: 'crew_dispatch',
      title: '3. Database dispatch with Box-backed artifacts',
      use: 'The backend records supervisor approval, authority routing, and crew dispatch while Box stores the generated notices and assignment files.',
      input: 'severityLevel, duplicateStatus, workflowPriority, workflowStatus, maintenanceZone, assignedCrew',
    },
    {
      id: 'resolved',
      title: '4. Database closeout with Box proof storage',
      use: 'Crew forms, after photos, reports, and audit outputs are saved by the backend and written back into Box for shared recordkeeping.',
      input: 'resolvedAt, resolutionNotes, afterPhotoFileId, laborHours, materialsUsed, workflowStatus',
    },
  ];
}

function buildWorkflowBlueprint(boxConfig = {}) {
  return {
    template: BOX_WORKFLOW_TEMPLATE,
    triggerFolderId: boxConfig.folderId || '',
    triggerFolderName: boxConfig.folderName || 'Incoming Detections',
    automationUses: [
      'Box as the content repository for intake, case folders, proof files, and reports',
      'JSON sidecar payload uploaded by the citizen portal',
      'Enriched metadata and readable summaries written back after backend processing',
      'Backend and app screens own approvals, dispatch, authority notifications, and closeout',
    ],
    inputFields: getWorkflowInputFields(),
    stages: getWorkflowStages(),
    metadataKeys: Object.keys(flattenWorkflowForBox({})),
  };
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    BOX_WORKFLOW_TEMPLATE,
    buildInitialWorkflow,
    buildEnrichedWorkflow,
    flattenWorkflowForBox,
    getWorkflowInputFields,
    getWorkflowStages,
    buildWorkflowBlueprint,
  };
}

if (typeof window !== 'undefined') {
  window.BoxWorkflow = {
    BOX_WORKFLOW_TEMPLATE,
    buildInitialWorkflow,
    buildEnrichedWorkflow,
    flattenWorkflowForBox,
    getWorkflowInputFields,
    getWorkflowStages,
    buildWorkflowBlueprint,
  };
}