feat(data-entry): Add unified receipt form with OCR confidence tracking
New unified receipt creation system with: - UnifiedReceiptForm component with inline OCR preview and confidence indicators - Compact upload zone with drag-drop and camera support - TVA and Payment fields with dynamic add/remove - Supplier dual-field with autocomplete and OCR hint - Receipt form sections with collapsible auxiliary data Backend OCR improvements: - Add confidence_tva and confidence_payment to extraction results - Update TVA extraction to return confidence scores - Include TVA (15%) and payment (10%) in overall_confidence calculation Also includes: - CSS design system rules documentation - Port check helper function for service scripts - Expanded design tokens documentation in CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,871 @@
|
||||
<template>
|
||||
<div class="receipt-unified-view">
|
||||
<!-- Header -->
|
||||
<div class="view-header">
|
||||
<div class="header-left">
|
||||
<Button
|
||||
icon="pi pi-arrow-left"
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
@click="goBack"
|
||||
/>
|
||||
<h1 class="view-title">
|
||||
<i :class="modeIcon"></i>
|
||||
{{ modeTitle }}
|
||||
</h1>
|
||||
<Tag
|
||||
v-if="isViewMode && receipt"
|
||||
:value="getStatusLabel(receipt.status)"
|
||||
:severity="getStatusSeverity(receipt.status)"
|
||||
/>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- Create/Edit mode actions -->
|
||||
<template v-if="!isViewMode">
|
||||
<Button
|
||||
icon="pi pi-save"
|
||||
label="Salveaza"
|
||||
:loading="saving"
|
||||
@click="saveReceipt"
|
||||
/>
|
||||
<Button
|
||||
v-if="isEditMode && receipt?.status === 'draft'"
|
||||
icon="pi pi-send"
|
||||
label="Trimite"
|
||||
severity="success"
|
||||
:loading="submitting"
|
||||
@click="submitForReview"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- View mode actions -->
|
||||
<template v-else>
|
||||
<Button
|
||||
v-if="receipt?.status === 'draft' || receipt?.status === 'rejected'"
|
||||
icon="pi pi-pencil"
|
||||
label="Editeaza"
|
||||
@click="goToEdit"
|
||||
/>
|
||||
<Button
|
||||
v-if="receipt?.status === 'draft'"
|
||||
icon="pi pi-send"
|
||||
label="Trimite"
|
||||
severity="success"
|
||||
@click="submitReceipt"
|
||||
:loading="submitting"
|
||||
/>
|
||||
<Button
|
||||
v-if="receipt?.status === 'pending_review'"
|
||||
icon="pi pi-check"
|
||||
label="Valideaza"
|
||||
severity="success"
|
||||
@click="approveReceipt"
|
||||
:loading="approving"
|
||||
/>
|
||||
<Button
|
||||
v-if="receipt?.status === 'pending_review'"
|
||||
icon="pi pi-times"
|
||||
label="Respinge"
|
||||
severity="danger"
|
||||
@click="openRejectDialog"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rejection Alert -->
|
||||
<Message
|
||||
v-if="receipt?.rejection_reason"
|
||||
severity="warn"
|
||||
:closable="false"
|
||||
class="rejection-message"
|
||||
>
|
||||
<strong>Motiv respingere:</strong> {{ receipt.rejection_reason }}
|
||||
<br>
|
||||
<small>Respins de {{ receipt.reviewed_by }} la {{ formatDateTime(receipt.reviewed_at) }}</small>
|
||||
</Message>
|
||||
|
||||
<!-- Unified Form -->
|
||||
<UnifiedReceiptForm
|
||||
ref="unifiedFormRef"
|
||||
v-model="form"
|
||||
:ocr-data="ocrData"
|
||||
:partners="partners"
|
||||
:expense-types="expenseTypes"
|
||||
:supplier-source="supplierSource"
|
||||
:supplier-warning="supplierWarning.show"
|
||||
:syncing-suppliers="syncingSuppliers"
|
||||
:existing-attachments="existingAttachments"
|
||||
:selected-files="selectedFiles"
|
||||
:readonly="isViewMode"
|
||||
@ocr-result="onOCRResult"
|
||||
@file-selected="onFileSelected"
|
||||
@ocr-error="onOCRError"
|
||||
@partner-selected="onPartnerSelected"
|
||||
@sync-suppliers="syncSuppliers"
|
||||
@create-supplier="createLocalSupplierFromWarning"
|
||||
@add-files="onAddFiles"
|
||||
@remove-file="onRemoveFile"
|
||||
@remove-attachment="removeExistingAttachment"
|
||||
@download-attachment="downloadAttachment"
|
||||
/>
|
||||
|
||||
<!-- Validation Banners -->
|
||||
<div class="validation-banners" v-if="!isViewMode && validationErrors.length > 0">
|
||||
<Message
|
||||
v-for="(error, idx) in validationErrors"
|
||||
:key="idx"
|
||||
severity="warn"
|
||||
:closable="false"
|
||||
>
|
||||
{{ error }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<!-- Reject Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="showRejectDialog"
|
||||
header="Respinge Bon"
|
||||
:modal="true"
|
||||
:style="{ width: '450px' }"
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<p>Introduceti motivul respingerii:</p>
|
||||
<Textarea
|
||||
v-model="rejectReason"
|
||||
rows="3"
|
||||
class="w-full"
|
||||
placeholder="Motiv respingere (minim 5 caractere)..."
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button
|
||||
label="Anuleaza"
|
||||
severity="secondary"
|
||||
@click="showRejectDialog = false"
|
||||
/>
|
||||
<Button
|
||||
label="Respinge"
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
@click="rejectReceipt"
|
||||
:loading="rejecting"
|
||||
:disabled="!rejectReason || rejectReason.length < 5"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Create Supplier Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="showCreateSupplierDialog"
|
||||
header="Creaza Furnizor Local"
|
||||
:modal="true"
|
||||
:style="{ width: '400px' }"
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<p>Furnizorul nu a fost gasit. Creati un furnizor local?</p>
|
||||
<div class="form-field">
|
||||
<label>Nume</label>
|
||||
<InputText v-model="pendingSupplierData.name" class="w-full" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>CUI</label>
|
||||
<InputText v-model="pendingSupplierData.fiscal_code" class="w-full" disabled />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>Adresa</label>
|
||||
<InputText v-model="pendingSupplierData.address" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Anuleaza" severity="secondary" @click="showCreateSupplierDialog = false" />
|
||||
<Button label="Creaza" icon="pi pi-plus" @click="createLocalSupplier" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
import Message from 'primevue/message'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
import { useReceiptsStore } from '@data-entry/stores/receiptsStore'
|
||||
import { useCompanyStore } from '@data-entry/stores/sharedStores'
|
||||
|
||||
import UnifiedReceiptForm from '@data-entry/components/receipts/UnifiedReceiptForm.vue'
|
||||
import {
|
||||
getDefaultUnifiedFormState,
|
||||
legacyToUnifiedForm,
|
||||
unifiedFormToApiPayload,
|
||||
apiToUiTva,
|
||||
apiToUiPayments,
|
||||
} from '@data-entry/utils/receiptConversions'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const store = useReceiptsStore()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
// Mode detection
|
||||
const isViewMode = computed(() => !!route.params.id && !route.path.endsWith('/edit'))
|
||||
const isEditMode = computed(() => !!route.params.id && route.path.endsWith('/edit'))
|
||||
const isCreateMode = computed(() => !route.params.id)
|
||||
|
||||
const modeTitle = computed(() => {
|
||||
if (isViewMode.value) return `Bon #${receipt.value?.id || ''}`
|
||||
if (isEditMode.value) return 'Editare Bon'
|
||||
return 'Bon Fiscal Nou'
|
||||
})
|
||||
|
||||
const modeIcon = computed(() => {
|
||||
if (isViewMode.value) return 'pi pi-receipt'
|
||||
return 'pi pi-plus-circle'
|
||||
})
|
||||
|
||||
// Form state
|
||||
const form = ref(getDefaultUnifiedFormState())
|
||||
const receipt = ref(null)
|
||||
const unifiedFormRef = ref(null)
|
||||
|
||||
// OCR state
|
||||
const ocrData = ref(null)
|
||||
|
||||
// Files state
|
||||
const selectedFiles = ref([])
|
||||
const existingAttachments = ref([])
|
||||
|
||||
// Loading states
|
||||
const saving = ref(false)
|
||||
const submitting = ref(false)
|
||||
const approving = ref(false)
|
||||
const rejecting = ref(false)
|
||||
const syncingSuppliers = ref(false)
|
||||
|
||||
// Supplier state
|
||||
const supplierSource = ref(null)
|
||||
const supplierWarning = ref({ show: false, cui: '', name: '' })
|
||||
const showCreateSupplierDialog = ref(false)
|
||||
const pendingSupplierData = ref({ name: '', fiscal_code: '', address: '' })
|
||||
|
||||
// Reject dialog
|
||||
const showRejectDialog = ref(false)
|
||||
const rejectReason = ref('')
|
||||
|
||||
// Computed
|
||||
const partners = computed(() => store.partners)
|
||||
const expenseTypes = computed(() => store.expenseTypes)
|
||||
|
||||
const validationErrors = computed(() => {
|
||||
const errors = []
|
||||
if (!form.value.amount || form.value.amount <= 0) {
|
||||
errors.push('Suma totala este obligatorie')
|
||||
}
|
||||
if (!form.value.receipt_date) {
|
||||
errors.push('Data este obligatorie')
|
||||
}
|
||||
if (selectedFiles.value.length === 0 && existingAttachments.value.length === 0 && isCreateMode.value) {
|
||||
errors.push('Atasati cel putin o imagine a bonului')
|
||||
}
|
||||
return errors
|
||||
})
|
||||
|
||||
// Status helpers
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
draft: 'Ciorna',
|
||||
pending_review: 'In Asteptare',
|
||||
approved: 'Aprobat',
|
||||
rejected: 'Respins',
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
const getStatusSeverity = (status) => {
|
||||
const severities = {
|
||||
draft: 'secondary',
|
||||
pending_review: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
}
|
||||
return severities[status] || 'info'
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
// Load nomenclatures
|
||||
await store.fetchAllNomenclatures()
|
||||
|
||||
// Sync suppliers if empty
|
||||
if (store.partners.length === 0) {
|
||||
await syncSuppliers()
|
||||
}
|
||||
|
||||
// Load receipt if edit/view mode
|
||||
if (isEditMode.value || isViewMode.value) {
|
||||
await loadReceipt()
|
||||
} else {
|
||||
// Set company ID for new receipts
|
||||
form.value.company_id = companyStore.selectedCompanyId || 1
|
||||
}
|
||||
})
|
||||
|
||||
// Load existing receipt
|
||||
const loadReceipt = async () => {
|
||||
try {
|
||||
receipt.value = await store.fetchReceiptById(route.params.id)
|
||||
|
||||
// Convert to unified form format
|
||||
form.value = {
|
||||
receipt_type: receipt.value.receipt_type || 'bon_fiscal',
|
||||
receipt_date: new Date(receipt.value.receipt_date),
|
||||
receipt_number: receipt.value.receipt_number || '',
|
||||
partner_name: receipt.value.partner_name || null,
|
||||
cui: receipt.value.cui || '',
|
||||
vendor_address: receipt.value.vendor_address || '',
|
||||
amount: parseFloat(receipt.value.amount) || null,
|
||||
tva: apiToUiTva(receipt.value.tva_breakdown),
|
||||
payments: apiToUiPayments(receipt.value.payment_methods),
|
||||
expense_type_code: receipt.value.expense_type_code || null,
|
||||
description: receipt.value.description || '',
|
||||
ocr_raw_text: receipt.value.ocr_raw_text || '',
|
||||
items_count: receipt.value.items_count || null,
|
||||
company_id: receipt.value.company_id,
|
||||
}
|
||||
|
||||
existingAttachments.value = receipt.value.attachments || []
|
||||
|
||||
// Set supplier source if CUI matches a known supplier
|
||||
if (form.value.cui) {
|
||||
const result = await store.searchSupplier(form.value.cui)
|
||||
if (result.found) {
|
||||
supplierSource.value = result.source
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: 'Nu s-a putut incarca bonul',
|
||||
life: 5000,
|
||||
})
|
||||
router.push('/data-entry')
|
||||
}
|
||||
}
|
||||
|
||||
// OCR handlers
|
||||
const onOCRResult = async (data) => {
|
||||
ocrData.value = data
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'OCR Procesare',
|
||||
detail: 'Datele au fost extrase',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
// Auto-apply OCR data to form
|
||||
if (data.receipt_type) form.value.receipt_type = data.receipt_type
|
||||
if (data.receipt_date) form.value.receipt_date = new Date(data.receipt_date)
|
||||
if (data.receipt_number) form.value.receipt_number = data.receipt_number
|
||||
if (data.amount) form.value.amount = parseFloat(data.amount)
|
||||
if (data.address) form.value.vendor_address = data.address
|
||||
if (data.raw_text) form.value.ocr_raw_text = data.raw_text
|
||||
if (data.items_count) form.value.items_count = data.items_count
|
||||
|
||||
// Apply TVA
|
||||
if (data.tva_entries?.length > 0) {
|
||||
form.value.tva = apiToUiTva(data.tva_entries)
|
||||
}
|
||||
|
||||
// Apply payments
|
||||
if (data.payment_methods?.length > 0) {
|
||||
form.value.payments = apiToUiPayments(data.payment_methods)
|
||||
}
|
||||
|
||||
// Search for supplier by CUI
|
||||
if (data.cui) {
|
||||
form.value.cui = data.cui
|
||||
|
||||
const result = await store.searchSupplier(data.cui)
|
||||
if (result.found && result.supplier) {
|
||||
form.value.partner_name = result.supplier.name
|
||||
form.value.cui = result.supplier.fiscal_code || data.cui
|
||||
form.value.vendor_address = result.supplier.address || data.address || ''
|
||||
supplierSource.value = result.source
|
||||
supplierWarning.value = { show: false, cui: '', name: '' }
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Furnizor gasit',
|
||||
detail: `${result.supplier.name} (${result.source})`,
|
||||
life: 3000,
|
||||
})
|
||||
} else {
|
||||
// Supplier not found - show warning
|
||||
form.value.partner_name = data.partner_name || ''
|
||||
supplierSource.value = null
|
||||
supplierWarning.value = {
|
||||
show: true,
|
||||
cui: data.cui,
|
||||
name: data.partner_name || ''
|
||||
}
|
||||
}
|
||||
} else if (data.partner_name) {
|
||||
form.value.partner_name = data.partner_name
|
||||
}
|
||||
}
|
||||
|
||||
const onFileSelected = (file) => {
|
||||
// Add OCR file to selected files
|
||||
if (!selectedFiles.value.some(f => f.name === file.name)) {
|
||||
selectedFiles.value = [file, ...selectedFiles.value]
|
||||
}
|
||||
}
|
||||
|
||||
const onOCRError = (message) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare OCR',
|
||||
detail: message,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
|
||||
const onPartnerSelected = (partner) => {
|
||||
if (partner) {
|
||||
supplierSource.value = partner.source || 'local'
|
||||
supplierWarning.value = { show: false, cui: '', name: '' }
|
||||
}
|
||||
}
|
||||
|
||||
// File handlers
|
||||
const onAddFiles = (files) => {
|
||||
selectedFiles.value = [...selectedFiles.value, ...files]
|
||||
}
|
||||
|
||||
const onRemoveFile = (index) => {
|
||||
selectedFiles.value = selectedFiles.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const removeExistingAttachment = async (attachmentId) => {
|
||||
try {
|
||||
await store.deleteAttachment(attachmentId)
|
||||
existingAttachments.value = existingAttachments.value.filter(a => a.id !== attachmentId)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: 'Atasamentul a fost sters',
|
||||
life: 2000,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: 'Nu s-a putut sterge atasamentul',
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const downloadAttachment = async (attachment) => {
|
||||
try {
|
||||
await store.downloadAttachment(attachment.id, attachment.filename)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: 'Nu s-a putut descarca fisierul',
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Supplier handlers
|
||||
const syncSuppliers = async () => {
|
||||
syncingSuppliers.value = true
|
||||
try {
|
||||
await store.syncSuppliers()
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Sincronizare',
|
||||
detail: 'Furnizorii au fost actualizati',
|
||||
life: 2000,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: 'Sincronizare esuata',
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
syncingSuppliers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createLocalSupplierFromWarning = () => {
|
||||
pendingSupplierData.value = {
|
||||
name: supplierWarning.value.name || form.value.partner_name || '',
|
||||
fiscal_code: supplierWarning.value.cui || form.value.cui || '',
|
||||
address: form.value.vendor_address || '',
|
||||
}
|
||||
showCreateSupplierDialog.value = true
|
||||
}
|
||||
|
||||
const createLocalSupplier = async () => {
|
||||
try {
|
||||
await store.createLocalSupplier(pendingSupplierData.value)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: 'Furnizor local creat',
|
||||
life: 2000,
|
||||
})
|
||||
showCreateSupplierDialog.value = false
|
||||
supplierSource.value = 'local'
|
||||
supplierWarning.value = { show: false, cui: '', name: '' }
|
||||
|
||||
// Update form with created supplier
|
||||
form.value.partner_name = pendingSupplierData.value.name
|
||||
form.value.cui = pendingSupplierData.value.fiscal_code
|
||||
form.value.vendor_address = pendingSupplierData.value.address
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: 'Nu s-a putut crea furnizorul',
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Save handlers
|
||||
const validateForm = () => {
|
||||
if (!form.value.receipt_date) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Validare',
|
||||
detail: 'Data este obligatorie',
|
||||
life: 3000,
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (!form.value.amount || form.value.amount <= 0) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Validare',
|
||||
detail: 'Suma este obligatorie',
|
||||
life: 3000,
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const saveReceipt = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
// Auto-create supplier if warning shown
|
||||
if (supplierWarning.value.show && form.value.cui) {
|
||||
try {
|
||||
await store.createLocalSupplier({
|
||||
name: form.value.partner_name || `Furnizor ${form.value.cui}`,
|
||||
fiscal_code: form.value.cui,
|
||||
address: form.value.vendor_address || null,
|
||||
})
|
||||
supplierWarning.value = { show: false, cui: '', name: '' }
|
||||
supplierSource.value = 'local'
|
||||
} catch (e) {
|
||||
console.warn('Auto-create supplier failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert form to API format
|
||||
const payload = unifiedFormToApiPayload(form.value)
|
||||
|
||||
let savedReceipt
|
||||
|
||||
if (isEditMode.value) {
|
||||
savedReceipt = await store.updateReceipt(route.params.id, payload)
|
||||
} else {
|
||||
savedReceipt = await store.createReceipt(payload)
|
||||
}
|
||||
|
||||
// Upload new files
|
||||
for (const file of selectedFiles.value) {
|
||||
try {
|
||||
await store.uploadAttachment(savedReceipt.id, file)
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Atentie',
|
||||
detail: `Nu s-a putut incarca: ${file.name}`,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: isEditMode.value ? 'Bonul a fost actualizat' : 'Bonul a fost creat',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
router.push(`/data-entry/${savedReceipt.id}`)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.message || 'Nu s-a putut salva bonul',
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitForReview = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
await saveReceipt()
|
||||
const result = await store.submitReceipt(route.params.id)
|
||||
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: 'Bonul a fost trimis spre aprobare',
|
||||
life: 3000,
|
||||
})
|
||||
router.push('/data-entry')
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: result.message,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.message || 'Nu s-a putut trimite bonul',
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitReceipt = async () => {
|
||||
submitting.value = true
|
||||
try {
|
||||
const result = await store.submitReceipt(route.params.id)
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: 'Bonul a fost trimis',
|
||||
life: 3000,
|
||||
})
|
||||
await loadReceipt()
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const approveReceipt = async () => {
|
||||
approving.value = true
|
||||
try {
|
||||
const result = await store.approveReceipt(route.params.id)
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: 'Bonul a fost aprobat',
|
||||
life: 3000,
|
||||
})
|
||||
await loadReceipt()
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
approving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openRejectDialog = () => {
|
||||
rejectReason.value = ''
|
||||
showRejectDialog.value = true
|
||||
}
|
||||
|
||||
const rejectReceipt = async () => {
|
||||
rejecting.value = true
|
||||
try {
|
||||
const result = await store.rejectReceipt(route.params.id, rejectReason.value)
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Succes',
|
||||
detail: 'Bonul a fost respins',
|
||||
life: 3000,
|
||||
})
|
||||
showRejectDialog.value = false
|
||||
await loadReceipt()
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
rejecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
const goBack = () => {
|
||||
router.push('/data-entry')
|
||||
}
|
||||
|
||||
const goToEdit = () => {
|
||||
router.push(`/data-entry/${route.params.id}/edit`)
|
||||
}
|
||||
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleString('ro-RO')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.receipt-unified-view {
|
||||
padding: 1rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.view-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.rejection-message {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Validation */
|
||||
.validation-banners {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.receipt-unified-view {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
[data-theme="dark"] .view-header {
|
||||
background: var(--surface-card);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user