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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user