WFDASH-WEBAPP/n8n_workfows/WFDASH — Purchase Order Send.json
SJCoetz 7f924ccc45 refactor: switch hardcoded UI colors to semantic theme tokens
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
2026-07-07 17:22:03 +02:00

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."
}
}