- Thread request data through UI bridge and dispatcher events - Add task models, repositories, services, and extension wiring - Include submitted request fields in converted order notes
1 line
22 KiB
JavaScript
1 line
22 KiB
JavaScript
window.cadDispatcherFormatters={getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,n="danger"===(t.status||"")?0:1;if(s!==n)return s-n;const r=e.callsign||e.groupId||"",i=t.callsign||t.groupId||"";return r.localeCompare(i)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestTypeLabel(e){switch(e){case"medevac_9line":return"9-Line MEDEVAC";case"ace_lace":return"ACE/LACE";case"fire_support":return"Fire Support";case"air_support":return"Air Support";case"logreq":return"LOGREQ";default:return(e||"request").replaceAll("_"," ")}},buildGroupOptions(e){return this.getSortedGroups().map(t=>{const s=t.groupId||"";return`<option value="${s}" ${s===e?"selected":""}>${t.callsign||s}</option>`}).join("")},formatRequestFieldLabel:e=>(e||"field").replaceAll("_"," ").replace(/\b\w/g,e=>e.toUpperCase()),formatRequestFieldValue(e){if(Array.isArray(e))return e.join(", ");if(e&&"object"==typeof e)return JSON.stringify(e);return String(e??"").trim()||"Not provided"},buildRequestOrderNote(e){const t=this.getRequestTypeLabel(e.type||"request"),s=e.groupCallsign||e.groupId||"Unknown Group",n=(e.summary||"").trim(),r=e.fields&&"object"==typeof e.fields?Object.entries(e.fields).map(([e,t])=>{const s=this.formatRequestFieldValue(t);return"Not provided"===s?"":`${this.formatRequestFieldLabel(e)} ${s}`}).filter(Boolean):[],i=r.length?r:[n].filter(Boolean);return i.length?`${t} requested by ${s}. ${i.join(" | ")}`:`${t} requested by ${s}.`}},window.cadDispatcherModals={openOrderModal(){this.convertingRequestId="",this.populateOrderModal(),document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden")},closeOrderModal(){this.convertingRequestId="",document.getElementById("dispatcherOrderNoteInput").value="",document.getElementById("dispatcherOrderPrioritySelect").value="priority",document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.add("is-hidden")},openRequestModal(e){const t=this.requests.find(t=>t.requestId===e);t&&(this.viewingRequestId=e,this.populateRequestModal(t),document.getElementById("dispatcherRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.viewingRequestId="",document.getElementById("dispatcherRequestModal").classList.add("is-hidden")},syncRequestModal(){if(!this.viewingRequestId)return;const e=this.requests.find(e=>e.requestId===this.viewingRequestId);e?this.populateRequestModal(e):this.closeRequestModal()},populateRequestModal(e){const t=e.fields&&"object"==typeof e.fields?Object.entries(e.fields):[],s=t.length?t.map(([e,t])=>`\n <div class="dispatch-detail-row">\n <span class="dispatch-detail-label">${this.formatRequestFieldLabel(e)}</span>\n <span class="dispatch-detail-value">${this.formatRequestFieldValue(t)}</span>\n </div>\n `).join(""):'<div class="placeholder-message"><p>No submitted fields.</p></div>';document.getElementById("dispatcherRequestTitle").textContent=e.title||e.requestId||"Support Request",document.getElementById("dispatcherRequestPriority").textContent=(e.priority||"priority").replaceAll("_"," "),document.getElementById("dispatcherRequestGroup").textContent=e.groupCallsign||e.groupId||"Unknown",document.getElementById("dispatcherRequestType").textContent=this.getRequestTypeLabel(e.type||"request"),document.getElementById("dispatcherRequestSummary").textContent=e.summary||"No summary provided.",document.getElementById("dispatcherRequestFields").innerHTML=s},convertRequestToOrder(e){const t=this.requests.find(t=>(t.requestId||"")===e);if(!t)return void this.setStatus("Selected request is no longer available.","error");const s=t.groupId||"";if(!s)return void this.setStatus("Selected request has no owning group to target.","error");this.groups.find(e=>(e.groupId||"")===s)?(this.convertingRequestId=e,this.populateOrderModal({selectedAssigneeID:this.getSortedGroups().find(e=>(e.groupId||"")!==s)?.groupId||"",selectedTargetID:s,note:this.buildRequestOrderNote(t),priority:t.priority||"priority"}),document.getElementById("dispatcherOrderModalTitle").textContent="Create Order From Request",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden"),this.setStatus("Preparing dispatch order from request...","info")):this.setStatus("Selected request group is no longer available.","error")},convertViewedRequestToOrder(){if(!this.viewingRequestId)return;const e=this.viewingRequestId;this.closeRequestModal(),this.convertRequestToOrder(e)},populateOrderModal(e={}){const t=this.getSortedGroups(),s=document.getElementById("dispatcherOrderAssigneeSelect"),n=document.getElementById("dispatcherOrderTargetSelect"),r=document.getElementById("dispatcherOrderNoteInput"),i=document.getElementById("dispatcherOrderPrioritySelect");if(!s||!n)return;const d=e.selectedAssigneeID||"",a=e.selectedTargetID||"",o=d||t.find(e=>(e.groupId||"")!==a)?.groupId||t[0]?.groupId||"",c=a||t.find(e=>(e.groupId||"")!==o)?.groupId||t[0]?.groupId||"";s.innerHTML=this.buildGroupOptions(o),n.innerHTML=this.buildGroupOptions(c),r&&(r.value=e.note||""),i&&(i.value=e.priority||"priority")},syncOrderModal(){const e=document.getElementById("dispatcherOrderModal");e&&!e.classList.contains("is-hidden")&&this.populateOrderModal({selectedAssigneeID:document.getElementById("dispatcherOrderAssigneeSelect")?.value||"",selectedTargetID:document.getElementById("dispatcherOrderTargetSelect")?.value||"",note:document.getElementById("dispatcherOrderNoteInput")?.value||"",priority:document.getElementById("dispatcherOrderPrioritySelect")?.value||"priority"})},openGroupModal(e){const t=this.groups.find(t=>t.groupId===e);t&&(this.editingGroupId=e,document.getElementById("dispatcherModalGroupCallsign").textContent=t.callsign||t.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=t.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=t.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=t.orgId||"default",document.getElementById("dispatcherModalRoleSelect").innerHTML=this.roles.map(e=>`<option value="${e}" ${e===t.role?"selected":""}>${e.replaceAll("_"," ")}</option>`).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(e=>`<option value="${e}" ${e===t.status?"selected":""}>${e.replaceAll("_"," ")}</option>`).join(""),document.getElementById("dispatcherGroupModal").classList.remove("is-hidden"))},closeGroupModal(){this.editingGroupId="",document.getElementById("dispatcherGroupModal").classList.add("is-hidden")},syncOpenModal(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);e?(document.getElementById("dispatcherModalGroupCallsign").textContent=e.callsign||e.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=e.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=e.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=e.orgId||"default"):this.closeGroupModal()}},window.cadDispatcherRender={updateDangerAlert(){const e=document.getElementById("dispatcherDangerAlert");if(!e)return;const t=this.getDangerGroups();if(!t.length)return e.textContent="",void e.classList.add("is-hidden");const s=t.map(e=>e.callsign||e.groupId||"Unknown Group");e.textContent=`Danger alert active: ${s.join(", ")}`,e.classList.remove("is-hidden")},updateRequestAlert(){const e=document.getElementById("dispatcherRequestAlert");if(!e)return;const t=this.buildSupportAlertMessage();if(!t)return e.textContent="",void e.classList.add("is-hidden");e.textContent=t,e.classList.remove("is-hidden")},buildGroupEditorButton:e=>`\n <button\n type="button"\n class="dispatch-icon-btn"\n onclick="window.cadDispatcher.openGroupModal('${e}')"\n aria-label="Edit group"\n title="Edit group"\n >\n ⚙\n </button>\n `,buildCloseOrderButton:e=>`\n <button\n type="button"\n class="dispatch-btn dispatch-btn-secondary"\n onclick="window.cadDispatcher.closeDispatchOrder('${e}')"\n >\n Close\n </button>\n `,buildCloseRequestButton:e=>`\n <button\n type="button"\n class="dispatch-btn dispatch-btn-secondary"\n onclick="event.stopPropagation(); window.cadDispatcher.closeSupportRequest('${e}')"\n >\n Close\n </button>\n `,buildConvertRequestButton:e=>`\n <button\n type="button"\n class="dispatch-btn"\n onclick="event.stopPropagation(); window.cadDispatcher.convertRequestToOrder('${e}')"\n >\n Convert to Order\n </button>\n `,renderMetrics(){const e=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned")),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned")),s=this.requests.length,n=this.getSupportAlertRequests(),r=this.groups.filter(e=>"danger"===(e.status||""));document.getElementById("metricOpenContracts").textContent=t.length,document.getElementById("metricAssignedContracts").textContent=e.length,document.getElementById("metricActiveGroups").textContent=this.groups.length,document.getElementById("metricOpenRequests").textContent=s,document.getElementById("metricDangerGroups").textContent=r.length;const i=document.getElementById("metricDangerGroupsCard");i&&i.classList.toggle("is-danger",r.length>0);const d=document.getElementById("metricOpenRequestsCard");d&&d.classList.toggle("is-warning",n.length>0)},renderOpenContracts(){const e=document.getElementById("dispatcherOpenContracts"),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned"));if(!t.length)return void(e.innerHTML='<div class="placeholder-message"><p>No open contracts.</p></div>');const s=this.buildGroupOptions("");e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",n=Array.isArray(e.position)?e.position:[0,0,0],r=this.groups.find(t=>t.groupId===(e.targetGroupId||""));return`\n <article class="dispatch-card">\n <header class="dispatch-card-header">\n <strong>${e.title||t}</strong>\n <span class="dispatch-badge">${this.formatTypeLabel(e)}</span>\n </header>\n <p class="dispatch-description">${e.description||""}</p>\n <div class="dispatch-meta">\n <span>Unassigned</span>\n <span>${window.mapUI.formatPosition(n)}</span>\n </div>\n <div class="dispatch-meta">\n <span>Target: ${r?r.callsign:e.targetGroupCallsign||"None"}</span>\n <span>Priority: ${(e.priority||"priority").replaceAll("_"," ")}</span>\n </div>\n <div class="dispatch-actions">\n <select id="dispatcher-assign-group-${t}" class="dispatch-select">\n <option value="">Assign to group</option>\n ${s}\n </select>\n <button type="button" class="dispatch-btn" onclick="window.cadDispatcher.assignTask('${t}')">Assign</button>\n </div>\n </article>\n `}).join("")},renderAssignedContracts(){const e=document.getElementById("dispatcherAssignedContracts"),t=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned"));t.length?e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=this.groups.find(t=>t.groupId===(e.assignedGroupId||"")),n=this.groups.find(t=>t.groupId===(e.targetGroupId||"")),r=this.isDispatchOrder(e);return`\n <article class="dispatch-card">\n <header class="dispatch-card-header">\n <strong>${e.title||t}</strong>\n <span class="dispatch-badge">${e.assignmentState||"assigned"}</span>\n </header>\n <p class="dispatch-description">${e.description||""}</p>\n <div class="dispatch-meta">\n <span>Group: ${s?s.callsign:e.assignedGroupId||"Unknown"}</span>\n <span>Type: ${this.formatTypeLabel(e)}</span>\n </div>\n <div class="dispatch-meta">\n <span>Target: ${n?n.callsign:e.targetGroupCallsign||"None"}</span>\n <span>Priority: ${(e.priority||"priority").replaceAll("_"," ")}</span>\n </div>\n ${r?`<div class="dispatch-actions dispatch-actions-split">${this.buildCloseOrderButton(t)}</div>`:""}\n </article>\n `}).join(""):e.innerHTML='<div class="placeholder-message"><p>No assigned contracts.</p></div>'},renderGroups(){const e=document.getElementById("dispatcherGroups");this.groups.length?e.innerHTML=this.getSortedGroups().map(e=>{const t="danger"===(e.status||"");return`\n <article class="dispatch-card dispatch-card-group ${t?"is-danger":""}">\n <header class="dispatch-card-header">\n <div class="dispatch-card-header-main">\n <strong>${e.callsign||e.groupId}</strong>\n <span class="dispatch-badge">${e.role||"group"}</span>\n ${t?'<span class="dispatch-alert-badge">Danger</span>':""}\n </div>\n <div class="dispatch-card-header-actions">\n ${this.buildGroupEditorButton(e.groupId)}\n </div>\n </header>\n <div class="dispatch-meta">\n <span>Leader: ${e.leaderName||"Unknown"}</span>\n <span>Status: ${e.status||"unknown"}</span>\n </div>\n <div class="dispatch-meta">\n <span>Org: ${e.orgId||"default"}</span>\n <span>Task: ${e.currentTaskId||"None"}</span>\n </div>\n </article>\n `}).join(""):e.innerHTML='<div class="placeholder-message"><p>No active groups available.</p></div>'},renderActivity(){const e=document.getElementById("dispatcherActivity"),t=this.requests.length?this.requests.map(e=>`\n <article class="dispatch-card dispatch-card-interactive ${["medevac_9line","fire_support","air_support"].includes(e.type||"")?"is-warning":""}" onclick="window.cadDispatcher.openRequestModal('${e.requestId||""}')">\n <header class="dispatch-card-header">\n <strong>${e.title||e.requestId||"Support Request"}</strong>\n <span class="dispatch-badge">${(e.priority||"priority").replaceAll("_"," ")}</span>\n </header>\n <p class="dispatch-description">${e.summary||""}</p>\n <div class="dispatch-meta">\n <span>Group: ${e.groupCallsign||e.groupId||"Unknown"}</span>\n <span>${this.getRequestTypeLabel(e.type||"request")}</span>\n </div>\n <div class="dispatch-actions dispatch-actions-split">\n ${this.buildConvertRequestButton(e.requestId||"")}\n ${this.buildCloseRequestButton(e.requestId||"")}\n </div>\n </article>\n `).join(""):'<div class="placeholder-message"><p>No active support requests.</p></div>',s=this.activity.length?this.activity.slice().reverse().slice(0,8).map(e=>`\n <article class="dispatch-card">\n <header class="dispatch-card-header">\n <strong>${e.type||"activity"}</strong>\n <span class="dispatch-badge">${Math.round(e.timestamp||0)}s</span>\n </header>\n <p class="dispatch-description">${e.message||""}</p>\n </article>\n `).join(""):'<div class="placeholder-message"><p>No recent activity.</p></div>';e.innerHTML=`\n <div class="dispatch-inline-section">\n <div class="dispatch-inline-header">Support Requests</div>\n ${t}\n </div>\n <div class="dispatch-inline-section">\n <div class="dispatch-inline-header">Recent Activity</div>\n ${s}\n </div>\n `},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.renderMetrics(),this.renderOpenContracts(),this.renderAssignedContracts(),this.renderGroups(),this.renderActivity()}};const dispatcherFormatters=window.cadDispatcherFormatters||{},dispatcherModals=window.cadDispatcherModals||{},dispatcherRender=window.cadDispatcherRender||{};window.cadDispatcher={contracts:[],requests:[],groups:[],activity:[],session:{},editingGroupId:"",viewingRequestId:"",convertingRequestId:"",statuses:["available","en_route","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],...dispatcherFormatters,...dispatcherModals,...dispatcherRender,init(){document.getElementById("dispatcherCreateOrderBtn").addEventListener("click",()=>{this.openOrderModal()}),document.getElementById("dispatcherGroupModalCloseBtn").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherGroupModalSaveBtn").addEventListener("click",()=>{this.applyGroupUpdates()}),document.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherOrderModalCloseBtn").addEventListener("click",()=>{this.closeOrderModal()}),document.getElementById("dispatcherOrderModalSaveBtn").addEventListener("click",()=>{this.createDispatchOrder()}),document.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeOrderModal()}),document.getElementById("dispatcherRequestModalCloseBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestModalDoneBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestConvertBtn").addEventListener("click",()=>{this.convertViewedRequestToOrder()}),document.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.mapUI.sendEvent("cad::dispatcher::ready",{})},receiveHydrate(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],this.session=e.session&&"object"==typeof e.session?e.session:{};const t=document.getElementById("dispatcherStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.syncOpenModal(),this.syncOrderModal(),this.syncRequestModal(),this.render()},setStatus(e,t){const s=document.getElementById("dispatcherStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},createDispatchOrder(){const e=document.getElementById("dispatcherOrderAssigneeSelect").value,t=document.getElementById("dispatcherOrderTargetSelect").value,s=document.getElementById("dispatcherOrderPrioritySelect").value,n=document.getElementById("dispatcherOrderNoteInput").value,r=this.convertingRequestId&&this.requests.find(e=>(e.requestId||"")===this.convertingRequestId)||null;e&&t?e!==t?(this.setStatus(this.convertingRequestId?"Creating dispatch order from request...":"Creating dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::create",{assigneeGroupID:e,targetGroupID:t,note:n.trim(),priority:s,request:r?{requestId:r.requestId||"",type:r.type||"",title:r.title||"",summary:r.summary||"",fields:r.fields&&"object"==typeof r.fields?r.fields:{}}:{}}),this.closeOrderModal()):this.setStatus("Assignee and target groups must be different.","error"):this.setStatus("Select both an assignee and a target group.","error")},assignTask(e){const t=document.getElementById(`dispatcher-assign-group-${e}`);t&&t.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:e,groupID:t.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},applyGroupUpdates(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);if(!e)return void this.closeGroupModal();const t=document.getElementById("dispatcherModalRoleSelect").value,s=document.getElementById("dispatcherModalStatusSelect").value,n=t&&t!==(e.role||"")?t:"",r=s&&s!==(e.status||"")?s:"";if(!(n||r))return this.setStatus("No group changes to save.","info"),void this.closeGroupModal();this.setStatus("Updating group profile...","info"),window.mapUI.sendEvent("cad::groups::profile",{groupID:this.editingGroupId,role:n,status:r}),this.closeGroupModal()},closeDispatchOrder(e){e&&(this.setStatus("Closing dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::close",{taskID:e}))},closeSupportRequest(e){e&&(this.setStatus("Closing support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))}},window.cadDispatcher.init(); |