feat: Add shared components, refactor stores, improve data-entry workflow
Shared Components: - Add CompanySelector.vue and PeriodSelector.vue components - Add AppHeader.vue and SlideMenu.vue layout components - Add shared stores factories (companies.js, accountingPeriod.js) - Add shared routes factories (companies.py, calendar.py) - Add shared models (company.py, calendar.py) - Add shared layout styles (header.css, navigation.css) Data Entry App: - Update CLAUDE.md with prod/test server documentation - Improve nomenclature sync service with better error handling - Update receipts router and CRUD operations - Add company/period stores using shared factories - Update App.vue layout with shared components - Fix OCRUploadZone file handling Reports App: - Refactor stores to use shared factories - Update App.vue to use shared layout components Infrastructure: - Replace start-data-entry.sh with separate dev/test scripts - Add .claude/rules for authentication, backend patterns, etc. - Add implementation plan for OCR receipt improvements - Clean up old documentation files 🤖 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,38 +1,33 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<header v-if="authStore.isAuthenticated" class="app-header">
|
||||
<div class="header-content">
|
||||
<h1 class="app-title">
|
||||
<i class="pi pi-receipt"></i>
|
||||
Data Entry - Bonuri Fiscale
|
||||
</h1>
|
||||
<nav class="app-nav">
|
||||
<router-link to="/" class="nav-link">
|
||||
<i class="pi pi-list"></i> Lista Bonuri
|
||||
</router-link>
|
||||
<router-link to="/create" class="nav-link">
|
||||
<i class="pi pi-plus"></i> Bon Nou
|
||||
</router-link>
|
||||
<router-link to="/approval" class="nav-link">
|
||||
<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>
|
||||
<AppHeader
|
||||
v-if="authStore.isAuthenticated"
|
||||
title="Data Entry"
|
||||
brand-link="/"
|
||||
header-class="header-container--gradient"
|
||||
:menu-open="menuOpen"
|
||||
:companies-store="companyStore"
|
||||
:period-store="periodStore"
|
||||
:current-user="authStore.currentUser"
|
||||
:show-user="false"
|
||||
@menu-toggle="menuOpen = !menuOpen"
|
||||
@company-changed="onCompanyChanged"
|
||||
@period-changed="onPeriodChanged"
|
||||
>
|
||||
<template #brand>
|
||||
<i class="pi pi-receipt"></i>
|
||||
<span>Data Entry</span>
|
||||
</template>
|
||||
</AppHeader>
|
||||
|
||||
<SlideMenu
|
||||
v-if="authStore.isAuthenticated"
|
||||
:is-open="menuOpen"
|
||||
:menu-items="dataEntryMenuItems"
|
||||
:current-user="authStore.currentUser"
|
||||
@close="menuOpen = false"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
|
||||
<main class="app-main">
|
||||
<router-view />
|
||||
@@ -44,23 +39,74 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { useCompanyStore } from './stores/companies'
|
||||
import { useAccountingPeriodStore } from './stores/accountingPeriod'
|
||||
import { useReceiptsStore } from './stores/receiptsStore'
|
||||
import apiService from './services/api'
|
||||
import AppHeader from '../../../shared/frontend/components/layout/AppHeader.vue'
|
||||
import SlideMenu from '../../../shared/frontend/components/layout/SlideMenu.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const periodStore = useAccountingPeriodStore()
|
||||
const receiptsStore = useReceiptsStore()
|
||||
const menuOpen = ref(false)
|
||||
const pendingCount = ref(0)
|
||||
|
||||
// Menu items configuration
|
||||
const dataEntryMenuItems = computed(() => [
|
||||
{
|
||||
title: 'Navigare',
|
||||
items: [
|
||||
{ to: '/', icon: 'pi pi-list', label: 'Lista Bonuri' },
|
||||
{ to: '/create', icon: 'pi pi-plus', label: 'Bon Nou' },
|
||||
{
|
||||
to: '/approval',
|
||||
icon: 'pi pi-check-circle',
|
||||
label: 'Aprobare',
|
||||
badge: pendingCount.value > 0 ? pendingCount.value : null
|
||||
},
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
companyStore.reset()
|
||||
periodStore.reset()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (authStore.isAuthenticated) {
|
||||
const onCompanyChanged = async (company) => {
|
||||
console.log('[App] Company changed:', company?.name)
|
||||
|
||||
// Trigger nomenclature sync for the selected company (non-blocking)
|
||||
if (company?.id_firma) {
|
||||
apiService.post('/nomenclature/sync/all', null, {
|
||||
headers: { 'X-Selected-Company': company.id_firma }
|
||||
}).then(() => {
|
||||
console.log('[App] Nomenclature sync completed for company:', company.name)
|
||||
}).catch(e => {
|
||||
console.warn('[App] Nomenclature sync failed:', e.message || e)
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh stats when company changes
|
||||
await refreshStats()
|
||||
}
|
||||
|
||||
const onPeriodChanged = (period) => {
|
||||
console.log('[App] Period changed:', period?.display_name)
|
||||
// Refresh data when period changes
|
||||
refreshStats()
|
||||
}
|
||||
|
||||
const refreshStats = async () => {
|
||||
if (authStore.isAuthenticated && companyStore.selectedCompany) {
|
||||
try {
|
||||
const stats = await receiptsStore.fetchStats()
|
||||
pendingCount.value = stats?.pending_review?.count || 0
|
||||
@@ -68,7 +114,38 @@ onMounted(async () => {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (authStore.isAuthenticated) {
|
||||
// Load companies first
|
||||
await companyStore.loadCompanies()
|
||||
|
||||
// If company is selected, trigger initial sync and load stats
|
||||
if (companyStore.selectedCompany) {
|
||||
// Sync nomenclatures for current company (background, non-blocking)
|
||||
apiService.post('/nomenclature/sync/all', null, {
|
||||
headers: { 'X-Selected-Company': companyStore.selectedCompany.id_firma }
|
||||
}).then(() => {
|
||||
console.log('[App] Initial nomenclature sync completed')
|
||||
}).catch(e => {
|
||||
console.warn('[App] Initial nomenclature sync skipped:', e.message || e)
|
||||
})
|
||||
|
||||
await refreshStats()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for company selection to refresh stats
|
||||
watch(
|
||||
() => companyStore.selectedCompany,
|
||||
async (newCompany) => {
|
||||
if (newCompany) {
|
||||
await refreshStats()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -78,85 +155,6 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: background-color 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-link.router-link-active {
|
||||
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;
|
||||
@@ -164,16 +162,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-nav {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
/* Global styles for Data Entry App */
|
||||
|
||||
/* Import shared layout styles */
|
||||
@import '../../../../../shared/frontend/styles/layout/header.css';
|
||||
@import '../../../../../shared/frontend/styles/layout/navigation.css';
|
||||
|
||||
:root {
|
||||
/* Layout variables */
|
||||
--header-height: 60px;
|
||||
--sidebar-width: 280px;
|
||||
--z-header: 100;
|
||||
--z-modal: 1000;
|
||||
--z-modal-backdrop: 999;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
|
||||
/* Typography */
|
||||
--font-semibold: 600;
|
||||
--font-medium: 500;
|
||||
--text-xs: 12px;
|
||||
--text-sm: 14px;
|
||||
--text-base: 16px;
|
||||
--text-lg: 18px;
|
||||
|
||||
/* Radius */
|
||||
--radius-md: 6px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 12px;
|
||||
--space-lg: 24px;
|
||||
|
||||
/* Colors - Primary palette (matching reports-app) */
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import api from '@/services/api'
|
||||
|
||||
const emit = defineEmits(['ocr-result', 'file-selected', 'error'])
|
||||
|
||||
@@ -137,7 +137,7 @@ const processOCR = async () => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', selectedFile.value)
|
||||
|
||||
const response = await axios.post('/api/ocr/extract', formData, {
|
||||
const response = await api.post('/ocr/extract', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000, // 60 second timeout for OCR
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ import Badge from 'primevue/badge'
|
||||
import Toolbar from 'primevue/toolbar'
|
||||
import Divider from 'primevue/divider'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import Message from 'primevue/message'
|
||||
|
||||
// PrimeVue styles
|
||||
import 'primevue/resources/themes/lara-light-blue/theme.css'
|
||||
@@ -80,6 +81,7 @@ app.component('ProgressSpinner', ProgressSpinner)
|
||||
app.component('Badge', Badge)
|
||||
app.component('Toolbar', Toolbar)
|
||||
app.component('Divider', Divider)
|
||||
app.component('Message', Message)
|
||||
|
||||
// Register PrimeVue directives
|
||||
app.directive('tooltip', Tooltip)
|
||||
|
||||
@@ -9,13 +9,31 @@ const apiService = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
// Request interceptor to add auth token and selected company
|
||||
apiService.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Add X-Selected-Company header from localStorage
|
||||
// The company store saves the selected company per user
|
||||
const user = JSON.parse(localStorage.getItem("user") || "null");
|
||||
if (user?.username) {
|
||||
const savedCompany = localStorage.getItem(`selected_company_${user.username}`);
|
||||
if (savedCompany) {
|
||||
try {
|
||||
const company = JSON.parse(savedCompany);
|
||||
if (company?.id_firma) {
|
||||
config.headers["X-Selected-Company"] = company.id_firma;
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
|
||||
17
data-entry-app/frontend/src/stores/accountingPeriod.js
Normal file
17
data-entry-app/frontend/src/stores/accountingPeriod.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Accounting Period Store for Data Entry App
|
||||
*
|
||||
* Uses the shared accounting period store factory from shared/frontend/stores/accountingPeriod.js
|
||||
* Configured with the data-entry API service (port 8003)
|
||||
*/
|
||||
|
||||
import { createAccountingPeriodStore } from "../../../../shared/frontend/stores/accountingPeriod";
|
||||
import { apiService } from "../services/api";
|
||||
import { useAuthStore } from "./auth";
|
||||
import { useCompanyStore } from "./companies";
|
||||
|
||||
export const useAccountingPeriodStore = createAccountingPeriodStore(
|
||||
apiService,
|
||||
useAuthStore,
|
||||
useCompanyStore
|
||||
);
|
||||
12
data-entry-app/frontend/src/stores/companies.js
Normal file
12
data-entry-app/frontend/src/stores/companies.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Companies Store for Data Entry App
|
||||
*
|
||||
* Uses the shared companies store factory from shared/frontend/stores/companies.js
|
||||
* Configured with the data-entry API service (port 8003)
|
||||
*/
|
||||
|
||||
import { createCompaniesStore } from "../../../../shared/frontend/stores/companies";
|
||||
import { apiService } from "../services/api";
|
||||
import { useAuthStore } from "./auth";
|
||||
|
||||
export const useCompanyStore = createCompaniesStore(apiService, useAuthStore);
|
||||
@@ -372,6 +372,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useReceiptsStore } from '../../stores/receiptsStore'
|
||||
import { useCompanyStore } from '../../stores/companies'
|
||||
import OCRUploadZone from '../../components/ocr/OCRUploadZone.vue'
|
||||
import OCRPreview from '../../components/ocr/OCRPreview.vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
@@ -380,11 +381,17 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const store = useReceiptsStore()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const isEditMode = computed(() => !!route.params.id)
|
||||
const receiptId = computed(() => route.params.id)
|
||||
const receipt = ref(null)
|
||||
|
||||
// Get selected company ID from store
|
||||
const getSelectedCompanyId = () => {
|
||||
return companyStore.selectedCompanyId || 1
|
||||
}
|
||||
|
||||
const form = ref({
|
||||
receipt_type: 'bon_fiscal',
|
||||
direction: 'cheltuiala',
|
||||
@@ -398,7 +405,7 @@ const form = ref({
|
||||
cash_register_account: null,
|
||||
receipt_number: '',
|
||||
description: '',
|
||||
company_id: 1, // Default company for Phase 1
|
||||
company_id: getSelectedCompanyId(),
|
||||
// TVA info (multiple entries support)
|
||||
tva_breakdown: [], // Array of {code, percent, amount}
|
||||
tva_total: null,
|
||||
@@ -429,6 +436,9 @@ onMounted(async () => {
|
||||
|
||||
if (isEditMode.value) {
|
||||
await loadReceipt()
|
||||
} else {
|
||||
// For new receipts, ensure company_id is set from the current selected company
|
||||
form.value.company_id = companyStore.selectedCompanyId || 1
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ export default defineConfig({
|
||||
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