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 class="header-right">
<span class="overall-confidence">
Incredere generala:
<OCRConfidenceIndicator
:confidence="data.overall_confidence"
:show-percentage="true"
size="normal"
/>
</span>
<Button
icon="pi pi-minus"
text
rounded
size="small"
@click="$emit('collapse')"
v-tooltip="'Minimizeaza'"
class="collapse-btn"
/>
</div>
</div>
<div class="preview-content">
<div class="preview-grid">
<!-- Receipt Type -->
<div class="preview-field" v-if="data.receipt_type">
<label>Tip Document</label>
<div class="field-value">
<!-- SECTION: FURNIZOR -->
<div class="ocr-section" v-if="data.partner_name || data.cui || data.address">
<div class="ocr-section-title">FURNIZOR</div>
<div class="ocr-section-content">
<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
v-if="data.receipt_type"
:value="data.receipt_type === 'bon_fiscal' ? 'Bon Fiscal' : 'Chitanta'"
: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>
<!-- Amount -->
<div class="preview-field" v-if="data.amount">
<label>
Suma
<!-- SECTION: TOTAL -->
<div class="ocr-section" v-if="data.amount">
<div class="ocr-section-title">TOTAL</div>
<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" />
</label>
<div class="field-value amount">
{{ formatAmount(data.amount) }} RON
</div>
<div v-if="data.items_count" class="items-count">
{{ data.items_count }} articole
</div>
</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) }}
</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">
<!-- 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
v-for="(pm, idx) in data.payment_methods"
:key="idx"
:severity="pm.method === 'CARD' ? 'info' : 'success'"
:value="`${pm.method}: ${formatAmount(pm.amount)} RON`"
class="mr-1"
:value="`${pm.method}: ${formatAmount(pm.amount)} LEI`"
/>
</div>
<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>
</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>
<!-- Address -->
<div class="preview-field full-width" v-if="data.address">
<label>Adresa</label>
<div class="field-value address-text">
{{ data.address }}
<!-- SECTION: TVA -->
<div class="ocr-section" v-if="data.tva_entries?.length > 0 || data.tva_total">
<div class="ocr-section-title">TVA</div>
<div class="ocr-section-content">
<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>
@@ -168,9 +158,10 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import OCRConfidenceIndicator from './OCRConfidenceIndicator.vue'
import Tag from 'primevue/tag'
import Button from 'primevue/button'
const props = defineProps({
data: {
@@ -179,10 +170,17 @@ const props = defineProps({
}
})
defineEmits(['apply', 'dismiss'])
defineEmits(['apply', 'dismiss', 'collapse'])
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 labels = {
'casa': 'Casa (numerar firma)',
@@ -279,6 +277,12 @@ const formatProcessingTime = (ms) => {
color: #166534;
}
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.overall-confidence {
display: flex;
align-items: center;
@@ -287,64 +291,145 @@ const formatProcessingTime = (ms) => {
color: #166534;
}
.collapse-btn {
color: #166534 !important;
}
.preview-content {
padding: 1rem;
padding: 0.75rem 1rem;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
/* Section-based layout (bon fiscal style) */
.ocr-section {
padding: 0.6rem 0;
border-bottom: 1px solid #d1fae5;
}
.preview-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
.ocr-section:last-of-type {
border-bottom: none;
}
.preview-field.full-width {
grid-column: 1 / -1;
.ocr-section-title {
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 {
font-size: 0.8rem;
color: #64748b;
display: flex;
align-items: center;
gap: 0.5rem;
}
.field-value {
font-weight: 500;
.ocr-section-content {
color: #1e293b;
}
.field-value.amount {
font-size: 1.25rem;
/* FURNIZOR section */
.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;
}
.cui-badge {
display: inline-block;
margin-left: 0.5rem;
padding: 0.15rem 0.5rem;
background: #e2e8f0;
border-radius: 4px;
.items-count {
text-align: center;
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;
flex-direction: column;
gap: 0.25rem;
}
.tva-entry {
.tva-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
}
.tva-code {
@@ -353,49 +438,25 @@ const formatProcessingTime = (ms) => {
min-width: 1rem;
}
.tva-percent-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
background: #dbeafe;
border-radius: 4px;
.tva-percent {
color: #64748b;
font-size: 0.8rem;
color: #1e40af;
min-width: 2.5rem;
text-align: center;
}
.tva-amount {
font-weight: 500;
margin-left: auto;
}
.tva-total {
.tva-total-row {
margin-top: 0.25rem;
padding-top: 0.25rem;
border-top: 1px dashed #cbd5e1;
border-top: 1px dashed #86efac;
}
.payment-methods-list {
display: flex;
flex-wrap: wrap;
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;
.tva-label {
font-weight: 600;
color: #166534;
}
.raw-text-section {

View File

@@ -15,318 +15,352 @@
</div>
<form @submit.prevent="saveReceipt">
<!-- OCR Upload Section (for both create and edit modes) -->
<div class="upload-section">
<h3>
<i class="pi pi-camera"></i>
{{ isEditMode ? 'Re-scanare OCR (optional)' : 'Poza Bon (obligatoriu)' }}
</h3>
<div class="receipt-form-layout">
<!-- COLOANA STÂNGA: Upload & OCR -->
<div class="form-column-left">
<div class="upload-section">
<h3>
<i class="pi pi-camera"></i>
{{ isEditMode ? 'Re-scanare OCR (optional)' : 'Poza Bon' }}
</h3>
<!-- OCR Upload Zone -->
<OCRUploadZone
ref="ocrUploadZone"
@ocr-result="onOCRResult"
@file-selected="onOCRFileSelected"
@error="onOCRError"
/>
<!-- OCR Upload Zone -->
<OCRUploadZone
ref="ocrUploadZone"
@ocr-result="onOCRResult"
@file-selected="onOCRFileSelected"
@error="onOCRError"
/>
<!-- OCR Preview (when results are available) -->
<OCRPreview
v-if="ocrData"
:data="ocrData"
@apply="applyOCRData"
@dismiss="dismissOCRData"
/>
</div>
<!-- Standard Upload Section (for edit mode or additional files) -->
<div class="upload-section" v-if="isEditMode || selectedFiles.length > 0">
<h3 v-if="isEditMode">
<i class="pi pi-camera"></i>
Poza Bon (obligatoriu)
</h3>
<h3 v-else-if="selectedFiles.length > 0">
<i class="pi pi-paperclip"></i>
Fisiere Selectate
</h3>
<FileUpload
v-if="isEditMode"
ref="fileUpload"
mode="advanced"
:multiple="true"
accept="image/*,application/pdf"
:maxFileSize="10000000"
@select="onFileSelect"
@remove="onFileRemove"
:auto="false"
:showUploadButton="false"
:showCancelButton="false"
>
<template #empty>
<div class="upload-area">
<i class="pi pi-cloud-upload" style="font-size: 3rem; color: #667eea;"></i>
<p>Trage fisierele aici sau click pentru a selecta</p>
<p style="font-size: 0.8rem; color: #888;">
Formate acceptate: JPG, PNG, PDF (max 10MB)
</p>
</div>
</template>
</FileUpload>
<!-- Existing attachments (edit mode) -->
<div v-if="existingAttachments.length" class="image-preview-grid">
<div
v-for="att in existingAttachments"
:key="att.id"
class="image-preview-item"
>
<img
v-if="att.mime_type?.startsWith('image/')"
:src="store.getAttachmentUrl(att.id)"
:alt="att.filename"
/>
<div v-else class="pdf-preview">
<i class="pi pi-file-pdf" style="font-size: 3rem;"></i>
<span>{{ att.filename }}</span>
</div>
<Button
icon="pi pi-times"
severity="danger"
rounded
class="remove-btn"
@click="removeExistingAttachment(att.id)"
/>
</div>
</div>
<!-- Selected files preview (create mode) -->
<div v-if="!isEditMode && selectedFiles.length" class="selected-files-list">
<div
v-for="(file, index) in selectedFiles"
:key="index"
class="selected-file-item"
>
<i :class="file.type.startsWith('image/') ? 'pi pi-image' : 'pi pi-file-pdf'"></i>
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<Button
icon="pi pi-times"
severity="danger"
rounded
size="small"
@click="removeSelectedFile(index)"
/>
</div>
</div>
</div>
<Divider />
<!-- Receipt Details -->
<h3>
<i class="pi pi-info-circle"></i>
Detalii Bon
</h3>
<div class="form-grid">
<div class="form-field">
<label>Tip Document *</label>
<div class="radio-group">
<div class="radio-item">
<RadioButton
v-model="form.receipt_type"
value="bon_fiscal"
inputId="type_bon"
/>
<label for="type_bon">Bon Fiscal</label>
</div>
<div class="radio-item">
<RadioButton
v-model="form.receipt_type"
value="chitanta"
inputId="type_chitanta"
/>
<label for="type_chitanta">Chitanta</label>
</div>
</div>
</div>
<div class="form-field">
<label>Directie *</label>
<div class="radio-group">
<div class="radio-item">
<RadioButton
v-model="form.direction"
value="cheltuiala"
inputId="dir_cheltuiala"
/>
<label for="dir_cheltuiala">Cheltuiala</label>
</div>
<div class="radio-item">
<RadioButton
v-model="form.direction"
value="incasare"
inputId="dir_incasare"
/>
<label for="dir_incasare">Incasare</label>
</div>
</div>
</div>
<div class="form-field">
<label>Data Bon *</label>
<Calendar
v-model="form.receipt_date"
dateFormat="dd.mm.yy"
showIcon
required
/>
</div>
<div class="form-field">
<label>Suma (RON) *</label>
<InputNumber
v-model="form.amount"
mode="currency"
currency="RON"
locale="ro-RO"
:minFractionDigits="2"
:maxFractionDigits="2"
required
/>
</div>
<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>
<Dropdown
v-model="form.expense_type_code"
:options="expenseTypes"
optionLabel="name"
optionValue="code"
placeholder="Selecteaza tip"
required
/>
</div>
<div class="form-field">
<label>Mod Plata</label>
<Dropdown
v-model="form.payment_mode"
:options="paymentModeOptions"
optionLabel="label"
optionValue="value"
placeholder="Selecteaza mod plata"
showClear
/>
<small class="field-hint text-secondary" v-if="!form.payment_mode">
Obligatoriu la trimiterea pentru aprobare
</small>
</div>
<div class="form-field">
<label>Numar Bon</label>
<InputText v-model="form.receipt_number" placeholder="Optional" />
</div>
<div class="form-field form-field-full">
<label>Descriere</label>
<Textarea
v-model="form.description"
rows="3"
placeholder="Descriere optionala..."
/>
</div>
</div>
<!-- Detalii Suplimentare (populated from OCR) -->
<div v-if="form.tva_breakdown?.length > 0 || form.items_count || form.vendor_address || form.payment_methods?.length > 0" class="extra-details-section">
<h3>
<i class="pi pi-list"></i>
Detalii Suplimentare (din OCR)
</h3>
<!-- 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
v-model="form.tva_breakdown[idx].amount"
mode="currency"
currency="RON"
locale="ro-RO"
:minFractionDigits="2"
class="tva-input"
/>
</div>
<div class="tva-row total" v-if="form.tva_breakdown.length > 0">
<span class="tva-label"><strong>Total TVA:</strong></span>
<span class="tva-value">{{ formatTvaTotal() }} RON</span>
</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>
<!-- 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
v-if="ocrData && !ocrCollapsed"
:data="ocrData"
@apply="applyOCRData"
@dismiss="dismissOCRData"
@collapse="ocrCollapsed = true"
/>
</div>
<!-- Standard Upload Section (for edit mode or additional files) -->
<div class="upload-section" v-if="isEditMode || selectedFiles.length > 0">
<h3 v-if="isEditMode">
<i class="pi pi-images"></i>
Atasamente
</h3>
<h3 v-else-if="selectedFiles.length > 0">
<i class="pi pi-paperclip"></i>
Fisiere Selectate
</h3>
<FileUpload
v-if="isEditMode"
ref="fileUpload"
mode="advanced"
:multiple="true"
accept="image/*,application/pdf"
:maxFileSize="10000000"
@select="onFileSelect"
@remove="onFileRemove"
:auto="false"
:showUploadButton="false"
:showCancelButton="false"
>
<template #empty>
<div class="upload-area">
<i class="pi pi-cloud-upload" style="font-size: 2rem; color: #667eea;"></i>
<p>Trage fisierele aici</p>
</div>
</template>
</FileUpload>
<!-- Existing attachments (edit mode) -->
<div v-if="existingAttachments.length" class="image-preview-grid">
<div
v-for="att in existingAttachments"
:key="att.id"
class="image-preview-item"
>
<img
v-if="att.mime_type?.startsWith('image/')"
:src="store.getAttachmentUrl(att.id)"
:alt="att.filename"
/>
<div v-else class="pdf-preview">
<i class="pi pi-file-pdf" style="font-size: 2rem;"></i>
<span>{{ att.filename }}</span>
</div>
<Button
icon="pi pi-times"
severity="danger"
rounded
class="remove-btn"
size="small"
@click="removeExistingAttachment(att.id)"
/>
</div>
</div>
<!-- Selected files preview (create mode) -->
<div v-if="!isEditMode && selectedFiles.length" class="selected-files-list">
<div
v-for="(file, index) in selectedFiles"
:key="index"
class="selected-file-item"
>
<i :class="file.type.startsWith('image/') ? 'pi pi-image' : 'pi pi-file-pdf'"></i>
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<Button
icon="pi pi-times"
severity="danger"
rounded
size="small"
@click="removeSelectedFile(index)"
/>
</div>
</div>
</div>
</div>
<!-- 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>
<!-- SECTION: DOCUMENT -->
<div class="form-section">
<div class="form-section-title">DOCUMENT</div>
<div class="form-section-content">
<div class="form-row">
<div class="form-field">
<div class="radio-group-inline">
<div class="radio-item">
<RadioButton
v-model="form.receipt_type"
value="bon_fiscal"
inputId="type_bon"
/>
<label for="type_bon">Bon</label>
</div>
<div class="radio-item">
<RadioButton
v-model="form.receipt_type"
value="chitanta"
inputId="type_chitanta"
/>
<label for="type_chitanta">Chitanta</label>
</div>
</div>
</div>
<div class="form-field">
<label>Nr.</label>
<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">
<RadioButton
v-model="form.direction"
value="cheltuiala"
inputId="dir_cheltuiala"
/>
<label for="dir_cheltuiala">Cheltuiala</label>
</div>
<div class="radio-item">
<RadioButton
v-model="form.direction"
value="incasare"
inputId="dir_incasare"
/>
<label for="dir_incasare">Incasare</label>
</div>
</div>
</div>
</div>
</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">
<label>Suma *</label>
<InputNumber
v-model="form.amount"
mode="currency"
currency="RON"
locale="ro-RO"
:minFractionDigits="2"
:maxFractionDigits="2"
required
class="amount-input"
/>
</div>
<div class="form-field flex-1">
<label>Tip Cheltuiala *</label>
<Dropdown
v-model="form.expense_type_code"
:options="expenseTypes"
optionLabel="name"
optionValue="code"
placeholder="Selecteaza tip"
required
/>
</div>
</div>
<div class="form-row" v-if="form.items_count">
<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>
<div class="payment-field-wrapper">
<Dropdown
v-model="form.payment_mode"
:options="paymentModeOptions"
optionLabel="label"
optionValue="value"
placeholder="Selecteaza mod plata"
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">
Obligatoriu la trimitere
</small>
</div>
</div>
<!-- Payment methods tags from OCR -->
<div v-if="form.payment_methods?.length > 0" 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)}`"
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>
<!-- End receipt-form-layout -->
<Divider />
@@ -457,12 +491,16 @@ const submitting = ref(false)
const ocrUploadZone = ref(null)
const ocrData = ref(null)
const ocrFile = ref(null)
const ocrCollapsed = ref(false)
// Supplier dialog refs
const showCreateSupplierDialog = ref(false)
const pendingSupplierData = ref(null)
const supplierWarning = ref({ show: false, cui: '', name: '' })
// OCR indicator for payment mode
const paymentSetFromOCR = ref(false)
// AutoComplete support
const filteredPartners = ref([])
const supplierSource = ref(null) // 'local', 'synced', or null
@@ -618,6 +656,7 @@ const applyOCRData = async (data) => {
// Auto-suggest payment_mode if OCR detected CARD
if (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
@@ -695,8 +734,8 @@ const applyOCRData = async (data) => {
}
}
// Clear OCR preview
ocrData.value = null
// NOTE: OCRPreview rămâne vizibil pentru comparație side-by-side
// (NU mai colapsăm automat - utilizatorul poate compara datele)
toast.add({
severity: 'success',
@@ -811,7 +850,8 @@ const formatTvaTotal = () => {
const validateForm = () => {
// 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({
severity: 'warn',
summary: 'Validare',
@@ -953,15 +993,86 @@ const submitForReview = async () => {
</script>
<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 {
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.upload-section h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
margin-bottom: 0.75rem;
margin-top: 0;
font-size: 1rem;
}
.radio-group {
@@ -1031,8 +1142,8 @@ const submitForReview = async () => {
/* Extra details section (TVA, items, address) */
.extra-details-section {
margin-top: 1.5rem;
padding: 1rem;
margin-top: 1rem;
padding: 0.75rem;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
@@ -1042,8 +1153,10 @@ const submitForReview = async () => {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
margin-bottom: 0.75rem;
margin-top: 0;
color: #0284c7;
font-size: 0.95rem;
}
.tva-table {
@@ -1142,4 +1255,163 @@ const submitForReview = async () => {
font-weight: 500;
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>