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'"
/> />
<span v-if="data.receipt_number" class="doc-number">
Nr: {{ data.receipt_series ? data.receipt_series + ' ' : '' }}{{ data.receipt_number }}
</span>
<span v-if="data.receipt_date" class="doc-date">
<i class="pi pi-calendar"></i>
{{ formatDate(data.receipt_date) }}
<OCRConfidenceIndicator :confidence="data.confidence_date" size="small" />
</span>
</div> </div>
</div> </div>
</div>
<!-- Amount --> <!-- SECTION: TOTAL -->
<div class="preview-field" v-if="data.amount"> <div class="ocr-section" v-if="data.amount">
<label> <div class="ocr-section-title">TOTAL</div>
Suma <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" /> <OCRConfidenceIndicator :confidence="data.confidence_amount" size="small" />
</label> </div>
<div class="field-value amount"> <div v-if="data.items_count" class="items-count">
{{ formatAmount(data.amount) }} RON {{ data.items_count }} articole
</div> </div>
</div> </div>
</div>
<!-- Date --> <!-- SECTION: PLATA -->
<div class="preview-field" v-if="data.receipt_date"> <div class="ocr-section" v-if="data.payment_methods?.length > 0">
<label> <div class="ocr-section-title">PLATA</div>
Data <div class="ocr-section-content">
<OCRConfidenceIndicator :confidence="data.confidence_date" size="small" /> <div class="ocr-payment-tags">
</label>
<div class="field-value">
{{ formatDate(data.receipt_date) }}
</div>
</div>
<!-- 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>
<!-- Payment Methods from OCR -->
<div class="preview-field full-width" v-if="data.payment_methods?.length > 0">
<label>Modalitati Plata (OCR)</label>
<div class="payment-methods-list">
<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>
</div>
<!-- Items Count --> <!-- SECTION: TVA -->
<div class="preview-field" v-if="data.items_count"> <div class="ocr-section" v-if="data.tva_entries?.length > 0 || data.tva_total">
<label>Nr. Articole</label> <div class="ocr-section-title">TVA</div>
<div class="field-value"> <div class="ocr-section-content">
{{ data.items_count }} articole <div class="ocr-tva-table">
</div> <div v-for="(entry, idx) in data.tva_entries" :key="idx" class="tva-row">
</div> <span class="tva-code" v-if="entry.code">{{ entry.code }}</span>
<span class="tva-percent">({{ entry.percent }}%)</span>
<!-- Address --> <span class="tva-amount">{{ formatAmount(entry.amount) }} LEI</span>
<div class="preview-field full-width" v-if="data.address"> </div>
<label>Adresa</label> <div v-if="computedTvaTotal > 0" class="tva-row tva-total-row">
<div class="field-value address-text"> <span class="tva-label">Total TVA:</span>
{{ data.address }} <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,318 +15,352 @@
</div> </div>
<form @submit.prevent="saveReceipt"> <form @submit.prevent="saveReceipt">
<!-- OCR Upload Section (for both create and edit modes) --> <div class="receipt-form-layout">
<div class="upload-section"> <!-- COLOANA STÂNGA: Upload & OCR -->
<h3> <div class="form-column-left">
<i class="pi pi-camera"></i> <div class="upload-section">
{{ isEditMode ? 'Re-scanare OCR (optional)' : 'Poza Bon (obligatoriu)' }} <h3>
</h3> <i class="pi pi-camera"></i>
{{ isEditMode ? 'Re-scanare OCR (optional)' : 'Poza Bon' }}
</h3>
<!-- OCR Upload Zone --> <!-- OCR Upload Zone -->
<OCRUploadZone <OCRUploadZone
ref="ocrUploadZone" ref="ocrUploadZone"
@ocr-result="onOCRResult" @ocr-result="onOCRResult"
@file-selected="onOCRFileSelected" @file-selected="onOCRFileSelected"
@error="onOCRError" @error="onOCRError"
/> />
<!-- OCR Preview (when results are available) --> <!-- OCR Applied Banner (collapsed state) -->
<OCRPreview <div
v-if="ocrData" v-if="ocrData && ocrCollapsed"
:data="ocrData" class="ocr-applied-banner"
@apply="applyOCRData" @click="ocrCollapsed = false"
@dismiss="dismissOCRData" >
/> <i class="pi pi-check-circle"></i>
</div> <span>Date OCR aplicate</span>
<i class="pi pi-chevron-down"></i>
<!-- Standard Upload Section (for edit mode or additional files) --> </div>
<div class="upload-section" v-if="isEditMode || selectedFiles.length > 0">
<h3 v-if="isEditMode"> <!-- OCR Preview (expanded state) -->
<i class="pi pi-camera"></i> <OCRPreview
Poza Bon (obligatoriu) v-if="ocrData && !ocrCollapsed"
</h3> :data="ocrData"
<h3 v-else-if="selectedFiles.length > 0"> @apply="applyOCRData"
<i class="pi pi-paperclip"></i> @dismiss="dismissOCRData"
Fisiere Selectate @collapse="ocrCollapsed = true"
</h3> />
</div>
<FileUpload
v-if="isEditMode" <!-- Standard Upload Section (for edit mode or additional files) -->
ref="fileUpload" <div class="upload-section" v-if="isEditMode || selectedFiles.length > 0">
mode="advanced" <h3 v-if="isEditMode">
:multiple="true" <i class="pi pi-images"></i>
accept="image/*,application/pdf" Atasamente
:maxFileSize="10000000" </h3>
@select="onFileSelect" <h3 v-else-if="selectedFiles.length > 0">
@remove="onFileRemove" <i class="pi pi-paperclip"></i>
:auto="false" Fisiere Selectate
:showUploadButton="false" </h3>
:showCancelButton="false"
> <FileUpload
<template #empty> v-if="isEditMode"
<div class="upload-area"> ref="fileUpload"
<i class="pi pi-cloud-upload" style="font-size: 3rem; color: #667eea;"></i> mode="advanced"
<p>Trage fisierele aici sau click pentru a selecta</p> :multiple="true"
<p style="font-size: 0.8rem; color: #888;"> accept="image/*,application/pdf"
Formate acceptate: JPG, PNG, PDF (max 10MB) :maxFileSize="10000000"
</p> @select="onFileSelect"
</div> @remove="onFileRemove"
</template> :auto="false"
</FileUpload> :showUploadButton="false"
:showCancelButton="false"
<!-- Existing attachments (edit mode) --> >
<div v-if="existingAttachments.length" class="image-preview-grid"> <template #empty>
<div <div class="upload-area">
v-for="att in existingAttachments" <i class="pi pi-cloud-upload" style="font-size: 2rem; color: #667eea;"></i>
:key="att.id" <p>Trage fisierele aici</p>
class="image-preview-item" </div>
> </template>
<img </FileUpload>
v-if="att.mime_type?.startsWith('image/')"
:src="store.getAttachmentUrl(att.id)" <!-- Existing attachments (edit mode) -->
:alt="att.filename" <div v-if="existingAttachments.length" class="image-preview-grid">
/> <div
<div v-else class="pdf-preview"> v-for="att in existingAttachments"
<i class="pi pi-file-pdf" style="font-size: 3rem;"></i> :key="att.id"
<span>{{ att.filename }}</span> class="image-preview-item"
</div> >
<Button <img
icon="pi pi-times" v-if="att.mime_type?.startsWith('image/')"
severity="danger" :src="store.getAttachmentUrl(att.id)"
rounded :alt="att.filename"
class="remove-btn" />
@click="removeExistingAttachment(att.id)" <div v-else class="pdf-preview">
/> <i class="pi pi-file-pdf" style="font-size: 2rem;"></i>
</div> <span>{{ att.filename }}</span>
</div> </div>
<Button
<!-- Selected files preview (create mode) --> icon="pi pi-times"
<div v-if="!isEditMode && selectedFiles.length" class="selected-files-list"> severity="danger"
<div rounded
v-for="(file, index) in selectedFiles" class="remove-btn"
:key="index" size="small"
class="selected-file-item" @click="removeExistingAttachment(att.id)"
> />
<i :class="file.type.startsWith('image/') ? 'pi pi-image' : 'pi pi-file-pdf'"></i> </div>
<span class="file-name">{{ file.name }}</span> </div>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<Button <!-- Selected files preview (create mode) -->
icon="pi pi-times" <div v-if="!isEditMode && selectedFiles.length" class="selected-files-list">
severity="danger" <div
rounded v-for="(file, index) in selectedFiles"
size="small" :key="index"
@click="removeSelectedFile(index)" class="selected-file-item"
/> >
</div> <i :class="file.type.startsWith('image/') ? 'pi pi-image' : 'pi pi-file-pdf'"></i>
</div> <span class="file-name">{{ file.name }}</span>
</div> <span class="file-size">{{ formatFileSize(file.size) }}</span>
<Button
<Divider /> icon="pi pi-times"
severity="danger"
<!-- Receipt Details --> rounded
<h3> size="small"
<i class="pi pi-info-circle"></i> @click="removeSelectedFile(index)"
Detalii Bon />
</h3> </div>
</div>
<div class="form-grid"> </div>
<div class="form-field"> </div>
<label>Tip Document *</label>
<div class="radio-group"> <!-- COLOANA DREAPTA: Formular Compact -->
<div class="radio-item"> <div class="form-column-right">
<RadioButton <!-- SECTION: FURNIZOR -->
v-model="form.receipt_type" <div class="form-section">
value="bon_fiscal" <div class="form-section-title">FURNIZOR</div>
inputId="type_bon" <div class="form-section-content">
/> <div class="form-row">
<label for="type_bon">Bon Fiscal</label> <div class="form-field flex-2">
</div> <label>Furnizor</label>
<div class="radio-item"> <AutoComplete
<RadioButton v-model="form.partner_name"
v-model="form.receipt_type" :suggestions="filteredPartners"
value="chitanta" optionLabel="name"
inputId="type_chitanta" field="name"
/> @complete="searchPartners"
<label for="type_chitanta">Chitanta</label> @item-select="onPartnerSelect"
</div> placeholder="Cauta furnizor..."
</div> dropdown
</div> :forceSelection="false"
/>
<div class="form-field"> <small v-if="supplierSource" class="p-text-success supplier-selected">
<label>Directie *</label> <i class="pi pi-check-circle"></i>
<div class="radio-group"> Validat ({{ supplierSource }})
<div class="radio-item"> </small>
<RadioButton </div>
v-model="form.direction" <div class="form-field flex-1">
value="cheltuiala" <label>CUI</label>
inputId="dir_cheltuiala" <InputText v-model="form.cui" placeholder="RO12345678" />
/> <small v-if="supplierWarning.show" class="p-text-warning supplier-warning">
<label for="dir_cheltuiala">Cheltuiala</label> <i class="pi pi-exclamation-triangle"></i>
</div> Negasit
<div class="radio-item"> </small>
<RadioButton </div>
v-model="form.direction" </div>
value="incasare" <div class="form-row" v-if="form.vendor_address">
inputId="dir_incasare" <div class="form-field flex-1">
/> <label>Adresa</label>
<label for="dir_incasare">Incasare</label> <InputText v-model="form.vendor_address" placeholder="Adresa furnizor" />
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="form-field">
<label>Data Bon *</label> <!-- SECTION: DOCUMENT -->
<Calendar <div class="form-section">
v-model="form.receipt_date" <div class="form-section-title">DOCUMENT</div>
dateFormat="dd.mm.yy" <div class="form-section-content">
showIcon <div class="form-row">
required <div class="form-field">
/> <div class="radio-group-inline">
</div> <div class="radio-item">
<RadioButton
<div class="form-field"> v-model="form.receipt_type"
<label>Suma (RON) *</label> value="bon_fiscal"
<InputNumber inputId="type_bon"
v-model="form.amount" />
mode="currency" <label for="type_bon">Bon</label>
currency="RON" </div>
locale="ro-RO" <div class="radio-item">
:minFractionDigits="2" <RadioButton
:maxFractionDigits="2" v-model="form.receipt_type"
required value="chitanta"
/> inputId="type_chitanta"
</div> />
<label for="type_chitanta">Chitanta</label>
<div class="form-field"> </div>
<label>Furnizor</label> </div>
<AutoComplete </div>
v-model="form.partner_name" <div class="form-field">
:suggestions="filteredPartners" <label>Nr.</label>
optionLabel="name" <InputText v-model="form.receipt_number" placeholder="12345" style="max-width: 120px;" />
field="name" </div>
@complete="searchPartners" <div class="form-field">
@item-select="onPartnerSelect" <label>Data *</label>
placeholder="Cauta furnizor..." <Calendar
dropdown v-model="form.receipt_date"
:forceSelection="false" dateFormat="dd.mm.yy"
/> showIcon
<small v-if="supplierSource" class="p-text-success supplier-selected"> required
<i class="pi pi-check-circle"></i> />
Validat ({{ supplierSource }}) </div>
</small> </div>
</div> <div class="form-row">
<div class="form-field">
<div class="form-field"> <div class="radio-group-inline">
<label>CUI (Cod Fiscal)</label> <div class="radio-item">
<InputText v-model="form.cui" placeholder="Ex: RO12345678" /> <RadioButton
<small v-if="supplierWarning.show" class="p-text-warning supplier-warning"> v-model="form.direction"
<i class="pi pi-exclamation-triangle"></i> value="cheltuiala"
CUI {{ supplierWarning.cui }} negasit in nomenclator inputId="dir_cheltuiala"
</small> />
</div> <label for="dir_cheltuiala">Cheltuiala</label>
</div>
<div class="form-field"> <div class="radio-item">
<label>Tip Cheltuiala *</label> <RadioButton
<Dropdown v-model="form.direction"
v-model="form.expense_type_code" value="incasare"
:options="expenseTypes" inputId="dir_incasare"
optionLabel="name" />
optionValue="code" <label for="dir_incasare">Incasare</label>
placeholder="Selecteaza tip" </div>
required </div>
/> </div>
</div> </div>
</div>
<div class="form-field"> </div>
<label>Mod Plata</label>
<Dropdown <!-- SECTION: TOTAL -->
v-model="form.payment_mode" <div class="form-section">
:options="paymentModeOptions" <div class="form-section-title">TOTAL</div>
optionLabel="label" <div class="form-section-content">
optionValue="value" <div class="form-row">
placeholder="Selecteaza mod plata" <div class="form-field">
showClear <label>Suma *</label>
/> <InputNumber
<small class="field-hint text-secondary" v-if="!form.payment_mode"> v-model="form.amount"
Obligatoriu la trimiterea pentru aprobare mode="currency"
</small> currency="RON"
</div> locale="ro-RO"
:minFractionDigits="2"
<div class="form-field"> :maxFractionDigits="2"
<label>Numar Bon</label> required
<InputText v-model="form.receipt_number" placeholder="Optional" /> class="amount-input"
</div> />
</div>
<div class="form-field form-field-full"> <div class="form-field flex-1">
<label>Descriere</label> <label>Tip Cheltuiala *</label>
<Textarea <Dropdown
v-model="form.description" v-model="form.expense_type_code"
rows="3" :options="expenseTypes"
placeholder="Descriere optionala..." optionLabel="name"
/> optionValue="code"
</div> placeholder="Selecteaza tip"
</div> required
/>
<!-- Detalii Suplimentare (populated from OCR) --> </div>
<div v-if="form.tva_breakdown?.length > 0 || form.items_count || form.vendor_address || form.payment_methods?.length > 0" class="extra-details-section"> </div>
<h3> <div class="form-row" v-if="form.items_count">
<i class="pi pi-list"></i> <div class="form-field">
Detalii Suplimentare (din OCR) <label>Nr. Articole</label>
</h3> <InputNumber
v-model="form.items_count"
<!-- TVA Breakdown --> :min="1"
<div class="form-field form-field-full" v-if="form.tva_breakdown?.length > 0"> placeholder="17"
<label>Defalcare TVA</label> style="max-width: 100px;"
<div class="tva-table"> />
<div v-for="(entry, idx) in form.tva_breakdown" :key="idx" class="tva-row"> </div>
<span class="tva-label">TVA {{ entry.code }} ({{ entry.percent }}%):</span> </div>
<InputNumber <div class="form-row">
v-model="form.tva_breakdown[idx].amount" <div class="form-field flex-1">
mode="currency" <label>Descriere</label>
currency="RON" <Textarea
locale="ro-RO" v-model="form.description"
:minFractionDigits="2" rows="2"
class="tva-input" placeholder="Descriere optionala..."
/> />
</div> </div>
<div class="tva-row total" v-if="form.tva_breakdown.length > 0"> </div>
<span class="tva-label"><strong>Total TVA:</strong></span> </div>
<span class="tva-value">{{ formatTvaTotal() }} RON</span> </div>
</div>
</div> <!-- SECTION: PLATA -->
</div> <div class="form-section">
<div class="form-section-title">PLATA</div>
<!-- Payment Methods (from OCR) --> <div class="form-section-content">
<div class="form-field form-field-full" v-if="form.payment_methods?.length > 0"> <div class="form-row">
<label>Modalitati Plata</label> <div class="form-field flex-1">
<div class="payment-methods-display"> <label>Mod Plata</label>
<Tag <div class="payment-field-wrapper">
v-for="pm in form.payment_methods" <Dropdown
:key="pm.method" v-model="form.payment_mode"
:severity="pm.method === 'CARD' ? 'info' : 'success'" :options="paymentModeOptions"
:value="`${pm.method}: ${formatCurrency(pm.amount)}`" optionLabel="label"
/> optionValue="value"
</div> placeholder="Selecteaza mod plata"
</div> showClear
/>
<div class="form-grid"> <span v-if="paymentSetFromOCR" class="ocr-indicator">
<div class="form-field" v-if="form.items_count"> <i class="pi pi-check-circle"></i>
<label>Nr. Articole</label> din OCR
<InputNumber </span>
v-model="form.items_count" </div>
:min="1" <small class="field-hint text-secondary" v-if="!form.payment_mode">
placeholder="Ex: 17" Obligatoriu la trimitere
/> </small>
</div> </div>
<div class="form-field" v-if="form.vendor_address"> </div>
<label>Adresa Furnizor</label> <!-- Payment methods tags from OCR -->
<InputText <div v-if="form.payment_methods?.length > 0" class="payment-methods-display">
v-model="form.vendor_address" <Tag
placeholder="Adresa din bon" v-for="pm in form.payment_methods"
/> :key="pm.method"
</div> :severity="pm.method === 'CARD' ? 'info' : 'success'"
</div> :value="`${pm.method}: ${formatCurrency(pm.amount)}`"
class="mr-1"
/>
</div>
</div>
</div>
<!-- SECTION: TVA -->
<div class="form-section" v-if="form.tva_breakdown?.length > 0">
<div class="form-section-title">TVA</div>
<div class="form-section-content">
<div class="tva-edit-table">
<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>
<InputNumber
v-model="form.tva_breakdown[idx].amount"
mode="currency"
currency="RON"
locale="ro-RO"
:minFractionDigits="2"
class="tva-input-compact"
/>
</div>
<div class="tva-edit-row tva-total-row" v-if="form.tva_breakdown.length > 0">
<span class="tva-label-compact"><strong>Total TVA:</strong></span>
<span class="tva-total-value">{{ formatTvaTotal() }} RON</span>
</div>
</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>