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:
147
src/App.vue
Normal file
147
src/App.vue
Normal 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>
|
||||
Reference in New Issue
Block a user