feat: Implement unified Vue SPA with granular service control

Consolidate Reports and Data Entry apps into a single Vue.js SPA with:

Architecture:
- Module-based structure with lazy-loaded routes (@reports, @data-entry)
- Error boundaries per module to prevent cascade failures
- Dual API proxy in Vite for microservices (reports:8001, data-entry:8003)
- Pinia store factories for shared auth, company, and period stores
- Vite path aliases for clear module boundaries (@shared, @reports, @data-entry)

Service Management:
- Granular service control scripts (backend-reports.sh, backend-data-entry.sh, bot.sh, frontend.sh)
- 87% faster frontend restart: 7s vs 53s full restart
- 38% faster full startup: 33s vs 53s via parallel backend initialization
- Enhanced start-dev.sh with proper service timeouts (OCR: 30s, Vite: 15s, Bot: 10s)
- status.sh for comprehensive health checks

Features:
- Auto-select first company on login with period auto-load
- Hamburger menu with feature toggle support
- JWT token auto-injection via axios interceptors
- Unified header with company/period selectors
- IIS web.config for production deployment with multi-API routing

UX Improvements:
- Vue watchers for reactive company/period loading
- Lazy store initialization with graceful error handling
- Period persistence per user+company in localStorage
- Feature flags for optional modules

Deployment:
- Single IIS site serves unified frontend with API proxy rules
- Maintains separate backend processes for microservices
- Windows line ending fixes (.env CRLF → LF conversion)

Stats: 112 files changed, 38,342 insertions(+), 2,342 deletions(-)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 19:06:23 +02:00
parent fed2e68fa2
commit d507a81b0a
112 changed files with 38382 additions and 2382 deletions

147
src/App.vue Normal file
View File

@@ -0,0 +1,147 @@
<template>
<div id="app">
<!-- Header -->
<AppHeader
v-if="authStore.isAuthenticated"
title="ROA2WEB"
brand-link="/reports/dashboard"
:menu-open="menuOpen"
:companies-store="companyStore"
:period-store="periodStore"
:current-user="authStore.currentUser"
:show-user="false"
@menu-toggle="menuOpen = !menuOpen"
@company-changed="handleCompanyChanged"
@period-changed="handlePeriodChanged"
/>
<!-- Slide Menu -->
<SlideMenu
v-if="authStore.isAuthenticated"
:is-open="menuOpen"
:menu-items="enabledMenuItems"
:current-user="authStore.currentUser"
@close="menuOpen = false"
@logout="handleLogout"
/>
<!-- Main Content -->
<main class="main-content" :class="{ 'with-navbar': authStore.isAuthenticated }">
<router-view />
</main>
<!-- Global Toast Messages -->
<Toast position="top-right" />
<ConfirmDialog />
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import AppHeader from '@shared/components/layout/AppHeader.vue'
import SlideMenu from '@shared/components/layout/SlideMenu.vue'
import Toast from 'primevue/toast'
import ConfirmDialog from 'primevue/confirmdialog'
import { createAuthStore } from '@shared/stores/auth.js'
import { createCompaniesStore } from '@shared/stores/companies.js'
import { createAccountingPeriodStore } from '@shared/stores/accountingPeriod.js'
import { menuSections } from '@/config/menu.js'
import { getEnabledMenuSections } from '@/config/features.js'
import axios from 'axios'
const router = useRouter()
// API service for auth (uses reports API)
const authApi = axios.create({
baseURL: '/api/reports',
headers: { 'Content-Type': 'application/json' }
})
// Add interceptor to inject auth token from localStorage
authApi.interceptors.request.use(config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Store definitions (factories return store definitions)
const useAuthStore = createAuthStore(authApi)
const useCompanyStore = createCompaniesStore(authApi, useAuthStore)
const useAccountingPeriodStore = createAccountingPeriodStore(authApi)
// Store instances (invoke the definitions to get instances)
const authStore = useAuthStore()
const companyStore = useCompanyStore()
const periodStore = useAccountingPeriodStore()
// Menu state
const menuOpen = ref(false)
// Get enabled menu items based on feature flags
const enabledMenuItems = computed(() => {
return getEnabledMenuSections(menuSections)
})
// Watch for company selection changes and auto-load periods
watch(
() => companyStore.selectedCompany,
async (newCompany, oldCompany) => {
// Only load periods if company actually changed and is valid
if (newCompany && newCompany.id_firma && newCompany !== oldCompany) {
console.log('[App] Company changed via watch, loading periods for:', newCompany.id_firma)
await periodStore.loadPeriods(newCompany.id_firma)
console.log('[App] Periods auto-loaded successfully')
}
},
{ immediate: true } // Run immediately with current value
)
// Initialize auth and load companies on mount
onMounted(async () => {
console.log('[App] Mounted - initializing auth...')
await authStore.initializeAuth()
console.log('[App] Auth initialized, isAuthenticated:', authStore.isAuthenticated)
// If authenticated, load companies immediately
if (authStore.isAuthenticated) {
console.log('[App] Loading companies...')
await companyStore.loadCompanies()
console.log('[App] Companies loaded, selectedCompany:', companyStore.selectedCompany)
// Period loading will be triggered by the watcher above
} else {
console.log('[App] Not authenticated, skipping company/period loading')
}
})
// Event handlers
const handleCompanyChanged = async (company) => {
console.log('[App] Company changed:', company)
// Load periods for the selected company
if (company && company.id_firma) {
console.log('[App] Loading periods for company:', company.id_firma)
await periodStore.loadPeriods(company.id_firma)
}
}
const handlePeriodChanged = (period) => {
console.log('[App] Period changed:', period)
}
const handleLogout = async () => {
await authStore.logout()
router.push('/login')
}
</script>
<style>
/* Import shared styles */
@import '@shared/styles/layout/header.css';
@import '@shared/styles/layout/navigation.css';
/* Import global CSS system */
@import './assets/css/main.css';
</style>