feat: Add JWT auth and nomenclature sync to data-entry-app

Integrate shared JWT authentication into data-entry-app:
- Add Oracle pool initialization for auth service
- Add AuthenticationMiddleware to protect API routes
- Update all receipt endpoints to use CurrentUser from JWT
- Add shared auth router (/api/auth/login, /api/auth/refresh)

Add nomenclature synchronization feature:
- Create SQLite models for synced suppliers, local suppliers, and cash registers
- Add nomenclature router with sync triggers and CRUD endpoints
- Add sync service for Oracle → SQLite nomenclature data
- Update nomenclature_service to use synced SQLite data with fallbacks

Create shared frontend components:
- Add shared/frontend/ with LoginView.vue, auth store factory, login.css
- Integrate shared login and auth into data-entry-app frontend
- Add axios-based API service with token refresh interceptor

🤖 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-14 18:36:24 +02:00
parent 682a4b64b9
commit c5fde510a8
37 changed files with 28907 additions and 903 deletions

View File

@@ -328,6 +328,42 @@
</div>
</form>
</div>
<!-- Create Supplier Dialog -->
<Dialog
v-model:visible="showCreateSupplierDialog"
header="Furnizor Negasit"
:modal="true"
:style="{ width: '450px' }"
>
<div class="dialog-content">
<p>
<i class="pi pi-exclamation-triangle" style="color: var(--orange-500);"></i>
Furnizorul cu CUI <strong>{{ pendingSupplierData?.fiscal_code }}</strong> nu a fost gasit in baza de date.
</p>
<p>Doriti sa creati un furnizor local cu datele extrase din bon?</p>
<div class="form-field" style="margin-top: 1rem;">
<label>Nume Furnizor</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="cancelCreateSupplier" />
<Button label="Creaza Furnizor" icon="pi pi-plus" @click="createLocalSupplier" />
</template>
</Dialog>
</div>
</template>
@@ -338,6 +374,7 @@ import { useToast } from 'primevue/usetoast'
import { useReceiptsStore } from '../../stores/receiptsStore'
import OCRUploadZone from '../../components/ocr/OCRUploadZone.vue'
import OCRPreview from '../../components/ocr/OCRPreview.vue'
import Dialog from 'primevue/dialog'
const route = useRoute()
const router = useRouter()
@@ -379,6 +416,10 @@ const ocrUploadZone = ref(null)
const ocrData = ref(null)
const ocrFile = ref(null)
// Supplier dialog refs
const showCreateSupplierDialog = ref(false)
const pendingSupplierData = ref(null)
const partners = computed(() => store.partners)
const expenseTypes = computed(() => store.expenseTypes)
const cashRegisters = computed(() => store.cashRegisters)
@@ -452,42 +493,21 @@ const onOCRError = (message) => {
})
}
const applyOCRData = (data) => {
// Apply OCR data to form
const applyOCRData = async (data) => {
// Apply basic 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.amount) {
form.value.amount = parseFloat(data.amount)
}
if (data.receipt_number) {
form.value.receipt_number = data.receipt_number
}
// Try to find matching partner by name or CUI
if (data.partner_name || data.cui) {
const matchingPartner = partners.value.find(p => {
const nameMatch = data.partner_name &&
p.name.toLowerCase().includes(data.partner_name.toLowerCase())
const cuiMatch = data.cui && p.cui === data.cui
return nameMatch || cuiMatch
})
if (matchingPartner) {
form.value.partner_id = matchingPartner.id
form.value.partner_name = matchingPartner.name
} else if (data.partner_name) {
// Store the extracted name even if no match
form.value.partner_name = data.partner_name
}
}
// Apply TVA entries
if (data.tva_entries?.length > 0) {
form.value.tva_breakdown = data.tva_entries.map(e => ({
@@ -496,14 +516,52 @@ const applyOCRData = (data) => {
amount: parseFloat(e.amount)
}))
}
if (data.tva_total) {
form.value.tva_total = parseFloat(data.tva_total)
}
if (data.items_count) {
form.value.items_count = data.items_count
}
if (data.address) {
form.value.vendor_address = data.address
if (data.tva_total) form.value.tva_total = parseFloat(data.tva_total)
if (data.items_count) form.value.items_count = data.items_count
if (data.address) form.value.vendor_address = data.address
// Auto-search supplier by CUI if available
if (data.cui) {
toast.add({
severity: 'info',
summary: 'Cautare furnizor',
detail: `Se cauta furnizor dupa CUI: ${data.cui}`,
life: 2000,
})
const result = await store.searchSupplier(data.cui)
if (result.found && result.supplier) {
// Found! Auto-select
form.value.partner_id = result.supplier.id
form.value.partner_name = result.supplier.name
toast.add({
severity: 'success',
summary: 'Furnizor gasit',
detail: `${result.supplier.name} (${result.source})`,
life: 3000,
})
} else {
// Not found - offer to create
pendingSupplierData.value = {
name: data.partner_name || '',
fiscal_code: data.cui,
address: data.address || '',
}
showCreateSupplierDialog.value = true
}
} else if (data.partner_name) {
// No CUI but have name - try name search
const matchingPartner = partners.value.find(p =>
p.name.toLowerCase().includes(data.partner_name.toLowerCase())
)
if (matchingPartner) {
form.value.partner_id = matchingPartner.id
form.value.partner_name = matchingPartner.name
} else {
form.value.partner_name = data.partner_name
}
}
// Clear OCR preview
@@ -521,6 +579,40 @@ const dismissOCRData = () => {
ocrData.value = null
}
const createLocalSupplier = async () => {
if (!pendingSupplierData.value) return
try {
const supplier = await store.createLocalSupplier(pendingSupplierData.value)
// Auto-select the new supplier
form.value.partner_id = supplier.id
form.value.partner_name = supplier.name
toast.add({
severity: 'success',
summary: 'Furnizor creat',
detail: `${supplier.name} a fost adaugat`,
life: 3000,
})
} catch (error) {
toast.add({
severity: 'error',
summary: 'Eroare',
detail: error.message,
life: 5000,
})
} finally {
showCreateSupplierDialog.value = false
pendingSupplierData.value = null
}
}
const cancelCreateSupplier = () => {
showCreateSupplierDialog.value = false
pendingSupplierData.value = null
}
const onPartnerChange = (event) => {
const partner = partners.value.find(p => p.id === event.value)
form.value.partner_name = partner?.name || null
@@ -853,4 +945,33 @@ const submitForReview = async () => {
font-weight: 600;
color: #0284c7;
}
/* Dialog content */
.dialog-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.dialog-content p {
margin: 0;
line-height: 1.6;
}
.dialog-content p:first-child {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.dialog-content .form-field {
margin-bottom: 0.5rem;
}
.dialog-content .form-field label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: #334155;
}
</style>