perf: parallelize dispatch API fetches + add sales_order/order_source fields

Dispatch performance:
- Replace sequential batch fetches (batches of 15, one after another)
  with full parallel Promise.all — all doc fetches fire simultaneously
- With 20 jobs: was ~3 sequential round-trips, now ~2 (1 list + 1 parallel)

Order traceability:
- Add sales_order (Link) and order_source (Select) fields to Dispatch Job
- checkout.js sets order_source='Online' + sales_order link on job creation
- acceptance.js sets order_source='Quotation' on quotation-sourced jobs
- Store maps new fields: salesOrder, orderSource

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-04-08 18:07:14 -04:00
parent c6b2dd1491
commit fd326ac52e
4 changed files with 23 additions and 21 deletions

View File

@ -29,34 +29,31 @@ async function apiPut (doctype, name, body) {
return data return data
} }
// Fetch docs in parallel batches to avoid browser connection limits // Single-call fetch with all fields + child tables
async function batchFetchDocs (doctype, names, batchSize = 10) { async function listDocs (doctype, fields = '["*"]', filters = null, limit = 200) {
const docs = [] let url = `/api/resource/${encodeURIComponent(doctype)}?fields=${fields}&limit_page_length=${limit}`
for (let i = 0; i < names.length; i += batchSize) { if (filters) url += '&filters=' + encodeURIComponent(JSON.stringify(filters))
const batch = names.slice(i, i + batchSize) const data = await apiGet(url)
const results = await Promise.all( return data.data || []
batch.map(n => apiGet(`/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(n)}`).then(d => d.data))
)
docs.push(...results)
} }
return docs
// Fetch single doc (needed for child tables not returned by list)
async function fetchDoc (doctype, name) {
return (await apiGet(`/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`)).data
} }
export async function fetchTechnicians () { export async function fetchTechnicians () {
const list = await apiGet('/api/resource/Dispatch%20Technician?fields=["name"]&limit=100') const names = (await listDocs('Dispatch Technician', '["name"]', null, 100)).map(t => t.name)
const names = (list.data || []).map(t => t.name)
if (!names.length) return [] if (!names.length) return []
return batchFetchDocs('Dispatch Technician', names, 15) // All individual fetches in parallel (child tables: tags)
return Promise.all(names.map(n => fetchDoc('Dispatch Technician', n)))
} }
// Fetch all jobs with child tables (assistants)
export async function fetchJobs (filters = null) { export async function fetchJobs (filters = null) {
let url = '/api/resource/Dispatch%20Job?fields=["name"]&limit=200' const names = (await listDocs('Dispatch Job', '["name"]', filters, 200)).map(j => j.name)
if (filters) url += '&filters=' + encodeURIComponent(JSON.stringify(filters))
const list = await apiGet(url)
const names = (list.data || []).map(j => j.name)
if (!names.length) return [] if (!names.length) return []
return batchFetchDocs('Dispatch Job', names, 15) // All individual fetches in parallel (child tables: assistants, tags)
return Promise.all(names.map(n => fetchDoc('Dispatch Job', n)))
} }
export async function updateJob (name, payload) { export async function updateJob (name, payload) {

View File

@ -51,7 +51,9 @@ export const useDispatchStore = defineStore('dispatch', () => {
stepOrder: j.step_order || 0, stepOrder: j.step_order || 0,
onOpenWebhook: j.on_open_webhook || null, onOpenWebhook: j.on_open_webhook || null,
onCloseWebhook: j.on_close_webhook || null, onCloseWebhook: j.on_close_webhook || null,
published: j.published === undefined ? true : !!j.published, // backwards compat: existing jobs default published salesOrder: j.sales_order || null,
orderSource: j.order_source || 'Manual',
published: j.published === undefined ? true : !!j.published,
} }
} }

View File

@ -182,6 +182,7 @@ async function createDeferredJobs (steps, ctx, quotationName) {
source_issue: ctx.issue || '', source_issue: ctx.issue || '',
customer: ctx.customer || '', customer: ctx.customer || '',
service_location: ctx.service_location || '', service_location: ctx.service_location || '',
order_source: 'Quotation',
depends_on: dependsOn, depends_on: dependsOn,
parent_job: parentJob, parent_job: parentJob,
step_order: step.step_order || (i + 1), step_order: step.step_order || (i + 1),

View File

@ -231,6 +231,8 @@ async function processCheckout (body) {
status: 'open', status: 'open',
job_type: step.job_type || 'Autre', job_type: step.job_type || 'Autre',
customer: customerName, customer: customerName,
sales_order: orderName || '',
order_source: 'Online',
depends_on: dependsOn, depends_on: dependsOn,
parent_job: parentJob, parent_job: parentJob,
step_order: i + 1, step_order: i + 1,