1export const DATA_BOX_PLUS_TRIAGE_QUEUES = [ 2 { id: "todo", label: "K vyÅÃzenÃ" }, 3 { id: "handed", label: "PÅedané" }, 4 { id: "done", label: "Hotové" } 5]; 6 7export const DATA_BOX_PLUS_TRIAGE_MAILBOXES = [ 8 { id: "dbp-kaiser-servis", name: "Kaiser servis", company: "Kaiser servis", slot: 1 }, 9 { id: "dbp-kaiser-technology", name: "Kaiser technology", company: "Kaiser technology", slot: 2 }, 10 { id: "dbp-nanolab-plus", name: "Nanolab plus", company: "Nanolab plus", slot: 3 }, 11 { id: "dbp-nanolab-shop", name: "Nanolab shop", company: "Nanolab shop", slot: 4 }, 12 { id: "dbp-lefleur", name: "LeFleur", company: "LeFleur", slot: 5 }, 13 { id: "dbp-kaiserman-fond", name: "Kaisermanův nadaÄnà fond", company: "Kaisermanův nadaÄnà fond", slot: 6 }, 14 { id: "dbp-kaiser-holding", name: "Kaiser holding", company: "Kaiser holding", slot: 7 } 15]; 16 17export const DATA_BOX_PLUS_TRIAGE_DERIVATION_VERSION = "production-workflow-v2"; 18 19export async function readDataBoxPlusTriageSnapshot(requestJson) { 20 if (typeof requestJson !== "function") throw new TypeError("requestJson must be a function"); 21 const [statusPayload, messagesResult, draftsResult] = await Promise.all([ 22 requestJson("/api/data-box-plus/status", { method: "GET" }), 23 requestJson("/api/data-box-plus/messages?limit=150", { method: "GET" }), 24 requestJson("/api/data-box-plus/drafts?status=all", { method: "GET" }) 25 ]); 26 const mailboxes = Array.isArray(statusPayload?.mailboxes) && statusPayload.mailboxes.length 27 ? statusPayload.mailboxes 28 : DATA_BOX_PLUS_TRIAGE_MAILBOXES.map((mailbox) => ({ ...mailbox })); 29 const statusResult = { 30 ...statusPayload, 31 apiStatus: statusPayload?.apiStatus || messagesResult?.apiStatus || "ready", 32 mailboxes 33 }; 34 return [ 35 statusResult, 36 messagesResult, 37 { recommendations: [] }, 38 { rules: [] }, 39 { syncRuns: statusPayload?.latestSyncRun ? [statusPayload.latestSyncRun] : [] }, 40 draftsResult 41 ]; 42} 43 44export async function readDataBoxPlusTriageDetail(requestJson, messageId) { 45 if (typeof requestJson !== "function") throw new TypeError("requestJson must be a function"); 46 const id = String(messageId || "").trim(); 47 if (!id) throw new TypeError("messageId is required"); 48 return requestJson(`/api/data-box-plus/messages/${encodeURIComponent(id)}`, { method: "GET" }); 49} 50 51function normalizeText(value) { 52 return String(value || "") 53 .normalize("NFD") 54 .replace(/[\u0300-\u036f]/g, "") 55 .toLocaleLowerCase("cs-CZ") 56 .replace(/\s+/g, " ") 57 .trim(); 58} 59 60function joinedMessageState(message = {}) { 61 return normalizeText([ 62 message.status, 63 message.archiveStatus, 64 message.attachmentStatus, 65 message.riskLevel, 66 message.priority, 67 message.type, 68 message.messageType 69 ].filter(Boolean).join(" ")); 70} 71 72function includesAny(value, needles) { 73 return needles.some((needle) => value.includes(needle)); 74} 75 76function isoDay(value) { 77 const match = String(value || "").trim().match(/^(\d{4}-\d{2}-\d{2})/); 78 return match?.[1] || ""; 79} 80 81function pragueDay(now = new Date()) { 82 const parts = new Intl.DateTimeFormat("en-CA", { 83 timeZone: "Europe/Prague", 84 year: "numeric", 85 month: "2-digit", 86 day: "2-digit" 87 }).formatToParts(now); 88 const values = Object.fromEntries(parts.map((part) => [part.type, part.value])); 89 return `${values.year}-${values.month}-${values.day}`; 90} 91 92function deliveredTime(message = {}) { 93 const value = Date.parse(message.deliveredAt || message.receivedAt || ""); 94 return Number.isFinite(value) ? value : 0; 95} 96 97function blockerForMessage(message = {}) { 98 const status = normalizeText(message.status); 99 const attachment = normalizeText(message.attachmentStatus); 100 101 if (includesAny(attachment, ["nepodarilo", "chyba", "problem", "nedostupna", "nedostupne"]) || status.includes("chybi priloha")) { 102 return { 103 code: "attachment_problem", 104 label: "PÅÃloha se nenaÄetla", 105 actionLabel: "OtevÅÃt chybu pÅÃlohy", 106 tone: "problem" 107 }; 108 } 109 if (status.includes("potrebuje adresata")) { 110 return { 111 code: "needs_recipient", 112 label: "Je potÅeba vybrat adresáta", 113 actionLabel: "Vybrat adresáta", 114 tone: "waiting" 115 }; 116 } 117 if (status.includes("chybi vozidlo")) { 118 return { 119 code: "needs_vehicle", 120 label: "Je potÅeba vybrat vozidlo", 121 actionLabel: "Vybrat vozidlo", 122 tone: "waiting" 123 }; 124 } 125 if (includesAny(status, ["potrebuje upresnit", "nejasne", "neurceno"])) { 126 return { 127 code: "needs_classification", 128 label: "RozÅazenà nenà jisté", 129 actionLabel: "ZaÅadit ruÄnÄ", 130 tone: "waiting" 131 }; 132 }
133 if (includesAny(status, ["nelze provest", "nepodarilo", "problem", "chyba"])) { 134 return { 135 code: "action_problem", 136 label: "Akci se nepodaÅilo provést", 137 actionLabel: "OtevÅÃt chybu akce", 138 tone: "problem" 139 }; 140 } 141 return null; 142} 143 144function isDoneMessage(message = {}) { 145 const archiveStatus = normalizeText(message.archiveStatus); 146 const status = normalizeText(message.status); 147 if (archiveStatus === "archived" || archiveStatus.startsWith("archivovan")) return true; 148 if ([ 149 "archivovano", 150 "archivovane", 151 "vyreseno", 152 "completed", 153 "closed" 154 ].includes(status)) return true; 155 return [ 156 "odeslano e-mailem", 157 "odeslano datovou schrankou", 158 "odeslano sms" 159 ].some((prefix) => status === prefix || status.startsWith(`${prefix} `)); 160} 161 162function isHandedMessage(message = {}) { 163 if (String(message.assignedTo || "").trim()) return true; 164 const status = normalizeText(message.status); 165 return ["predano", "handoff", "assigned"].some((prefix) => status === prefix || status.startsWith(`${prefix} `)); 166} 167 168function isUrgentMessage(message = {}, today = pragueDay()) { 169 const state = joinedMessageState(message); 170 if (includesAny(state, ["vysoke", "kriticke", "urgent", "pravni", "legal"])) return true; 171 const dueDay = isoDay(message.dueDate); 172 return Boolean(dueDay && today && dueDay <= today); 173} 174 175function donePresentation(message = {}) { 176 const status = normalizeText(message.status); 177 if (status.includes("odeslano")) { 178 return { code: "sent", label: "Hotovo: zpráva byla odeslána", actionLabel: "Detail historie", tone: "resolved" }; 179 } 180 if (normalizeText(message.archiveStatus).startsWith("archiv") || status.includes("archiv")) { 181 return { code: "archived", label: "Hotovo: zpráva je archivovaná", actionLabel: "Detail historie", tone: "archive" }; 182 } 183 return { code: "resolved", label: "Hotovo: zpráva je vyÅÃzená", actionLabel: "Detail historie", tone: "resolved" }; 184} 185 186function handedPresentation(message = {}) { 187 const assignee = String(message.assignedTo || "").trim(); 188 return { 189 code: "waiting_owner", 190 label: assignee ? `PÅedáno: ${assignee}` : "PÅedáno odpovÄdné osobÄ", 191 actionLabel: "Detail pÅedánÃ", 192 tone: "handoff" 193 }; 194} 195 196function todoPresentation(message = {}, today = pragueDay()) { 197 const blocker = blockerForMessage(message); 198 if (blocker) return blocker; 199 200 if (isUrgentMessage(message, today)) { 201 const dueDay = isoDay(message.dueDate); 202 const pastDue = Boolean(dueDay && dueDay < today); 203 const dueToday = Boolean(dueDay && dueDay === today); 204 return { 205 code: pastDue ? "past_due" : dueToday ? "due_today" : "urgent", 206 label: pastDue ? "Po termÃnu â zkontrolovat nutný krok" : dueToday ? "TermÃn je dnes" : "Nutná kontrola pÅed pÅedánÃm", 207 actionLabel: "Zkontrolovat nutný krok", 208 tone: "problem" 209 }; 210 } 211 212 const target = dataBoxPlusTriageTarget(message); 213 if (target) { 214 return { 215 code: "suggested_route", 216 label: `DoporuÄeno: ${target}`, 217 actionLabel: "Zkontrolovat pÅedánÃ", 218 tone: "prepared" 219 }; 220 } 221 222 const status = normalizeText(message.status); 223 return { 224 code: status.includes("nov") ? "new" : "open", 225 label: status.includes("nov") ? "Nová zpráva" : "Äeká na zaÅazenÃ", 226 actionLabel: "OtevÅÃt a zaÅadit", 227 tone: "new" 228 }; 229} 230 231export function dataBoxPlusTriageQueueId(message = {}) { 232 if (isDoneMessage(message)) return "done"; 233 if (blockerForMessage(message)) return "todo"; 234 if (isHandedMessage(message)) return "handed"; 235 return "todo"; 236} 237 238export function dataBoxPlusTriageTarget(message = {}) { 239 const assignedTo = String(message.assignedTo || "").trim(); 240 if (assignedTo) return assignedTo; 241 const suggestion = String(message.recommendedAction || message.suggestedAction || "").trim(); 242 const normalized = normalizeText(suggestion); 243 if (!suggestion || includesAny(normalized, ["chat s autopilotem", "vyresit v chatu", "otevrit zpravu"])) return ""; 244 return suggestion.replace(/[.!]+$/, ""); 245} 246 247export function dataBoxPlusTriagePresentation(message = {}, options = {}) { 248 const queueId = dataBoxPlusTriageQueueId(message); 249 const today = options.today || pragueDay(options.now); 250 if (queueId === "done") return donePresentation(message); 251 if (queueId === "handed") return handedPresentation(message); 252 return todoPresentation(message, today); 253} 254 255export function dataBoxPlusTriageItem(message = {}, options = {}) { 256 const mailbox = options.mailbox || null; 257 const sentHistoryOnly = normalizeText(message.direction) === "sent"; 258 if (sentHistoryOnly) { 259 return { 260 id: String(message.id || ""), 261 mailboxId: String(message.mailboxId || ""),
262 mailboxLabel: String(mailbox?.name || mailbox?.company || "Schránka"), 263 senderName: String(message.senderName || "Datová schránka"), 264 recipientName: String(message.recipientName || message.recipientBoxId || "PÅÃjemce"), 265 subject: String(message.subject || "Datová zpráva"), 266 deliveredAt: String(message.deliveredAt || message.receivedAt || ""), 267 dueDate: "", 268 riskLevel: "", 269 priority: "", 270 sourceStatus: String(message.status || "Odesláno"), 271 assignedTo: "", 272 attachmentStatus: String(message.attachmentStatus || ""), 273 attachmentCount: Number(message.attachmentCount || 0) || (Array.isArray(message.attachments) ? message.attachments.length : 0), 274 isUnread: false, 275 target: "", 276 queueId: "done", 277 laneLabel: "Historie", 278 microstate: "sent", 279 microstateLabel: "Odesláno", 280 actionLabel: "OtevÅÃt", 281 tone: "quiet", 282 readOnly: true, 283 persisted: true, 284 derivationVersion: DATA_BOX_PLUS_TRIAGE_DERIVATION_VERSION, 285 message 286 }; 287 } 288 const queueId = dataBoxPlusTriageQueueId(message); 289 const presentation = dataBoxPlusTriagePresentation(message, options); 290 return { 291 id: String(message.id || ""), 292 mailboxId: String(message.mailboxId || ""), 293 mailboxLabel: String(mailbox?.name || mailbox?.company || "Schránka"), 294 senderName: String(message.senderName || "Datová schránka"), 295 recipientName: String(message.recipientName || message.recipientBoxId || ""), 296 subject: String(message.subject || "Datová zpráva"), 297 deliveredAt: String(message.deliveredAt || message.receivedAt || ""), 298 dueDate: String(message.dueDate || ""), 299 riskLevel: String(message.riskLevel || ""), 300 priority: String(message.priority || ""), 301 sourceStatus: String(message.status || ""), 302 assignedTo: String(message.assignedTo || ""), 303 attachmentStatus: String(message.attachmentStatus || ""), 304 attachmentCount: Number(message.attachmentCount || 0) || (Array.isArray(message.attachments) ? message.attachments.length : 0), 305 isUnread: includesAny(normalizeText([message.readStatus, message.status].filter(Boolean).join(" ")), ["neprect", "nova", "new", "unread"]), 306 target: dataBoxPlusTriageTarget(message), 307 queueId, 308 laneLabel: DATA_BOX_PLUS_TRIAGE_QUEUES.find((queue) => queue.id === queueId)?.label || "K vyÅÃzenÃ", 309 microstate: presentation.code, 310 microstateLabel: presentation.label, 311 actionLabel: presentation.actionLabel, 312 tone: presentation.tone, 313 readOnly: false, 314 persisted: true, 315 derivationVersion: DATA_BOX_PLUS_TRIAGE_DERIVATION_VERSION, 316 message 317 }; 318} 319 320export function dataBoxPlusTriageSort(items = [], queueId = "todo") { 321 return [...items].sort((left, right) => { 322 if (queueId === "done" || queueId === "handed") { 323 return deliveredTime(right.message) - deliveredTime(left.message) || left.id.localeCompare(right.id, "cs"); 324 } 325 326 const blockerRank = (item) => ({ 327 attachment_problem: 0, 328 action_problem: 0, 329 needs_recipient: 1, 330 needs_vehicle: 1, 331 needs_classification: 1, 332 past_due: 2, 333 due_today: 3, 334 urgent: 4, 335 new: 5, 336 suggested_route: 6, 337 open: 7 338 }[item.microstate] ?? 8); 339 const rankDifference = blockerRank(left) - blockerRank(right); 340 if (rankDifference) return rankDifference; 341 342 const leftDue = isoDay(left.dueDate) || "9999-12-31"; 343 const rightDue = isoDay(right.dueDate) || "9999-12-31"; 344 if (leftDue !== rightDue) return leftDue.localeCompare(rightDue); 345 346 const leftRisk = includesAny(normalizeText([left.riskLevel, left.priority].join(" ")), ["vysoke", "kriticke", "urgent", "pravni", "legal"]) ? 0 : 1; 347 const rightRisk = includesAny(normalizeText([right.riskLevel, right.priority].join(" ")), ["vysoke", "kriticke", "urgent", "pravni", "legal"]) ? 0 : 1; 348 if (leftRisk !== rightRisk) return leftRisk - rightRisk; 349 350 return deliveredTime(left.message) - deliveredTime(right.message) || left.id.localeCompare(right.id, "cs"); 351 }); 352} 353 354export function dataBoxPlusTriageItems(messages = [], mailboxes = [], options = {}) { 355 const mailboxMap = new Map(mailboxes.map((mailbox) =>
355 [String(mailbox.id || ""), mailbox])); 356 const mailboxId = String(options.mailboxId || ""); 357 const queueId = String(options.queueId || ""); 358 const query = normalizeText(options.query); 359 const folder = String(options.folder || "received"); 360 const today = options.today || pragueDay(options.now); 361 362 const items = messages 363 .filter((message) => { 364 const direction = normalizeText(message?.direction); 365 if (folder === "sent" && direction !== "sent") return false; 366 if (folder === "archive" && (direction === "sent" || dataBoxPlusTriageQueueId(message) !== "done")) return false; 367 if (folder === "received" && direction && direction !== "received") return false; 368 if (mailboxId && String(message?.mailboxId || "") !== mailboxId) return false; 369 return true; 370 }) 371 .map((message) => dataBoxPlusTriageItem(message, { mailbox: mailboxMap.get(String(message.mailboxId || "")), today })) 372 .filter((item) => !queueId || item.queueId === queueId) 373 .filter((item) => { 374 if (!query) return true; 375 return normalizeText([ 376 item.senderName, 377 item.recipientName, 378 item.subject, 379 item.mailboxLabel, 380 item.microstateLabel, 381 item.target, 382 item.sourceStatus, 383 item.riskLevel, 384 item.priority 385 ].join(" ")).includes(query); 386 }); 387 388 return dataBoxPlusTriageSort(items, queueId || "todo"); 389} 390 391export function dataBoxPlusTriageCounts(messages = [], mailboxes = [], options = {}) { 392 const counts = { todo: 0, handed: 0, done: 0 }; 393 for (const item of dataBoxPlusTriageItems(messages, mailboxes, { 394 mailboxId: options.mailboxId, 395 folder: options.folder || "received", 396 today: options.today, 397 now: options.now 398 })) { 399 counts[item.queueId] += 1; 400 } 401 return counts; 402}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.