Overhaul the app's color system by replacing all hardcoded Tailwind color classes with semantic theme variables to ensure consistent theming across all components, pages, and tests. Additional changes: - Add assigned date support for order item status updates: add `assignedDate` field to the order item status PATCH API, implement server-side normalization/validation, and add corresponding tests - Refactor purchase order PDF workflows to use the consolidated `purchase_orders.documents` field instead of the legacy separate `purchase_order_pdfs` collection - Fix quote number parsing and formatting to support both dashed (Q-YYYY-MM-NNN) and dashless (QYYYYMMNNN) formats - Rename `driver_pool` item status to `collection_pool` across the codebase - Add null handling for order item quantities and pricing to display "—" for missing values - Improve filter tab styling: make mobile tabs scrollable, prevent text wrapping, hide scrollbar - Add new utility scripts, API routes, migrations, and polish small UI/UX fixes including tsconfig updates and theme hook improvements
114 lines
No EOL
7 KiB
JSON
114 lines
No EOL
7 KiB
JSON
{
|
|
"name": "WFDASH — Purchase Order Send",
|
|
"description": "Sends supplier PO emails with the latest PO PDF attached. Fired by Next.js route /api/v1/purchase-orders/[id]/send-email after it builds the full render-ready payload (recipient + subject + body + PDF attachment URL + AGENT_API_KEY + dashboard base URL). n8n never reads PocketBase or env vars directly. Records success/failure back to the dashboard via the outcome callback route.",
|
|
"tags": [
|
|
{ "name": "wfdash", "id": "wf-tag-wfdash" }
|
|
],
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "wf-po-send",
|
|
"responseMode": "lastNode",
|
|
"options": {}
|
|
},
|
|
"id": "wf-po-send-webhook",
|
|
"name": "PO Send Webhook",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2,
|
|
"position": [0, 0],
|
|
"webhookId": "wf-po-send"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Resolve any attachment URLs into { name, content (base64), type }\n// entries the n8n emailSend node accepts. Returns the original input\n// shape unchanged when there are no attachments.\n//\n// n8n Code node: no $env.* access in this environment\n// (N8N_BLOCK_ENV_ACCESS_IN_NODE=true in docker-compose). The dashboard\n// route pushes everything we need (incl. the agentApiKey Bearer\n// credential) on the webhook body. The file proxy whitelist now\n// includes purchase_orders so the GET succeeds without further auth\n// headers from the cookie-less n8n-worker.\nconst first = ($input.first() || {}).json || {};\nconst body = first.body || first;\nconst attachments = body.attachments || [];\nconst agentApiKey = body.agentApiKey || '';\n\nif (!Array.isArray(attachments) || attachments.length === 0) {\n return [{ json: { ...first, body: { ...body, attachments: [] }, attachmentsResolved: [] } }];\n}\n\nconst resolved = [];\nfor (const att of attachments) {\n try {\n const res = await this.helpers.httpRequest({\n method: 'GET',\n url: att.url,\n headers: agentApiKey\n ? { 'Authorization': 'Bearer ' + agentApiKey }\n : {},\n encoding: 'base64',\n returnBinary: false,\n timeout: 15000,\n });\n resolved.push({\n name: att.filename || 'attachment.pdf',\n content: typeof res === 'string' ? res : (res && res.data) || '',\n type: att.contentType || 'application/pdf',\n });\n } catch (err) {\n return [{ json: {\n ...first,\n body: {\n ...body,\n attachments: [],\n _attachmentFetchError: 'PDF fetch failed: ' + (err.message || String(err)),\n },\n attachmentsResolved: [],\n } }];\n }\n}\n\nreturn [{ json: {\n ...first,\n body: { ...body, attachments: [] },\n attachmentsResolved: resolved,\n} }];"
|
|
},
|
|
"id": "wf-po-send-fetch-pdf",
|
|
"name": "Fetch PDF attachment bytes",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [220, 0]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"fromEmail": "={{ $json.body.fromEmail || 'orders@wfdash.local' }}",
|
|
"toEmail": "={{ $json.body.recipient }}",
|
|
"ccEmail": "={{ $json.body.cc }}",
|
|
"bccEmail": "={{ $json.body.bcc }}",
|
|
"subject": "={{ $json.body.subject }}",
|
|
"text": "={{ $json.body.body }}",
|
|
"html": "={{ $json.body.body }}",
|
|
"options": {
|
|
"attachments": "={{ $json.attachmentsResolved && $json.attachmentsResolved.length ? $json.attachmentsResolved : [] }}"
|
|
}
|
|
},
|
|
"id": "wf-po-send-smtp",
|
|
"name": "Send PO email (SMTP)",
|
|
"type": "n8n-nodes-base.emailSend",
|
|
"typeVersion": 2.1,
|
|
"position": [440, 0]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Resolve the SMTP result from the Send PO email (SMTP) node, then\n// POST the outcome back to the dashboard's\n// /api/v1/purchase-orders/[id]/send-email/outcome route. The dashboard\n// owns the canonical write (status flip + order_items.item_status flip)\n// using its own admin PB auth. n8n never carries the admin token.\n//\n// The Bearer key and dashboard base URL are pushed on the webhook body\n// (not $env.*) because the docker-compose has\n// N8N_BLOCK_ENV_ACCESS_IN_NODE=true — Code nodes cannot read env vars\n// at runtime.\nconst first = ($input.first() || {}).json || {};\nconst body = first.body || first;\nconst agentApiKey = body.agentApiKey || '';\nconst dashboardBase = (body.dashboardBaseUrl || '').replace(/\\/$/, '');\nconst poId = body.poId;\n\nif (!agentApiKey) {\n return [{ json: {\n poId,\n ok: false,\n error: 'agentApiKey missing from webhook body — route did not push it',\n } }];\n}\nif (!dashboardBase) {\n return [{ json: {\n poId,\n ok: false,\n error: 'dashboardBaseUrl missing from webhook body — route did not push it',\n } }];\n}\n\nconst sendResult = ($('Send PO email (SMTP)').first().json) || {};\nconst ok = !sendResult.error && sendResult.ok !== false;\nconst messageId = sendResult.messageId || sendResult.message_id || '';\nconst errorMessage = ok ? '' : (sendResult.error || sendResult.response || 'Unknown SMTP error').toString().slice(0, 990);\n\ntry {\n const out = await this.helpers.httpRequest({\n method: 'POST',\n url: dashboardBase + '/api/v1/purchase-orders/' + poId + '/send-email/outcome',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + agentApiKey,\n },\n json: true,\n body: {\n outcome: ok ? 'success' : 'failure',\n messageId,\n errorMessage,\n },\n });\n return [{ json: { poId, ok, messageId, error: errorMessage, outcome: out } }];\n} catch (err) {\n return [{ json: {\n poId,\n ok,\n messageId,\n error: 'Outcome callback failed: ' + (err.message || String(err)),\n } }];\n}"
|
|
},
|
|
"id": "wf-po-send-record-outcome",
|
|
"name": "Record outcome to PB",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [660, 0]
|
|
}
|
|
],
|
|
"connections": {
|
|
"PO Send Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Fetch PDF attachment bytes",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Fetch PDF attachment bytes": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send PO email (SMTP)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send PO email (SMTP)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Record outcome to PB",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"saveDataSuccessExecution": "all",
|
|
"saveDataErrorExecution": "all",
|
|
"saveManualExecutions": true,
|
|
"saveExecutionProgress": true,
|
|
"timezone": "Africa/Johannesburg"
|
|
},
|
|
"staticData": null,
|
|
"pinData": {},
|
|
"versionId": 1,
|
|
"triggerCount": 1,
|
|
"meta": {
|
|
"instanceId": "wfdash-po-send",
|
|
"templateCredsSetupCompleted": false,
|
|
"description": "After import: open the workflow, attach an SMTP credential to the 'Send PO email (SMTP)' node (any name; the SDK does not pin a credential id). Then activate."
|
|
}
|
|
} |