feat: Restructure receipt form UI with bon fiscal sections

- Restructure OCRPreview with 5 sections matching Romanian receipt format:
  FURNIZOR, DOCUMENT, TOTAL, PLATA, TVA
- Add collapse button to OCRPreview header with minimize functionality
- Restructure form with matching compact sections for side-by-side comparison
- Add "din OCR" indicator when payment mode is auto-set from OCR
- Remove auto-collapse behavior - OCRPreview stays visible after applying data
- Add prominent TOTAL box styling with gradient background
- Compact form layout with fields on same row where logical
- Add TVA section to form with editable breakdown

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-16 15:08:55 +02:00
parent c1220e86a6
commit a6ae628934
2 changed files with 795 additions and 462 deletions

View File

@@ -7,117 +7,107 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<span class="overall-confidence"> <span class="overall-confidence">
Incredere generala:
<OCRConfidenceIndicator <OCRConfidenceIndicator
:confidence="data.overall_confidence" :confidence="data.overall_confidence"
:show-percentage="true" :show-percentage="true"
size="normal" size="normal"
/> />
</span> </span>
<Button
icon="pi pi-minus"
text
rounded
size="small"
@click="$emit('collapse')"
v-tooltip="'Minimizeaza'"
class="collapse-btn"
/>
</div> </div>
</div> </div>
<div class="preview-content"> <div class="preview-content">
<div class="preview-grid"> <!-- SECTION: FURNIZOR -->
<!-- Receipt Type --> <div class="ocr-section" v-if="data.partner_name || data.cui || data.address">
<div class="preview-field" v-if="data.receipt_type"> <div class="ocr-section-title">FURNIZOR</div>
<label>Tip Document</label> <div class="ocr-section-content">
<div class="field-value"> <div class="vendor-name" v-if="data.partner_name">
{{ data.partner_name }}
<OCRConfidenceIndicator :confidence="data.confidence_vendor" size="small" />
</div>
<div class="vendor-cui" v-if="data.cui">CUI: {{ data.cui }}</div>
<div class="vendor-address" v-if="data.address">{{ data.address }}</div>
</div>
</div>
<!-- SECTION: DOCUMENT -->
<div class="ocr-section" v-if="data.receipt_type || data.receipt_number || data.receipt_date">
<div class="ocr-section-title">DOCUMENT</div>
<div class="ocr-section-content">
<div class="document-row">
<Tag <Tag
v-if="data.receipt_type"
:value="data.receipt_type === 'bon_fiscal' ? 'Bon Fiscal' : 'Chitanta'" :value="data.receipt_type === 'bon_fiscal' ? 'Bon Fiscal' : 'Chitanta'"
:severity="data.receipt_type === 'bon_fiscal' ? 'info' : 'success'" :severity="data.receipt_type === 'bon_fiscal' ? 'info' : 'success'"
/> />
</div> <span v-if="data.receipt_number" class="doc-number">
</div> Nr: {{ data.receipt_series ? data.receipt_series + ' ' : '' }}{{ data.receipt_number }}
</span>
<!-- Amount --> <span v-if="data.receipt_date" class="doc-date">
<div class="preview-field" v-if="data.amount"> <i class="pi pi-calendar"></i>
<label>
Suma
<OCRConfidenceIndicator :confidence="data.confidence_amount" size="small" />
</label>
<div class="field-value amount">
{{ formatAmount(data.amount) }} RON
</div>
</div>
<!-- Date -->
<div class="preview-field" v-if="data.receipt_date">
<label>
Data
<OCRConfidenceIndicator :confidence="data.confidence_date" size="small" />
</label>
<div class="field-value">
{{ formatDate(data.receipt_date) }} {{ formatDate(data.receipt_date) }}
</div> <OCRConfidenceIndicator :confidence="data.confidence_date" size="small" />
</div> </span>
<!-- Receipt Number -->
<div class="preview-field" v-if="data.receipt_number">
<label>Numar Bon</label>
<div class="field-value">
{{ data.receipt_series ? data.receipt_series + ' ' : '' }}{{ data.receipt_number }}
</div>
</div>
<!-- Vendor -->
<div class="preview-field full-width" v-if="data.partner_name">
<label>
Furnizor
<OCRConfidenceIndicator :confidence="data.confidence_vendor" size="small" />
</label>
<div class="field-value">
{{ data.partner_name }}
<span v-if="data.cui" class="cui-badge">CUI: {{ data.cui }}</span>
</div>
</div>
<!-- TVA Entries (multiple rates) -->
<div class="preview-field full-width" v-if="data.tva_entries?.length > 0 || data.tva_total">
<label>TVA</label>
<div class="tva-breakdown">
<div v-for="(entry, idx) in data.tva_entries" :key="idx" class="tva-entry">
<span class="tva-code" v-if="entry.code">{{ entry.code }}</span>
<span class="tva-percent-badge">{{ entry.percent }}%</span>
<span class="tva-amount">{{ formatAmount(entry.amount) }} RON</span>
</div>
<div v-if="data.tva_total && data.tva_entries?.length > 1" class="tva-total">
<strong>Total TVA:</strong> {{ formatAmount(data.tva_total) }} RON
</div> </div>
</div> </div>
</div> </div>
<!-- Payment Methods from OCR --> <!-- SECTION: TOTAL -->
<div class="preview-field full-width" v-if="data.payment_methods?.length > 0"> <div class="ocr-section" v-if="data.amount">
<label>Modalitati Plata (OCR)</label> <div class="ocr-section-title">TOTAL</div>
<div class="payment-methods-list"> <div class="ocr-section-content">
<div class="ocr-total-box">
<span class="total-amount">{{ formatAmount(data.amount) }} LEI</span>
<OCRConfidenceIndicator :confidence="data.confidence_amount" size="small" />
</div>
<div v-if="data.items_count" class="items-count">
{{ data.items_count }} articole
</div>
</div>
</div>
<!-- SECTION: PLATA -->
<div class="ocr-section" v-if="data.payment_methods?.length > 0">
<div class="ocr-section-title">PLATA</div>
<div class="ocr-section-content">
<div class="ocr-payment-tags">
<Tag <Tag
v-for="(pm, idx) in data.payment_methods" v-for="(pm, idx) in data.payment_methods"
:key="idx" :key="idx"
:severity="pm.method === 'CARD' ? 'info' : 'success'" :severity="pm.method === 'CARD' ? 'info' : 'success'"
:value="`${pm.method}: ${formatAmount(pm.amount)} RON`" :value="`${pm.method}: ${formatAmount(pm.amount)} LEI`"
class="mr-1"
/> />
</div> </div>
<div v-if="data.suggested_payment_mode" class="suggested-payment-mode"> <div v-if="data.suggested_payment_mode" class="suggested-payment-mode">
<i class="pi pi-lightbulb" style="color: #f59e0b;"></i> <i class="pi pi-lightbulb"></i>
<span>Sugestie: <strong>{{ getSuggestedPaymentLabel(data.suggested_payment_mode) }}</strong></span> <span>Sugestie: <strong>{{ getSuggestedPaymentLabel(data.suggested_payment_mode) }}</strong></span>
</div> </div>
</div> </div>
<!-- Items Count -->
<div class="preview-field" v-if="data.items_count">
<label>Nr. Articole</label>
<div class="field-value">
{{ data.items_count }} articole
</div>
</div> </div>
<!-- Address --> <!-- SECTION: TVA -->
<div class="preview-field full-width" v-if="data.address"> <div class="ocr-section" v-if="data.tva_entries?.length > 0 || data.tva_total">
<label>Adresa</label> <div class="ocr-section-title">TVA</div>
<div class="field-value address-text"> <div class="ocr-section-content">
{{ data.address }} <div class="ocr-tva-table">
<div v-for="(entry, idx) in data.tva_entries" :key="idx" class="tva-row">
<span class="tva-code" v-if="entry.code">{{ entry.code }}</span>
<span class="tva-percent">({{ entry.percent }}%)</span>
<span class="tva-amount">{{ formatAmount(entry.amount) }} LEI</span>
</div>
<div v-if="computedTvaTotal > 0" class="tva-row tva-total-row">
<span class="tva-label">Total TVA:</span>
<span class="tva-amount">{{ formatAmount(computedTvaTotal) }} LEI</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -168,9 +158,10 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed } from 'vue'
import OCRConfidenceIndicator from './OCRConfidenceIndicator.vue' import OCRConfidenceIndicator from './OCRConfidenceIndicator.vue'
import Tag from 'primevue/tag' import Tag from 'primevue/tag'
import Button from 'primevue/button'
const props = defineProps({ const props = defineProps({
data: { data: {
@@ -179,10 +170,17 @@ const props = defineProps({
} }
}) })
defineEmits(['apply', 'dismiss']) defineEmits(['apply', 'dismiss', 'collapse'])
const showRawText = ref(false) const showRawText = ref(false)
// Computed TVA total from entries
const computedTvaTotal = computed(() => {
if (props.data.tva_total) return parseFloat(props.data.tva_total)
if (!props.data.tva_entries?.length) return 0
return props.data.tva_entries.reduce((sum, e) => sum + parseFloat(e.amount || 0), 0)
})
const getSuggestedPaymentLabel = (mode) => { const getSuggestedPaymentLabel = (mode) => {
const labels = { const labels = {
'casa': 'Casa (numerar firma)', 'casa': 'Casa (numerar firma)',
@@ -279,6 +277,12 @@ const formatProcessingTime = (ms) => {
color: #166534; color: #166534;
} }
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.overall-confidence { .overall-confidence {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -287,64 +291,145 @@ const formatProcessingTime = (ms) => {
color: #166534; color: #166534;
} }
.collapse-btn {
color: #166534 !important;
}
.preview-content { .preview-content {
padding: 1rem; padding: 0.75rem 1rem;
} }
.preview-grid { /* Section-based layout (bon fiscal style) */
display: grid; .ocr-section {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); padding: 0.6rem 0;
gap: 1rem; border-bottom: 1px solid #d1fae5;
} }
.preview-field { .ocr-section:last-of-type {
display: flex; border-bottom: none;
flex-direction: column;
gap: 0.25rem;
} }
.preview-field.full-width { .ocr-section-title {
grid-column: 1 / -1; font-size: 0.7rem;
font-weight: 600;
color: #166534;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.35rem;
opacity: 0.8;
} }
.preview-field label { .ocr-section-content {
font-size: 0.8rem;
color: #64748b;
display: flex;
align-items: center;
gap: 0.5rem;
}
.field-value {
font-weight: 500;
color: #1e293b; color: #1e293b;
} }
.field-value.amount { /* FURNIZOR section */
font-size: 1.25rem; .vendor-name {
font-weight: 600;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.vendor-cui {
font-size: 0.85rem;
color: #475569;
margin-top: 0.15rem;
}
.vendor-address {
font-size: 0.8rem;
color: #64748b;
margin-top: 0.15rem;
}
/* DOCUMENT section */
.document-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.doc-number {
font-weight: 500;
color: #334155;
}
.doc-date {
display: flex;
align-items: center;
gap: 0.35rem;
color: #475569;
font-size: 0.9rem;
}
.doc-date .pi-calendar {
font-size: 0.85rem;
color: #64748b;
}
/* TOTAL section - prominent */
.ocr-total-box {
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
border: 2px solid #86efac;
border-radius: 8px;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
.total-amount {
font-size: 1.5rem;
font-weight: 700;
color: #166534; color: #166534;
} }
.cui-badge { .items-count {
display: inline-block; text-align: center;
margin-left: 0.5rem;
padding: 0.15rem 0.5rem;
background: #e2e8f0;
border-radius: 4px;
font-size: 0.8rem; font-size: 0.8rem;
color: #475569; color: #64748b;
margin-top: 0.35rem;
} }
.tva-breakdown { /* PLATA section */
.ocr-payment-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.suggested-payment-mode {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.4rem 0.6rem;
background: #fef3c7;
border-radius: 6px;
font-size: 0.8rem;
color: #92400e;
}
.suggested-payment-mode .pi-lightbulb {
color: #f59e0b;
}
/* TVA section */
.ocr-tva-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.25rem;
} }
.tva-entry { .tva-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-size: 0.85rem;
} }
.tva-code { .tva-code {
@@ -353,49 +438,25 @@ const formatProcessingTime = (ms) => {
min-width: 1rem; min-width: 1rem;
} }
.tva-percent-badge { .tva-percent {
display: inline-block; color: #64748b;
padding: 0.15rem 0.5rem;
background: #dbeafe;
border-radius: 4px;
font-size: 0.8rem; font-size: 0.8rem;
color: #1e40af;
min-width: 2.5rem;
text-align: center;
} }
.tva-amount { .tva-amount {
font-weight: 500; font-weight: 500;
margin-left: auto;
} }
.tva-total { .tva-total-row {
margin-top: 0.25rem; margin-top: 0.25rem;
padding-top: 0.25rem; padding-top: 0.25rem;
border-top: 1px dashed #cbd5e1; border-top: 1px dashed #86efac;
} }
.payment-methods-list { .tva-label {
display: flex; font-weight: 600;
flex-wrap: wrap; color: #166534;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.suggested-payment-mode {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: #fef3c7;
border-radius: 6px;
font-size: 0.85rem;
color: #92400e;
}
.address-text {
font-size: 0.9rem;
color: #475569;
} }
.raw-text-section { .raw-text-section {

View File

@@ -15,11 +15,13 @@
</div> </div>
<form @submit.prevent="saveReceipt"> <form @submit.prevent="saveReceipt">
<!-- OCR Upload Section (for both create and edit modes) --> <div class="receipt-form-layout">
<!-- COLOANA STÂNGA: Upload & OCR -->
<div class="form-column-left">
<div class="upload-section"> <div class="upload-section">
<h3> <h3>
<i class="pi pi-camera"></i> <i class="pi pi-camera"></i>
{{ isEditMode ? 'Re-scanare OCR (optional)' : 'Poza Bon (obligatoriu)' }} {{ isEditMode ? 'Re-scanare OCR (optional)' : 'Poza Bon' }}
</h3> </h3>
<!-- OCR Upload Zone --> <!-- OCR Upload Zone -->
@@ -30,20 +32,32 @@
@error="onOCRError" @error="onOCRError"
/> />
<!-- OCR Preview (when results are available) --> <!-- OCR Applied Banner (collapsed state) -->
<div
v-if="ocrData && ocrCollapsed"
class="ocr-applied-banner"
@click="ocrCollapsed = false"
>
<i class="pi pi-check-circle"></i>
<span>Date OCR aplicate</span>
<i class="pi pi-chevron-down"></i>
</div>
<!-- OCR Preview (expanded state) -->
<OCRPreview <OCRPreview
v-if="ocrData" v-if="ocrData && !ocrCollapsed"
:data="ocrData" :data="ocrData"
@apply="applyOCRData" @apply="applyOCRData"
@dismiss="dismissOCRData" @dismiss="dismissOCRData"
@collapse="ocrCollapsed = true"
/> />
</div> </div>
<!-- Standard Upload Section (for edit mode or additional files) --> <!-- Standard Upload Section (for edit mode or additional files) -->
<div class="upload-section" v-if="isEditMode || selectedFiles.length > 0"> <div class="upload-section" v-if="isEditMode || selectedFiles.length > 0">
<h3 v-if="isEditMode"> <h3 v-if="isEditMode">
<i class="pi pi-camera"></i> <i class="pi pi-images"></i>
Poza Bon (obligatoriu) Atasamente
</h3> </h3>
<h3 v-else-if="selectedFiles.length > 0"> <h3 v-else-if="selectedFiles.length > 0">
<i class="pi pi-paperclip"></i> <i class="pi pi-paperclip"></i>
@@ -65,11 +79,8 @@
> >
<template #empty> <template #empty>
<div class="upload-area"> <div class="upload-area">
<i class="pi pi-cloud-upload" style="font-size: 3rem; color: #667eea;"></i> <i class="pi pi-cloud-upload" style="font-size: 2rem; color: #667eea;"></i>
<p>Trage fisierele aici sau click pentru a selecta</p> <p>Trage fisierele aici</p>
<p style="font-size: 0.8rem; color: #888;">
Formate acceptate: JPG, PNG, PDF (max 10MB)
</p>
</div> </div>
</template> </template>
</FileUpload> </FileUpload>
@@ -87,7 +98,7 @@
:alt="att.filename" :alt="att.filename"
/> />
<div v-else class="pdf-preview"> <div v-else class="pdf-preview">
<i class="pi pi-file-pdf" style="font-size: 3rem;"></i> <i class="pi pi-file-pdf" style="font-size: 2rem;"></i>
<span>{{ att.filename }}</span> <span>{{ att.filename }}</span>
</div> </div>
<Button <Button
@@ -95,6 +106,7 @@
severity="danger" severity="danger"
rounded rounded
class="remove-btn" class="remove-btn"
size="small"
@click="removeExistingAttachment(att.id)" @click="removeExistingAttachment(att.id)"
/> />
</div> </div>
@@ -120,26 +132,65 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<Divider /> <!-- COLOANA DREAPTA: Formular Compact -->
<div class="form-column-right">
<!-- SECTION: FURNIZOR -->
<div class="form-section">
<div class="form-section-title">FURNIZOR</div>
<div class="form-section-content">
<div class="form-row">
<div class="form-field flex-2">
<label>Furnizor</label>
<AutoComplete
v-model="form.partner_name"
:suggestions="filteredPartners"
optionLabel="name"
field="name"
@complete="searchPartners"
@item-select="onPartnerSelect"
placeholder="Cauta furnizor..."
dropdown
:forceSelection="false"
/>
<small v-if="supplierSource" class="p-text-success supplier-selected">
<i class="pi pi-check-circle"></i>
Validat ({{ supplierSource }})
</small>
</div>
<div class="form-field flex-1">
<label>CUI</label>
<InputText v-model="form.cui" placeholder="RO12345678" />
<small v-if="supplierWarning.show" class="p-text-warning supplier-warning">
<i class="pi pi-exclamation-triangle"></i>
Negasit
</small>
</div>
</div>
<div class="form-row" v-if="form.vendor_address">
<div class="form-field flex-1">
<label>Adresa</label>
<InputText v-model="form.vendor_address" placeholder="Adresa furnizor" />
</div>
</div>
</div>
</div>
<!-- Receipt Details --> <!-- SECTION: DOCUMENT -->
<h3> <div class="form-section">
<i class="pi pi-info-circle"></i> <div class="form-section-title">DOCUMENT</div>
Detalii Bon <div class="form-section-content">
</h3> <div class="form-row">
<div class="form-grid">
<div class="form-field"> <div class="form-field">
<label>Tip Document *</label> <div class="radio-group-inline">
<div class="radio-group">
<div class="radio-item"> <div class="radio-item">
<RadioButton <RadioButton
v-model="form.receipt_type" v-model="form.receipt_type"
value="bon_fiscal" value="bon_fiscal"
inputId="type_bon" inputId="type_bon"
/> />
<label for="type_bon">Bon Fiscal</label> <label for="type_bon">Bon</label>
</div> </div>
<div class="radio-item"> <div class="radio-item">
<RadioButton <RadioButton
@@ -151,10 +202,23 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-field"> <div class="form-field">
<label>Directie *</label> <label>Nr.</label>
<div class="radio-group"> <InputText v-model="form.receipt_number" placeholder="12345" style="max-width: 120px;" />
</div>
<div class="form-field">
<label>Data *</label>
<Calendar
v-model="form.receipt_date"
dateFormat="dd.mm.yy"
showIcon
required
/>
</div>
</div>
<div class="form-row">
<div class="form-field">
<div class="radio-group-inline">
<div class="radio-item"> <div class="radio-item">
<RadioButton <RadioButton
v-model="form.direction" v-model="form.direction"
@@ -173,19 +237,17 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="form-field"> </div>
<label>Data Bon *</label>
<Calendar
v-model="form.receipt_date"
dateFormat="dd.mm.yy"
showIcon
required
/>
</div> </div>
<!-- SECTION: TOTAL -->
<div class="form-section">
<div class="form-section-title">TOTAL</div>
<div class="form-section-content">
<div class="form-row">
<div class="form-field"> <div class="form-field">
<label>Suma (RON) *</label> <label>Suma *</label>
<InputNumber <InputNumber
v-model="form.amount" v-model="form.amount"
mode="currency" mode="currency"
@@ -194,38 +256,10 @@
:minFractionDigits="2" :minFractionDigits="2"
:maxFractionDigits="2" :maxFractionDigits="2"
required required
class="amount-input"
/> />
</div> </div>
<div class="form-field flex-1">
<div class="form-field">
<label>Furnizor</label>
<AutoComplete
v-model="form.partner_name"
:suggestions="filteredPartners"
optionLabel="name"
field="name"
@complete="searchPartners"
@item-select="onPartnerSelect"
placeholder="Cauta furnizor..."
dropdown
:forceSelection="false"
/>
<small v-if="supplierSource" class="p-text-success supplier-selected">
<i class="pi pi-check-circle"></i>
Validat ({{ supplierSource }})
</small>
</div>
<div class="form-field">
<label>CUI (Cod Fiscal)</label>
<InputText v-model="form.cui" placeholder="Ex: RO12345678" />
<small v-if="supplierWarning.show" class="p-text-warning supplier-warning">
<i class="pi pi-exclamation-triangle"></i>
CUI {{ supplierWarning.cui }} negasit in nomenclator
</small>
</div>
<div class="form-field">
<label>Tip Cheltuiala *</label> <label>Tip Cheltuiala *</label>
<Dropdown <Dropdown
v-model="form.expense_type_code" v-model="form.expense_type_code"
@@ -236,9 +270,39 @@
required required
/> />
</div> </div>
</div>
<div class="form-row" v-if="form.items_count">
<div class="form-field"> <div class="form-field">
<label>Nr. Articole</label>
<InputNumber
v-model="form.items_count"
:min="1"
placeholder="17"
style="max-width: 100px;"
/>
</div>
</div>
<div class="form-row">
<div class="form-field flex-1">
<label>Descriere</label>
<Textarea
v-model="form.description"
rows="2"
placeholder="Descriere optionala..."
/>
</div>
</div>
</div>
</div>
<!-- SECTION: PLATA -->
<div class="form-section">
<div class="form-section-title">PLATA</div>
<div class="form-section-content">
<div class="form-row">
<div class="form-field flex-1">
<label>Mod Plata</label> <label>Mod Plata</label>
<div class="payment-field-wrapper">
<Dropdown <Dropdown
v-model="form.payment_mode" v-model="form.payment_mode"
:options="paymentModeOptions" :options="paymentModeOptions"
@@ -247,86 +311,56 @@
placeholder="Selecteaza mod plata" placeholder="Selecteaza mod plata"
showClear showClear
/> />
<span v-if="paymentSetFromOCR" class="ocr-indicator">
<i class="pi pi-check-circle"></i>
din OCR
</span>
</div>
<small class="field-hint text-secondary" v-if="!form.payment_mode"> <small class="field-hint text-secondary" v-if="!form.payment_mode">
Obligatoriu la trimiterea pentru aprobare Obligatoriu la trimitere
</small> </small>
</div> </div>
<div class="form-field">
<label>Numar Bon</label>
<InputText v-model="form.receipt_number" placeholder="Optional" />
</div> </div>
<!-- Payment methods tags from OCR -->
<div class="form-field form-field-full"> <div v-if="form.payment_methods?.length > 0" class="payment-methods-display">
<label>Descriere</label> <Tag
<Textarea v-for="pm in form.payment_methods"
v-model="form.description" :key="pm.method"
rows="3" :severity="pm.method === 'CARD' ? 'info' : 'success'"
placeholder="Descriere optionala..." :value="`${pm.method}: ${formatCurrency(pm.amount)}`"
class="mr-1"
/> />
</div> </div>
</div> </div>
</div>
<!-- Detalii Suplimentare (populated from OCR) --> <!-- SECTION: TVA -->
<div v-if="form.tva_breakdown?.length > 0 || form.items_count || form.vendor_address || form.payment_methods?.length > 0" class="extra-details-section"> <div class="form-section" v-if="form.tva_breakdown?.length > 0">
<h3> <div class="form-section-title">TVA</div>
<i class="pi pi-list"></i> <div class="form-section-content">
Detalii Suplimentare (din OCR) <div class="tva-edit-table">
</h3> <div v-for="(entry, idx) in form.tva_breakdown" :key="idx" class="tva-edit-row">
<span class="tva-label-compact">{{ entry.code }} ({{ entry.percent }}%)</span>
<!-- TVA Breakdown -->
<div class="form-field form-field-full" v-if="form.tva_breakdown?.length > 0">
<label>Defalcare TVA</label>
<div class="tva-table">
<div v-for="(entry, idx) in form.tva_breakdown" :key="idx" class="tva-row">
<span class="tva-label">TVA {{ entry.code }} ({{ entry.percent }}%):</span>
<InputNumber <InputNumber
v-model="form.tva_breakdown[idx].amount" v-model="form.tva_breakdown[idx].amount"
mode="currency" mode="currency"
currency="RON" currency="RON"
locale="ro-RO" locale="ro-RO"
:minFractionDigits="2" :minFractionDigits="2"
class="tva-input" class="tva-input-compact"
/> />
</div> </div>
<div class="tva-row total" v-if="form.tva_breakdown.length > 0"> <div class="tva-edit-row tva-total-row" v-if="form.tva_breakdown.length > 0">
<span class="tva-label"><strong>Total TVA:</strong></span> <span class="tva-label-compact"><strong>Total TVA:</strong></span>
<span class="tva-value">{{ formatTvaTotal() }} RON</span> <span class="tva-total-value">{{ formatTvaTotal() }} RON</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Payment Methods (from OCR) -->
<div class="form-field form-field-full" v-if="form.payment_methods?.length > 0">
<label>Modalitati Plata</label>
<div class="payment-methods-display">
<Tag
v-for="pm in form.payment_methods"
:key="pm.method"
:severity="pm.method === 'CARD' ? 'info' : 'success'"
:value="`${pm.method}: ${formatCurrency(pm.amount)}`"
/>
</div>
</div>
<div class="form-grid">
<div class="form-field" v-if="form.items_count">
<label>Nr. Articole</label>
<InputNumber
v-model="form.items_count"
:min="1"
placeholder="Ex: 17"
/>
</div>
<div class="form-field" v-if="form.vendor_address">
<label>Adresa Furnizor</label>
<InputText
v-model="form.vendor_address"
placeholder="Adresa din bon"
/>
</div> </div>
</div> </div>
<!-- End form-column-right -->
</div> </div>
<!-- End receipt-form-layout -->
<Divider /> <Divider />
@@ -457,12 +491,16 @@ const submitting = ref(false)
const ocrUploadZone = ref(null) const ocrUploadZone = ref(null)
const ocrData = ref(null) const ocrData = ref(null)
const ocrFile = ref(null) const ocrFile = ref(null)
const ocrCollapsed = ref(false)
// Supplier dialog refs // Supplier dialog refs
const showCreateSupplierDialog = ref(false) const showCreateSupplierDialog = ref(false)
const pendingSupplierData = ref(null) const pendingSupplierData = ref(null)
const supplierWarning = ref({ show: false, cui: '', name: '' }) const supplierWarning = ref({ show: false, cui: '', name: '' })
// OCR indicator for payment mode
const paymentSetFromOCR = ref(false)
// AutoComplete support // AutoComplete support
const filteredPartners = ref([]) const filteredPartners = ref([])
const supplierSource = ref(null) // 'local', 'synced', or null const supplierSource = ref(null) // 'local', 'synced', or null
@@ -618,6 +656,7 @@ const applyOCRData = async (data) => {
// Auto-suggest payment_mode if OCR detected CARD // Auto-suggest payment_mode if OCR detected CARD
if (data.suggested_payment_mode) { if (data.suggested_payment_mode) {
form.value.payment_mode = data.suggested_payment_mode form.value.payment_mode = data.suggested_payment_mode
paymentSetFromOCR.value = true // Show OCR indicator
} }
// Auto-search supplier by CUI if available // Auto-search supplier by CUI if available
@@ -695,8 +734,8 @@ const applyOCRData = async (data) => {
} }
} }
// Clear OCR preview // NOTE: OCRPreview rămâne vizibil pentru comparație side-by-side
ocrData.value = null // (NU mai colapsăm automat - utilizatorul poate compara datele)
toast.add({ toast.add({
severity: 'success', severity: 'success',
@@ -811,7 +850,8 @@ const formatTvaTotal = () => {
const validateForm = () => { const validateForm = () => {
// Check if we have at least one file (for new receipts) // Check if we have at least one file (for new receipts)
if (!isEditMode.value && selectedFiles.value.length === 0) { // Also check ocrFile as a fallback (file selected for OCR processing)
if (!isEditMode.value && selectedFiles.value.length === 0 && !ocrFile.value) {
toast.add({ toast.add({
severity: 'warn', severity: 'warn',
summary: 'Validare', summary: 'Validare',
@@ -953,15 +993,86 @@ const submitForReview = async () => {
</script> </script>
<style scoped> <style scoped>
/* 2-column layout */
.receipt-form-layout {
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(380px, 1.5fr);
gap: 1.5rem;
align-items: start;
}
.form-column-left {
position: sticky;
top: 1rem;
}
.form-column-right {
min-width: 0;
}
.form-column-right h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
margin-top: 0;
}
@media (max-width: 1024px) {
.receipt-form-layout {
grid-template-columns: 1fr;
}
.form-column-left {
position: static;
}
}
/* OCR Applied Banner (collapsed state) */
.ocr-applied-banner {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
background: #dcfce7;
border: 1px solid #86efac;
border-radius: 8px;
cursor: pointer;
margin-top: 0.75rem;
transition: background 0.2s;
}
.ocr-applied-banner:hover {
background: #bbf7d0;
}
.ocr-applied-banner .pi-check-circle {
color: #22c55e;
font-size: 1.1rem;
}
.ocr-applied-banner span {
flex: 1;
font-weight: 500;
color: #166534;
font-size: 0.9rem;
}
.ocr-applied-banner .pi-chevron-down {
color: #166534;
font-size: 0.8rem;
}
.upload-section { .upload-section {
margin-bottom: 1.5rem; margin-bottom: 1rem;
} }
.upload-section h3 { .upload-section h3 {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1rem; margin-bottom: 0.75rem;
margin-top: 0;
font-size: 1rem;
} }
.radio-group { .radio-group {
@@ -1031,8 +1142,8 @@ const submitForReview = async () => {
/* Extra details section (TVA, items, address) */ /* Extra details section (TVA, items, address) */
.extra-details-section { .extra-details-section {
margin-top: 1.5rem; margin-top: 1rem;
padding: 1rem; padding: 0.75rem;
background: #f0f9ff; background: #f0f9ff;
border: 1px solid #bae6fd; border: 1px solid #bae6fd;
border-radius: 8px; border-radius: 8px;
@@ -1042,8 +1153,10 @@ const submitForReview = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1rem; margin-bottom: 0.75rem;
margin-top: 0;
color: #0284c7; color: #0284c7;
font-size: 0.95rem;
} }
.tva-table { .tva-table {
@@ -1142,4 +1255,163 @@ const submitForReview = async () => {
font-weight: 500; font-weight: 500;
color: #334155; color: #334155;
} }
/* ========================================
COMPACT FORM SECTIONS (matching OCRPreview)
======================================== */
.form-section {
padding: 0.6rem 0;
border-bottom: 1px solid #e2e8f0;
}
.form-section:last-of-type {
border-bottom: none;
}
.form-section-title {
font-size: 0.7rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.form-section-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Compact form rows */
.form-row {
display: flex;
gap: 0.75rem;
align-items: flex-start;
flex-wrap: wrap;
}
.form-row .form-field {
margin-bottom: 0;
}
.form-field.flex-1 {
flex: 1;
min-width: 150px;
}
.form-field.flex-2 {
flex: 2;
min-width: 200px;
}
/* Inline radio groups */
.radio-group-inline {
display: flex;
gap: 1rem;
padding: 0.35rem 0;
}
.radio-group-inline .radio-item {
display: flex;
align-items: center;
gap: 0.35rem;
}
.radio-group-inline .radio-item label {
font-size: 0.9rem;
margin-bottom: 0;
}
/* Amount input styling */
.amount-input {
max-width: 150px;
}
/* Payment field wrapper with OCR indicator */
.payment-field-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.payment-field-wrapper .p-dropdown {
flex: 1;
}
.ocr-indicator {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #22c55e;
font-weight: 500;
white-space: nowrap;
}
.ocr-indicator .pi-check-circle {
font-size: 0.85rem;
}
/* TVA edit table - compact */
.tva-edit-table {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.tva-edit-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.tva-label-compact {
min-width: 80px;
font-size: 0.85rem;
color: #475569;
}
.tva-input-compact {
max-width: 120px;
}
.tva-edit-row.tva-total-row {
margin-top: 0.35rem;
padding-top: 0.35rem;
border-top: 1px dashed #cbd5e1;
}
.tva-total-value {
font-weight: 600;
color: #0284c7;
font-size: 0.9rem;
}
/* Payment methods display in form */
.payment-methods-display {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.35rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
.form-field.flex-1,
.form-field.flex-2 {
width: 100%;
}
.payment-field-wrapper {
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
}
}
</style> </style>