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