New unified receipt creation system with: - UnifiedReceiptForm component with inline OCR preview and confidence indicators - Compact upload zone with drag-drop and camera support - TVA and Payment fields with dynamic add/remove - Supplier dual-field with autocomplete and OCR hint - Receipt form sections with collapsible auxiliary data Backend OCR improvements: - Add confidence_tva and confidence_payment to extraction results - Update TVA extraction to return confidence scores - Include TVA (15%) and payment (10%) in overall_confidence calculation Also includes: - CSS design system rules documentation - Port check helper function for service scripts - Expanded design tokens documentation in CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
283 lines
7.4 KiB
JavaScript
283 lines
7.4 KiB
JavaScript
/**
|
|
* Conversion utilities for Receipt form data
|
|
*
|
|
* These functions convert between the API format (dynamic arrays) and
|
|
* the UI format (fixed fields for TVA and payments).
|
|
*/
|
|
|
|
/**
|
|
* Default fixed TVA structure for UI
|
|
*/
|
|
export const getDefaultTva = () => ({
|
|
A: { percent: 19, amount: 0 },
|
|
B: { percent: 9, amount: 0 },
|
|
C: { percent: 5, amount: 0 },
|
|
D: { percent: 0, amount: 0 },
|
|
E: { percent: null, amount: 0 },
|
|
})
|
|
|
|
/**
|
|
* Default fixed payments structure for UI
|
|
*/
|
|
export const getDefaultPayments = () => ({
|
|
CARD: 0,
|
|
NUMERAR: 0,
|
|
ALTE: { amount: 0, type: null },
|
|
})
|
|
|
|
/**
|
|
* Convert API tva_entries/tva_breakdown array → UI fixed fields
|
|
*
|
|
* @param {Array} entries - Array of {code, percent, amount}
|
|
* @returns {Object} Fixed TVA fields {A, B, C, D, E}
|
|
*/
|
|
export const apiToUiTva = (entries) => {
|
|
const ui = getDefaultTva()
|
|
|
|
if (!entries || !Array.isArray(entries)) {
|
|
return ui
|
|
}
|
|
|
|
entries.forEach(entry => {
|
|
const code = entry.code?.toUpperCase()
|
|
if (code && ui[code]) {
|
|
ui[code] = {
|
|
percent: entry.percent ?? ui[code].percent,
|
|
amount: parseFloat(entry.amount) || 0
|
|
}
|
|
}
|
|
})
|
|
|
|
return ui
|
|
}
|
|
|
|
/**
|
|
* Convert UI fixed TVA fields → API tva_breakdown array
|
|
* Only includes entries with amount > 0
|
|
*
|
|
* @param {Object} tvaUi - Fixed TVA fields {A, B, C, D, E}
|
|
* @returns {Array} Array of {code, percent, amount} or null if empty
|
|
*/
|
|
export const uiToApiTva = (tvaUi) => {
|
|
if (!tvaUi) return null
|
|
|
|
const entries = Object.entries(tvaUi)
|
|
.filter(([_, v]) => v.amount && v.amount > 0)
|
|
.map(([code, v]) => ({
|
|
code,
|
|
percent: v.percent ?? 0,
|
|
amount: parseFloat(v.amount) || 0
|
|
}))
|
|
|
|
return entries.length > 0 ? entries : null
|
|
}
|
|
|
|
/**
|
|
* Calculate total TVA from UI fixed fields
|
|
*
|
|
* @param {Object} tvaUi - Fixed TVA fields {A, B, C, D, E}
|
|
* @returns {number} Total TVA amount
|
|
*/
|
|
export const calculateTvaTotal = (tvaUi) => {
|
|
if (!tvaUi) return 0
|
|
|
|
return Object.values(tvaUi)
|
|
.reduce((sum, v) => sum + (parseFloat(v.amount) || 0), 0)
|
|
}
|
|
|
|
/**
|
|
* Convert API payment_methods array → UI fixed fields
|
|
*
|
|
* @param {Array} methods - Array of {method, amount}
|
|
* @returns {Object} Fixed payment fields {CARD, NUMERAR, ALTE}
|
|
*/
|
|
export const apiToUiPayments = (methods) => {
|
|
const ui = getDefaultPayments()
|
|
|
|
if (!methods || !Array.isArray(methods)) {
|
|
return ui
|
|
}
|
|
|
|
methods.forEach(pm => {
|
|
const method = pm.method?.toUpperCase()
|
|
const amount = parseFloat(pm.amount) || 0
|
|
|
|
if (method === 'CARD') {
|
|
ui.CARD = amount
|
|
} else if (method === 'NUMERAR' || method === 'CASH') {
|
|
ui.NUMERAR = amount
|
|
} else if (method) {
|
|
// Other payment types go to ALTE
|
|
ui.ALTE.amount += amount
|
|
// Try to determine type from method name
|
|
if (method.includes('TICH') || method.includes('MASA')) {
|
|
ui.ALTE.type = 'tichete_masa'
|
|
} else if (method.includes('VOUCHER')) {
|
|
ui.ALTE.type = 'voucher'
|
|
} else if (method.includes('CREDIT')) {
|
|
ui.ALTE.type = 'credit_magazin'
|
|
}
|
|
}
|
|
})
|
|
|
|
return ui
|
|
}
|
|
|
|
/**
|
|
* Convert UI fixed payment fields → API payment_methods array
|
|
* Only includes entries with amount > 0
|
|
*
|
|
* @param {Object} paymentsUi - Fixed payment fields {CARD, NUMERAR, ALTE}
|
|
* @returns {Array} Array of {method, amount} or null if empty
|
|
*/
|
|
export const uiToApiPayments = (paymentsUi) => {
|
|
if (!paymentsUi) return null
|
|
|
|
const methods = []
|
|
|
|
if (paymentsUi.CARD && paymentsUi.CARD > 0) {
|
|
methods.push({ method: 'CARD', amount: paymentsUi.CARD })
|
|
}
|
|
|
|
if (paymentsUi.NUMERAR && paymentsUi.NUMERAR > 0) {
|
|
methods.push({ method: 'NUMERAR', amount: paymentsUi.NUMERAR })
|
|
}
|
|
|
|
if (paymentsUi.ALTE?.amount && paymentsUi.ALTE.amount > 0) {
|
|
// Map type to method name
|
|
let methodName = 'ALTE'
|
|
if (paymentsUi.ALTE.type === 'tichete_masa') {
|
|
methodName = 'TICHETE_MASA'
|
|
} else if (paymentsUi.ALTE.type === 'voucher') {
|
|
methodName = 'VOUCHER'
|
|
} else if (paymentsUi.ALTE.type === 'credit_magazin') {
|
|
methodName = 'CREDIT_MAGAZIN'
|
|
}
|
|
methods.push({ method: methodName, amount: paymentsUi.ALTE.amount })
|
|
}
|
|
|
|
return methods.length > 0 ? methods : null
|
|
}
|
|
|
|
/**
|
|
* Calculate total payments from UI fixed fields
|
|
*
|
|
* @param {Object} paymentsUi - Fixed payment fields {CARD, NUMERAR, ALTE}
|
|
* @returns {number} Total payment amount
|
|
*/
|
|
export const calculatePaymentsTotal = (paymentsUi) => {
|
|
if (!paymentsUi) return 0
|
|
|
|
return (
|
|
(paymentsUi.CARD || 0) +
|
|
(paymentsUi.NUMERAR || 0) +
|
|
(paymentsUi.ALTE?.amount || 0)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get default form state for unified receipt form
|
|
*/
|
|
export const getDefaultUnifiedFormState = () => ({
|
|
receipt_type: 'bon_fiscal',
|
|
receipt_date: new Date(),
|
|
receipt_number: '',
|
|
|
|
// Supplier (DB validated)
|
|
partner_name: null,
|
|
cui: '',
|
|
vendor_address: '',
|
|
|
|
// Total
|
|
amount: null,
|
|
|
|
// TVA (5 fixed fields)
|
|
tva: getDefaultTva(),
|
|
|
|
// Payments (3 fixed fields)
|
|
payments: getDefaultPayments(),
|
|
|
|
// Auxiliary
|
|
expense_type_code: null,
|
|
description: '',
|
|
|
|
// Metadata
|
|
ocr_raw_text: '',
|
|
items_count: null,
|
|
company_id: null,
|
|
})
|
|
|
|
/**
|
|
* Convert legacy form state to unified form state
|
|
*
|
|
* @param {Object} legacyForm - Old form format
|
|
* @returns {Object} Unified form format
|
|
*/
|
|
export const legacyToUnifiedForm = (legacyForm) => {
|
|
return {
|
|
receipt_type: legacyForm.receipt_type || 'bon_fiscal',
|
|
receipt_date: legacyForm.receipt_date instanceof Date
|
|
? legacyForm.receipt_date
|
|
: new Date(legacyForm.receipt_date),
|
|
receipt_number: legacyForm.receipt_number || '',
|
|
|
|
partner_name: legacyForm.partner_name || null,
|
|
cui: legacyForm.cui || '',
|
|
vendor_address: legacyForm.vendor_address || '',
|
|
|
|
amount: parseFloat(legacyForm.amount) || null,
|
|
|
|
tva: apiToUiTva(legacyForm.tva_breakdown),
|
|
payments: apiToUiPayments(legacyForm.payment_methods),
|
|
|
|
expense_type_code: legacyForm.expense_type_code || null,
|
|
description: legacyForm.description || '',
|
|
|
|
ocr_raw_text: legacyForm.ocr_raw_text || '',
|
|
items_count: legacyForm.items_count || null,
|
|
company_id: legacyForm.company_id || null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert unified form state to API payload
|
|
*
|
|
* @param {Object} unifiedForm - Unified form format
|
|
* @returns {Object} API payload format
|
|
*/
|
|
export const unifiedFormToApiPayload = (unifiedForm) => {
|
|
return {
|
|
receipt_type: unifiedForm.receipt_type,
|
|
direction: 'cheltuiala', // Always expense (no more income)
|
|
receipt_date: unifiedForm.receipt_date instanceof Date
|
|
? unifiedForm.receipt_date.toISOString().split('T')[0]
|
|
: unifiedForm.receipt_date,
|
|
receipt_number: unifiedForm.receipt_number || null,
|
|
|
|
partner_name: typeof unifiedForm.partner_name === 'string'
|
|
? unifiedForm.partner_name
|
|
: unifiedForm.partner_name?.name || null,
|
|
cui: unifiedForm.cui || null,
|
|
vendor_address: unifiedForm.vendor_address || null,
|
|
|
|
amount: unifiedForm.amount || 0,
|
|
|
|
tva_breakdown: uiToApiTva(unifiedForm.tva),
|
|
tva_total: calculateTvaTotal(unifiedForm.tva) || null,
|
|
payment_methods: uiToApiPayments(unifiedForm.payments),
|
|
|
|
expense_type_code: unifiedForm.expense_type_code || null,
|
|
description: unifiedForm.description || null,
|
|
|
|
ocr_raw_text: unifiedForm.ocr_raw_text || null,
|
|
items_count: unifiedForm.items_count || null,
|
|
company_id: unifiedForm.company_id,
|
|
|
|
// Legacy fields (removed but kept for backwards compat)
|
|
payment_mode: null,
|
|
cash_register_id: null,
|
|
cash_register_name: null,
|
|
cash_register_account: null,
|
|
}
|
|
}
|