Files
roa2web-service-auto/src/modules/data-entry/utils/receiptConversions.js
Claude Agent b4a226409c feat(data-entry): Add unified receipt form with OCR confidence tracking
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>
2026-01-08 21:48:37 +00:00

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,
}
}