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:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<header v-if="authStore.isAuthenticated" class="app-header">
|
||||
<div class="header-content">
|
||||
<h1 class="app-title">
|
||||
<i class="pi pi-receipt"></i>
|
||||
@@ -17,6 +17,19 @@
|
||||
<i class="pi pi-check-circle"></i> Aprobare
|
||||
<Badge v-if="pendingCount > 0" :value="pendingCount" severity="danger" />
|
||||
</router-link>
|
||||
<div class="user-menu">
|
||||
<span class="user-name">
|
||||
<i class="pi pi-user"></i>
|
||||
{{ authStore.currentUser?.username || 'User' }}
|
||||
</span>
|
||||
<Button
|
||||
icon="pi pi-sign-out"
|
||||
label="Ieșire"
|
||||
class="logout-button"
|
||||
@click="handleLogout"
|
||||
text
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -32,17 +45,28 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { useReceiptsStore } from './stores/receiptsStore'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const receiptsStore = useReceiptsStore()
|
||||
const pendingCount = ref(0)
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const stats = await receiptsStore.fetchStats()
|
||||
pendingCount.value = stats?.pending_review?.count || 0
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
if (authStore.isAuthenticated) {
|
||||
try {
|
||||
const stats = await receiptsStore.fetchStats()
|
||||
pendingCount.value = stats?.pending_review?.count || 0
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -83,6 +107,7 @@ onMounted(async () => {
|
||||
.app-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@@ -105,6 +130,33 @@ onMounted(async () => {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
color: white !important;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
/* Global styles for Data Entry App */
|
||||
|
||||
:root {
|
||||
/* Colors - Primary palette (matching reports-app) */
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
--color-primary-light: #3b82f6;
|
||||
|
||||
/* Compatibility aliases */
|
||||
--primary-color: var(--color-primary);
|
||||
--text-color: #111827;
|
||||
--text-color-secondary: #6b7280;
|
||||
|
||||
/* Surface colors for PrimeVue */
|
||||
--surface-0: #ffffff;
|
||||
--surface-50: #f8fafc;
|
||||
--surface-100: #f1f5f9;
|
||||
--surface-200: #e2e8f0;
|
||||
|
||||
/* Red color palette for errors */
|
||||
--red-50: #fef2f2;
|
||||
--red-200: #fecaca;
|
||||
--red-800: #991b1b;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import router from './router'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Password from 'primevue/password'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import Calendar from 'primevue/calendar'
|
||||
import Textarea from 'primevue/textarea'
|
||||
@@ -31,6 +32,7 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Badge from 'primevue/badge'
|
||||
import Toolbar from 'primevue/toolbar'
|
||||
import Divider from 'primevue/divider'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
|
||||
// PrimeVue styles
|
||||
import 'primevue/resources/themes/lara-light-blue/theme.css'
|
||||
@@ -57,6 +59,7 @@ app.use(ConfirmationService)
|
||||
app.component('Button', Button)
|
||||
app.component('InputText', InputText)
|
||||
app.component('InputNumber', InputNumber)
|
||||
app.component('Password', Password)
|
||||
app.component('Dropdown', Dropdown)
|
||||
app.component('Calendar', Calendar)
|
||||
app.component('Textarea', Textarea)
|
||||
@@ -78,4 +81,7 @@ app.component('Badge', Badge)
|
||||
app.component('Toolbar', Toolbar)
|
||||
app.component('Divider', Divider)
|
||||
|
||||
// Register PrimeVue directives
|
||||
app.directive('tooltip', Tooltip)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/LoginView.vue'),
|
||||
meta: { title: 'Conectare', requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'ReceiptsList',
|
||||
component: () => import('../views/receipts/ReceiptsListView.vue'),
|
||||
meta: { title: 'Lista Bonuri' }
|
||||
meta: { title: 'Lista Bonuri', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/create',
|
||||
name: 'ReceiptCreate',
|
||||
component: () => import('../views/receipts/ReceiptCreateView.vue'),
|
||||
meta: { title: 'Bon Nou' }
|
||||
meta: { title: 'Bon Nou', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/receipt/:id',
|
||||
name: 'ReceiptDetail',
|
||||
component: () => import('../views/receipts/ReceiptDetailView.vue'),
|
||||
meta: { title: 'Detalii Bon' }
|
||||
meta: { title: 'Detalii Bon', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/receipt/:id/edit',
|
||||
name: 'ReceiptEdit',
|
||||
component: () => import('../views/receipts/ReceiptCreateView.vue'),
|
||||
meta: { title: 'Editare Bon' }
|
||||
meta: { title: 'Editare Bon', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/approval',
|
||||
name: 'ReceiptApproval',
|
||||
component: () => import('../views/receipts/ReceiptApprovalView.vue'),
|
||||
meta: { title: 'Aprobare Bonuri' }
|
||||
meta: { title: 'Aprobare Bonuri', requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -38,12 +45,26 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
// Update page title
|
||||
// Authentication guard and page title
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Update page title
|
||||
document.title = to.meta.title
|
||||
? `${to.meta.title} | Data Entry`
|
||||
: 'Data Entry - Bonuri Fiscale'
|
||||
next()
|
||||
|
||||
// Check authentication
|
||||
const authStore = useAuthStore()
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
|
||||
if (requiresAuth && !authStore.isAuthenticated) {
|
||||
// Redirect to login if not authenticated
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
} else if (to.name === 'Login' && authStore.isAuthenticated) {
|
||||
// Redirect to home if already authenticated
|
||||
next({ name: 'ReceiptsList' })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
82
data-entry-app/frontend/src/services/api.js
Normal file
82
data-entry-app/frontend/src/services/api.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import axios from "axios";
|
||||
|
||||
// Create axios instance with base configuration
|
||||
const apiService = axios.create({
|
||||
baseURL: import.meta.env.BASE_URL + "api",
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
apiService.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// Response interceptor for handling errors and token refresh
|
||||
apiService.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Handle 401 Unauthorized errors
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem("refresh_token");
|
||||
if (refreshToken) {
|
||||
const response = await axios.post(
|
||||
import.meta.env.BASE_URL + "api/auth/refresh",
|
||||
{ refresh_token: refreshToken },
|
||||
);
|
||||
|
||||
const { access_token } = response.data;
|
||||
localStorage.setItem("access_token", access_token);
|
||||
|
||||
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
||||
originalRequest.headers["Authorization"] = `Bearer ${access_token}`;
|
||||
|
||||
return apiService(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user");
|
||||
|
||||
const loginPath = import.meta.env.BASE_URL + "login";
|
||||
if (window.location.pathname !== loginPath) {
|
||||
window.location.href = loginPath;
|
||||
}
|
||||
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response) {
|
||||
const message = error.response.data?.detail || error.response.data?.message || `Server error: ${error.response.status}`;
|
||||
console.error("API Error:", { status: error.response.status, message, url: error.config.url });
|
||||
} else if (error.request) {
|
||||
console.error("Network Error:", error.message);
|
||||
} else {
|
||||
console.error("Request Error:", error.message);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export { apiService };
|
||||
export default apiService;
|
||||
11
data-entry-app/frontend/src/stores/auth.js
Normal file
11
data-entry-app/frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Auth Store for Data Entry App
|
||||
*
|
||||
* Uses the shared auth store factory from shared/frontend/stores/auth.js
|
||||
* Configured with the data-entry API service (port 8003)
|
||||
*/
|
||||
|
||||
import { createAuthStore } from "../../../../shared/frontend/stores/auth";
|
||||
import { apiService } from "../services/api";
|
||||
|
||||
export const useAuthStore = createAuthStore(apiService);
|
||||
@@ -1,12 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import { apiService } from '../services/api'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/receipts',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
// Create receipts-specific API wrapper
|
||||
const api = {
|
||||
get: (url, config) => apiService.get(`/receipts${url}`, config),
|
||||
post: (url, data, config) => apiService.post(`/receipts${url}`, data, config),
|
||||
put: (url, data, config) => apiService.put(`/receipts${url}`, data, config),
|
||||
delete: (url, config) => apiService.delete(`/receipts${url}`, config),
|
||||
}
|
||||
|
||||
export const useReceiptsStore = defineStore('receipts', {
|
||||
state: () => ({
|
||||
@@ -324,6 +325,33 @@ export const useReceiptsStore = defineStore('receipts', {
|
||||
])
|
||||
},
|
||||
|
||||
async searchSupplier(fiscalCode) {
|
||||
try {
|
||||
const response = await apiService.get('/nomenclature/suppliers/search', {
|
||||
params: { fiscal_code: fiscalCode },
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Supplier search failed:', error)
|
||||
return { found: false, source: 'error' }
|
||||
}
|
||||
},
|
||||
|
||||
async createLocalSupplier(data) {
|
||||
try {
|
||||
const response = await apiService.post('/nomenclature/suppliers/local', data)
|
||||
// Add to local partners list
|
||||
this.partners.push({
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
code: response.data.fiscal_code,
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.detail || 'Failed to create supplier')
|
||||
}
|
||||
},
|
||||
|
||||
// ============ Stats ============
|
||||
|
||||
async fetchStats() {
|
||||
|
||||
16
data-entry-app/frontend/src/views/LoginView.vue
Normal file
16
data-entry-app/frontend/src/views/LoginView.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<SharedLoginView
|
||||
app-title="Data Entry"
|
||||
app-subtitle="Introducere Bonuri Fiscale"
|
||||
app-icon="pi-file-edit"
|
||||
redirect-path="/"
|
||||
:auth-store="authStore"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SharedLoginView from "@shared/frontend/components/LoginView.vue";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
@@ -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>
|
||||
|
||||
@@ -6,12 +6,17 @@ export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('../../shared', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3010,
|
||||
proxy: {
|
||||
'/api/auth': {
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:8003',
|
||||
changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user