diff --git a/frontend/src/components/job/JobActualTab.vue b/frontend/src/components/job/JobActualTab.vue index 18878e805..4cd115560 100644 --- a/frontend/src/components/job/JobActualTab.vue +++ b/frontend/src/components/job/JobActualTab.vue @@ -81,6 +81,7 @@ :jobId="jobId" :tabKind="'actual'" :lines="costLines" + :draftSession="costLineDraftSession" :readOnly="false" :showItemColumn="true" :showSourceColumn="true" @@ -93,7 +94,6 @@ @delete-line="handleSmartDelete" @duplicate-line="() => {}" @move-line="() => {}" - @create-line="handleCreateLine" /> @@ -454,6 +454,7 @@ import { costlineService } from '../../services/costline.service' import { schemas } from '../../api/generated/api' import { useSmartCostLineDelete } from '../../composables/useSmartCostLineDelete' import { useCostSummary } from '../../composables/useCostSummary' +import { useCostLineDrafts } from '@/composables/useCostLineDrafts' import { useXeroConnection } from '../../composables/useXeroConnection' import { api } from '../../api/client' import { z } from 'zod' @@ -860,47 +861,41 @@ async function consumeStockForNewLine(payload: { } } -// Handler for table's @create-line (for adjustments, since material is handled in table) -async function handleCreateLine(line: CostLine) { - if (line.kind === 'adjust') { - // For adjustments, create via service as in EstimateTab - isLoading.value = true - jobActualSaveFeedback.saving() - try { - const createPayload = { - kind: 'adjust' as const, - desc: line.desc, - quantity: line.quantity, - unit_cost: line.unit_cost, - unit_rev: line.unit_rev, - accounting_date: toLocalDateString(), - ext_refs: (line.ext_refs as Record) || {}, - meta: { source: 'manual_adjustment' }, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - } +// Adjustment persistence callback; material creation remains in consumeStockForNewLine. +async function handleCreateLine(line: CostLine): Promise { + if (line.kind !== 'adjust') { + throw new Error(`Cannot persist ${line.kind} through the adjustment creation path.`) + } - const created = await costlineService.createCostLine(props.jobId, 'actual', createPayload) - // Replace if the source line exists in parent's array, otherwise append (phantom row case) - const idx = costLines.value.findIndex((l) => l === line || l.id === line.id) - if (idx >= 0) { - costLines.value[idx] = created - } else { - costLines.value.push(created) - } - jobActualSaveFeedback.saved() - emit('cost-line-changed') - } catch (error) { - jobActualSaveFeedback.error('Failed to create adjustment.') - toast.error('Failed to create adjustment.') - console.error('Failed to create adjustment:', error) - } finally { - isLoading.value = false + jobActualSaveFeedback.saving() + try { + const createPayload = { + kind: 'adjust' as const, + desc: line.desc, + quantity: line.quantity, + unit_cost: line.unit_cost, + unit_rev: line.unit_rev, + accounting_date: toLocalDateString(), + ext_refs: (line.ext_refs as Record) || {}, + meta: { source: 'manual_adjustment' }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), } + + const created = await costlineService.createCostLine(props.jobId, 'actual', createPayload) + jobActualSaveFeedback.saved() + emit('cost-line-changed') + return created + } catch (error) { + jobActualSaveFeedback.error('Failed to create adjustment.') + toast.error('Failed to create adjustment.') + console.error('Failed to create adjustment:', error) + throw error } - // For material, table already handled consumption, so no-op or reload } +const costLineDraftSession = useCostLineDrafts({ costLines, createLine: handleCreateLine }) + onMounted(async () => { await Promise.all([loadStaff(), loadActualCosts(), loadCostsSummary(), loadInvoices()]) }) diff --git a/frontend/src/components/job/JobEstimateTab.vue b/frontend/src/components/job/JobEstimateTab.vue index 190f3b01e..66585dc8d 100644 --- a/frontend/src/components/job/JobEstimateTab.vue +++ b/frontend/src/components/job/JobEstimateTab.vue @@ -39,13 +39,13 @@ :jobId="jobId" :tabKind="'estimate'" :lines="costLines" + :draftSession="costLineDraftSession" :readOnly="false" :showItemColumn="true" :showSourceColumn="false" @delete-line="handleSmartDelete" @duplicate-line="(line) => handleAddMaterial(line as any)" @move-line="(index, direction) => {}" - @create-line="handleCreateFromEmpty" /> @@ -103,6 +103,7 @@ import CompactSummaryCard from '../shared/CompactSummaryCard.vue' import { fetchCostSet } from '../../services/costing.service' import { useCostSummary } from '../../composables/useCostSummary' import { useCostLinesActions } from '../../composables/useCostLinesActions' +import { useCostLineDrafts } from '@/composables/useCostLineDrafts' import { schemas } from '../../api/generated/api' import type { z } from 'zod' import { @@ -212,11 +213,15 @@ async function handleAddMaterial(line: CostLine) { async function handleCreateFromEmpty(line: CostLine) { if (!isCompanyDefaultsReady.value) { toast.error('Company defaults not loaded yet.') - return + throw new Error('Company defaults not loaded yet.') } - await createFromEmptyInternal(line) + const created = await createFromEmptyInternal(line) + if (!created) throw new Error('Cost line creation was prevented.') + return created } + +const costLineDraftSession = useCostLineDrafts({ costLines, createLine: handleCreateFromEmpty })