Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 32 additions & 37 deletions frontend/src/components/job/JobActualTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
:jobId="jobId"
:tabKind="'actual'"
:lines="costLines"
:draftSession="costLineDraftSession"
:readOnly="false"
:showItemColumn="true"
:showSourceColumn="true"
Expand All @@ -93,7 +94,6 @@
@delete-line="handleSmartDelete"
@duplicate-line="() => {}"
@move-line="() => {}"
@create-line="handleCreateLine"
/>
</div>
</div>
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, unknown>) || {},
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<CostLine> {
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<string, unknown>) || {},
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()])
})
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/components/job/JobEstimateTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
</div>
</main>
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 })
</script>

<style scoped>
Expand Down
27 changes: 20 additions & 7 deletions frontend/src/components/job/JobQuoteTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
:jobId="jobId"
:tabKind="'quote'"
:lines="costLines"
:draftSession="costLineDraftSession"
:readOnly="isLoading || areEditsBlocked"
:showItemColumn="true"
:showSourceColumn="false"
@delete-line="handleSmartDelete"
@duplicate-line="(line) => handleAddMaterial(line as any)"
@create-line="handleCreateFromEmpty"
/>
<div v-else class="text-center py-8 text-gray-500">No quote data available</div>
</template>
Expand Down Expand Up @@ -503,6 +503,7 @@ import { z } from 'zod'
import { costlineService } from '../../services/costline.service'
import { fetchCostSet } from '../../services/costing.service'
import { useCostLinesActions } from '../../composables/useCostLinesActions'
import { useCostLineDrafts } from '@/composables/useCostLineDrafts'
import { useXeroConnection } from '../../composables/useXeroConnection'
import CompactSummaryCard from '../shared/CompactSummaryCard.vue'
import {
Expand Down Expand Up @@ -586,6 +587,7 @@ const currentQuote = computed(() => {
})

const isLoading = ref(false)
let quoteRefreshVersion = 0
const showQuoteRevisionsModal = ref(false)
const quoteRevisionsData = ref<QuoteRevisionsListResponse | null>(null)
const isCreatingRevision = ref(false)
Expand Down Expand Up @@ -694,9 +696,10 @@ watch(
{ immediate: true },
)

async function refreshQuoteData() {
async function refreshQuoteData(showLoading = true) {
if (!props.jobId) return
isLoading.value = true
const refreshVersion = ++quoteRefreshVersion
if (showLoading) isLoading.value = true

// DEBUG: Log before refresh
debugLog('REFRESH QUOTE - BEFORE:')
Expand All @@ -705,6 +708,7 @@ async function refreshQuoteData() {

try {
const response = await fetchCostSet(props.jobId, 'quote')
if (refreshVersion !== quoteRefreshVersion) return

// Update our local quote cost set
quoteCostSet.value = response
Expand All @@ -720,17 +724,18 @@ async function refreshQuoteData() {
const xeroQuoteResponse: Quote = await api.job_jobs_quote_retrieve({
params: { job_id: props.jobId },
})
xeroQuote.value = xeroQuoteResponse
if (refreshVersion === quoteRefreshVersion) xeroQuote.value = xeroQuoteResponse
} catch {
// Xero quote not available, that's ok
xeroQuote.value = null
if (refreshVersion === quoteRefreshVersion) xeroQuote.value = null
}
} catch (error) {
if (refreshVersion !== quoteRefreshVersion) return
toast.error('Failed to refresh quote data')
console.error('Failed to refresh quote data:', error)
console.error('🔍 REFRESH QUOTE DEBUG - ERROR:', error)
} finally {
isLoading.value = false
if (refreshVersion === quoteRefreshVersion) isLoading.value = false
}
}

Expand Down Expand Up @@ -797,10 +802,18 @@ const { handleAddMaterial, handleSmartDelete, handleCreateFromEmpty } = useCostL
onCostLinesChanged: async () => {
// Refresh quote data to update summary from API
emit('cost-line-changed')
await refreshQuoteData()
await refreshQuoteData(false)
},
})

async function persistNewLine(line: CostLine): Promise<CostLine> {
const created = await handleCreateFromEmpty(line)
if (!created) throw new Error('Cost line creation was prevented.')
return created
}

const costLineDraftSession = useCostLineDrafts({ costLines, createLine: persistNewLine })

// --- QUOTE METHODS ---
const createQuote = () => {
// Show modal to ask user if they want breakdown or total
Expand Down
Loading