Spaces:
Sleeping
Sleeping
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,
};
}
|