import { toIdempiereDate } from './idempiereDate' import { sendFcmToUser } from './fcmNotifier' import { sendPushToUser } from './pushNotifier' /** * Push an existing ticket (r_request) to a mobile worker as a priority task. * * Shared by `requests/requests/[id]/push-task.post.ts` (ticket detail view) and * `orders/[id]/roadmap/push-step.post.ts` (order roadmap). One round trip on the * ticket: SalesRep_ID → worker, IsPushedTask/PushedTaskAt flagged, TaskStatus '0' * (= not started; push-task-status.post.ts moves it on). Then the dispatcher's * note is stored as an INTERNAL chat entry ("[Anweisung] …", never shown to the * merchant) and both push channels fire in parallel (native FCM for the * Capacitor app, web push for the PWA). * * Returns `{ status: 200, pushedTaskAt }` or `{ status: 500, message }` when the * ticket write failed (e.g. the custom columns IsPushedTask/PushedTaskAt don't * exist yet) — in that case nothing is pushed. */ export interface PushTaskOpts { targetUserId: number targetUserName?: string note?: string } export const pushTicketToWorker = async (event: any, token: any, ticketId: number | string, opts: PushTaskOpts) => { const id = Number(ticketId) const targetUserId = Number(opts?.targetUserId) if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(targetUserId) || targetUserId <= 0) { return { status: 400, message: 'targetUserId ist erforderlich' } } const ticket: any = await event.context.fetch( `models/r_request/${id}?$select=DocumentNo,Name,Summary,AD_Org_ID`, 'GET', token, null ) if (!ticket?.id) return { status: 404, message: 'Ticket nicht gefunden' } const docNo = ticket.DocumentNo || String(id) const subject = String(ticket.Name || '').trim() || String(ticket.Summary || '').trim().slice(0, 140) const note = (typeof opts?.note === 'string' ? opts.note.trim() : '').slice(0, 1000) const targetUserName = (typeof opts?.targetUserName === 'string' ? opts.targetUserName.trim() : '').slice(0, 120) const pushedTaskAt = toIdempiereDate(new Date()) try { await event.context.fetch(`models/r_request/${id}`, 'PUT', token, { SalesRep_ID: { id: targetUserId, tableName: 'AD_User' }, isPushedTask: true, pushedTaskAt, // Standard R_Request list column: '0' = not started. Flips to '2' when the // worker starts the timer (push-task-status.post.ts) → desktop "In Arbeit". TaskStatus: { id: '0' }, tableName: 'R_Request' }) } catch (e: any) { console.error('[push-task] ticket update failed (IsPushedTask/PushedTaskAt missing in iDempiere?):', e?.message) return { status: 500, message: 'Aufgabe konnte nicht gepusht werden — evtl. fehlen die Felder IsPushedTask/PushedTaskAt in iDempiere.' } } // Internal instruction note — fail-soft, the push itself already went out. if (note) { try { await event.context.fetch('models/r_requestupdate', 'POST', token, { AD_Org_ID: { id: ticket?.AD_Org_ID?.id, tableName: 'AD_Org' }, R_Request_ID: { id, tableName: 'R_Request' }, ConfidentialTypeEntry: { id: 'I' }, isActive: true, qtySpent: 0, qtyInvoiced: 0, result: `[Anweisung] für ${targetUserName || 'Mitarbeiter'}\n${note}`, tableName: 'R_RequestUpdate' }) } catch (e: any) { console.warn('[push-task] instruction note failed:', e?.message) } } const title = `📌 Neue priorisierte Aufgabe — #${docNo}` const pushBody = [subject, note].filter(Boolean).join('\n').slice(0, 400) || 'Ihnen wurde eine Aufgabe zugewiesen.' const data = { type: 'pushed_task', ticketId: String(id), docNo } await Promise.allSettled([ sendFcmToUser(targetUserId, { title, body: pushBody, data }), sendPushToUser(targetUserId, { title, body: pushBody, data, url: `/mobile/tasks?pushedTaskId=${id}`, icon: '/favicon.ico', badge: '/favicon.ico', topic: `pushed-task-${id}` }) ]) return { status: 200, message: '', pushedTaskAt, docNo } }