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

View File

@@ -0,0 +1,943 @@
<template>
<div class="app-container">
<div class="register-view">
<!-- Header -->
<div class="page-header">
<h1 class="page-title">
<i class="pi pi-wallet"></i>
Registru Casă / Bancă
</h1>
</div>
<!-- Company Selection (when no company selected) -->
<Card v-if="!companyStore.selectedCompany" class="company-selection-card">
<template #content>
<div class="company-selection">
<p class="text-color-secondary mb-3">
Selectați o companie pentru a vizualiza registrul de casă și
bancă:
</p>
<Dropdown
v-model="selectedCompanyId"
:options="companyStore.companyListFormatted"
option-label="displayName"
option-value="id_firma"
placeholder="Alegeți compania"
class="w-full"
@change="handleCompanyChange"
/>
</div>
</template>
</Card>
<!-- Mobile: Two-row toolbar -->
<div v-if="isMobile && companyStore.selectedCompany" class="mobile-toolbar-container">
<!-- Row 1: Icon-only action buttons -->
<div class="mobile-toolbar-buttons">
<Button
icon="pi pi-filter"
:class="{ 'filter-active': hasActiveFilters }"
class="p-button-text"
@click="showFilters = !showFilters"
v-tooltip.bottom="'Filtre'"
/>
<Button
icon="pi pi-filter-slash"
class="p-button-text"
@click="resetFilters"
v-tooltip.bottom="'Resetează'"
/>
<Button
icon="pi pi-file-excel"
class="p-button-text p-button-success"
@click="exportExcel"
:disabled="!hasData"
v-tooltip.bottom="'Excel'"
/>
<Button
icon="pi pi-file-pdf"
class="p-button-text p-button-danger"
@click="exportPDF"
:disabled="!hasData"
v-tooltip.bottom="'PDF'"
/>
<Button
icon="pi pi-refresh"
class="p-button-text"
:loading="treasuryStore.isLoading"
@click="refreshData"
v-tooltip.bottom="'Actualizează'"
/>
</div>
<!-- Row 2: Totals grid -->
<div class="mobile-toolbar-totals">
<div class="mobile-totals-grid">
<div class="total-item">
<span class="total-label">Sold Prec:</span>
<span class="total-value">{{ formatCompact(treasuryStore.totals.sold_precedent_all) }}</span>
</div>
<div class="total-item">
<span class="total-label">Încasări:</span>
<span class="total-value incasari">{{ formatCompact(treasuryStore.totals.total_incasari_all) }}</span>
</div>
<div class="total-item">
<span class="total-label">Plăți:</span>
<span class="total-value plati">{{ formatCompact(treasuryStore.totals.total_plati_all) }}</span>
</div>
<div class="total-item">
<span class="total-label">Sold Final:</span>
<span class="total-value">{{ formatCompact(treasuryStore.totals.sold_final_all) }}</span>
</div>
</div>
</div>
</div>
<!-- Filters -->
<Card v-if="companyStore.selectedCompany && (!isMobile || showFilters)" class="filters-card">
<template #content>
<div class="form">
<div class="form-row">
<div class="form-col">
<div class="form-group">
<label class="form-label">Tip Registru</label>
<Dropdown
v-model="filters.registerType"
:options="registerTypeOptions"
option-label="label"
option-value="value"
placeholder="Selectați tipul"
class="w-full"
@change="handleFilterChange"
/>
</div>
</div>
<div class="form-col">
<div class="form-group">
<label class="form-label">{{ contColumnHeader }}</label>
<Dropdown
v-model="filters.bankAccount"
:options="bankAccountOptions"
:placeholder="`Toate ${isBancaType ? 'băncile' : 'casele'}`"
:showClear="true"
class="w-full"
@change="handleFilterChange"
:disabled="
!filters.registerType || bankAccountOptions.length === 0
"
/>
</div>
</div>
<div class="form-col">
<div class="form-group">
<label class="form-label">Căutare Partener</label>
<InputText
v-model="filters.partnerName"
placeholder="Nume partener..."
class="w-full"
@input="handleSearchChange"
/>
</div>
</div>
</div>
<!-- Desktop: Action buttons -->
<div v-if="!isMobile" class="form-actions">
<Button
icon="pi pi-filter-slash"
label="Resetează Filtre"
class="p-button-outlined p-button-secondary"
@click="resetFilters"
/>
<Button
icon="pi pi-file-excel"
label="Export Excel"
class="p-button-outlined p-button-success"
@click="exportExcel"
:disabled="!hasData"
/>
<Button
icon="pi pi-file-pdf"
label="Export PDF"
class="p-button-outlined p-button-danger"
@click="exportPDF"
:disabled="!hasData"
/>
<Button
icon="pi pi-refresh"
label="Actualizează"
:loading="treasuryStore.isLoading"
@click="refreshData"
/>
</div>
</div>
</template>
</Card>
<!-- Summary Stats - Compact, right aligned (hidden on mobile - only Sold Final in toolbar) -->
<!-- Folosește totaluri din TOATE înregistrările (backend) nu doar pagina curentă -->
<div v-if="!isMobile && companyStore.selectedCompany" class="summary-stats-inline">
<div class="stat-item">
<span class="stat-label">Sold Precedent:</span>
<span
class="stat-value"
:class="treasuryStore.totals.sold_precedent_all >= 0 ? 'incasari' : 'plati'"
>{{ formatCurrency(treasuryStore.totals.sold_precedent_all) }}</span
>
</div>
<div class="stat-item">
<span class="stat-label">Încasări:</span>
<span class="stat-value incasari">{{
formatCurrency(treasuryStore.totals.total_incasari_all)
}}</span>
</div>
<div class="stat-item">
<span class="stat-label">Plăți:</span>
<span class="stat-value plati">{{
formatCurrency(treasuryStore.totals.total_plati_all)
}}</span>
</div>
<div class="stat-item">
<span class="stat-label">Sold Final:</span>
<span
class="stat-value"
:class="treasuryStore.totals.sold_final_all >= 0 ? 'incasari' : 'plati'"
>{{ formatCurrency(treasuryStore.totals.sold_final_all) }}</span
>
</div>
</div>
<!-- Data Table -->
<Card v-if="companyStore.selectedCompany" class="data-card">
<template #content>
<!-- Mobile: Card Layout -->
<div v-if="isMobile" class="mobile-card-list">
<div
v-for="reg in treasuryStore.registers"
:key="`${reg.dataact}-${reg.nract}`"
class="mobile-data-card"
>
<div class="card-header">{{ reg.nume || 'Fără partener' }}</div>
<div class="card-row">
<span class="card-meta">{{ formatDateShort(reg.dataact) }} · {{ reg.nume_cont_bancar }}</span>
<span
class="card-amount"
:class="reg.incasari > 0 ? 'positive' : (reg.plati > 0 ? 'negative' : '')"
>
<template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template>
<template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template>
<template v-else>{{ formatNumber(0) }}</template>
</span>
</div>
</div>
<div v-if="treasuryStore.registers.length === 0" class="mobile-empty">
<i class="pi pi-info-circle"></i>
<p>Nu au fost găsite înregistrări</p>
</div>
</div>
<!-- Desktop: DataTable -->
<DataTable
v-if="!isMobile"
:value="treasuryStore.registers"
:loading="treasuryStore.isLoading"
:paginator="true"
:rows="pagination.rows"
:total-records="treasuryStore.pagination.totalRecords"
:lazy="true"
:striped-rows="true"
paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rows-per-page-options="[25, 50, 100]"
current-page-report-template="Afișare {first} - {last} din {totalRecords} înregistrări"
responsive-layout="scroll"
@page="onPage"
class="p-datatable-sm"
:rowClass="getRowClass"
>
<template #empty>
<div class="table-empty">
<i class="pi pi-info-circle table-empty-icon"></i>
<p class="table-empty-message">
Nu au fost găsite înregistrări
</p>
</div>
</template>
<template #loading>
<div class="loading-state">
<ProgressSpinner />
<p>Se încarcă registrul...</p>
</div>
</template>
<Column field="dataact" header="Data" sortable class="col-data">
<template #body="slotProps">
{{ formatDate(slotProps.data.dataact) }}
</template>
</Column>
<Column field="nract" header="Nr." sortable class="col-nr" />
<Column
field="nume_cont_bancar"
:header="contColumnHeader"
sortable
class="col-cont"
/>
<Column
field="nume"
header="Partener"
sortable
class="col-partener"
/>
<Column
v-if="isValutaType"
field="valuta"
header="Valuta"
sortable
class="col-valuta"
/>
<Column
field="incasari"
header="Încasări"
sortable
class="col-numeric"
>
<template #body="slotProps">
<span class="numeric-value" v-if="slotProps.data.incasari > 0">
{{ formatNumber(slotProps.data.incasari) }}
</span>
<span class="numeric-value zero" v-else>0,00</span>
</template>
</Column>
<Column field="plati" header="Plăți" sortable class="col-numeric">
<template #body="slotProps">
<span class="numeric-value" v-if="slotProps.data.plati > 0">
{{ formatNumber(slotProps.data.plati) }}
</span>
<span class="numeric-value zero" v-else>0,00</span>
</template>
</Column>
<Column
field="sold"
header="Sold Cumulat"
sortable
class="col-numeric col-sold"
>
<template #body="slotProps">
<span
class="numeric-value"
:class="{ negative: slotProps.data.sold < 0 }"
>
{{ formatNumber(slotProps.data.sold) }}
</span>
</template>
</Column>
<Column
field="explicatia"
header="Explicație"
class="col-explicatie"
>
<template #body="slotProps">
{{ truncateText(slotProps.data.explicatia, 100) }}
</template>
</Column>
</DataTable>
</template>
</Card>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useToast } from "primevue/usetoast";
import { useTreasuryStore } from "@reports/stores/treasury";
import { useCompanyStore } from "@reports/stores/sharedStores";
import { useAccountingPeriodStore } from "@reports/stores/sharedStores";
import { format } from "date-fns";
import { exportToExcel, exportBankCashRegisterPDF } from "@reports/utils/exportUtils";
const toast = useToast();
const treasuryStore = useTreasuryStore();
const companyStore = useCompanyStore();
const periodStore = useAccountingPeriodStore();
// State for company selection
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
// Mobile state
const isMobile = ref(window.innerWidth < 768);
const showFilters = ref(false);
const actionsMenu = ref(null);
// Handle window resize
const handleResize = () => {
isMobile.value = window.innerWidth < 768;
if (!isMobile.value) {
showFilters.value = false; // Reset when switching to desktop
}
};
// Register type options for dropdown - doar cele 4 tipuri, fără "Toate"
const registerTypeOptions = [
{ label: "Casă LEI", value: "CASA_LEI" },
{ label: "Casă Valută", value: "CASA_VALUTA" },
{ label: "Bancă LEI", value: "BANCA_LEI" },
{ label: "Bancă Valută", value: "BANCA_VALUTA" },
];
const filters = ref({
registerType: "BANCA_LEI", // Default: Registrul de Banca Lei
partnerName: "",
bankAccount: null, // Filter for specific bank/cash account
});
// Bank/cash account options for dropdown
const bankAccountOptions = ref([]);
const pagination = ref({
page: 0,
rows: 50,
});
const formatCurrency = (amount, currency = "RON") => {
if (!amount) return "0,00 " + currency;
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: currency,
}).format(amount);
};
const formatNumber = (amount) => {
if (amount === null || amount === undefined) return "";
return new Intl.NumberFormat("ro-RO", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
};
const formatDate = (dateString) => {
if (!dateString) return "";
return format(new Date(dateString), "dd.MM.yyyy");
};
// Short date format for mobile cards (DD/MM)
const formatDateShort = (dateString) => {
if (!dateString) return "";
const date = new Date(dateString);
return `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}`;
};
// Compact number format (no decimals for large numbers)
const formatCompact = (amount) => {
if (!amount) return "0";
if (Math.abs(amount) >= 10000) {
return new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 0,
}).format(amount);
}
return new Intl.NumberFormat("ro-RO", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
};
// Truncate text to maxLength characters
const truncateText = (text, maxLength = 100) => {
if (!text) return "";
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
};
// Check if current filter is a VALUTA type (to show Valuta column)
const isValutaType = computed(() => {
return (
filters.value.registerType === "CASA_VALUTA" ||
filters.value.registerType === "BANCA_VALUTA"
);
});
// Check if current filter is BANCA type (for dynamic column header)
const isBancaType = computed(() => {
return (
filters.value.registerType === "BANCA_LEI" ||
filters.value.registerType === "BANCA_VALUTA"
);
});
// Dynamic column header for Casa/Banca
const contColumnHeader = computed(() => {
return isBancaType.value ? "Banca" : "Casa";
});
// Accounting period text for PDF export
const accountingPeriodText = computed(() => {
// Use the global period store
return periodStore.selectedPeriod?.display_name || "";
});
// Helper to remove diacritics from text
const removeDiacritics = (text) => {
if (!text) return "";
return text
.replace(/[ăâ]/gi, (match) => (match === match.toLowerCase() ? "a" : "A"))
.replace(/[îâ]/gi, (match) => (match === match.toLowerCase() ? "i" : "I"))
.replace(/[ș]/gi, (match) => (match === match.toLowerCase() ? "s" : "S"))
.replace(/[ț]/gi, (match) => (match === match.toLowerCase() ? "t" : "T"))
.replace(/[Ă]/g, "A")
.replace(/[Â]/g, "A")
.replace(/[Î]/g, "I")
.replace(/[Ș]/g, "S")
.replace(/[Ț]/g, "T");
};
// Get register type label for PDF (no diacritics)
const getRegisterTypeLabel = (type) => {
const labels = {
CASA_LEI: "Casa LEI",
CASA_VALUTA: "Casa Valuta",
BANCA_LEI: "Banca LEI",
BANCA_VALUTA: "Banca Valuta",
};
return labels[type] || type;
};
// Get PDF title based on register type
const getPdfTitle = (type) => {
const titles = {
CASA_LEI: "Registrul de Casa LEI",
CASA_VALUTA: "Registrul de Casa Valuta",
BANCA_LEI: "Registrul de Banca LEI",
BANCA_VALUTA: "Registrul de Banca Valuta",
};
return titles[type] || "Registrul de Casa si Banca";
};
// Load bank/cash accounts for dropdown when register type changes
const loadBankAccounts = async () => {
if (!companyStore.selectedCompany || !filters.value.registerType) {
bankAccountOptions.value = [];
return;
}
try {
const apiService = (await import("../services/api")).apiService;
const response = await api.get("/treasury/bank-cash-accounts", {
params: {
company: companyStore.selectedCompany.id_firma,
register_type: filters.value.registerType,
},
});
bankAccountOptions.value = response.data || [];
} catch (error) {
console.error("Failed to load bank accounts:", error);
bankAccountOptions.value = [];
}
};
// Watch for register type changes to reload bank accounts
watch(
() => filters.value.registerType,
async () => {
// Reset bank account selection when register type changes
filters.value.bankAccount = null;
await loadBankAccounts();
},
);
const getRowClass = (data) => {
return data.tip_registru.includes("BANCA") ? "bank-row" : "cash-row";
};
const onPage = async (event) => {
// PrimeVue pagination is 0-indexed for page
pagination.value.page = event.page;
pagination.value.rows = event.rows;
await loadData();
};
const resetFilters = async () => {
filters.value = {
registerType: "BANCA_LEI", // Reset la default: Registrul de Banca Lei
partnerName: "",
bankAccount: null, // Reset bank account filter
};
pagination.value.page = 0;
await loadBankAccounts(); // Reload bank accounts for default register type
await loadData();
};
// Computed
const hasData = computed(() => treasuryStore.registers.length > 0);
// Mobile: Check if any filter is active (non-default value)
const hasActiveFilters = computed(() => {
return (
filters.value.registerType !== "BANCA_LEI" ||
filters.value.partnerName !== "" ||
filters.value.bankAccount !== null
);
});
// Mobile: Actions menu items
const actionMenuItems = computed(() => [
{
label: "Resetează Filtre",
icon: "pi pi-filter-slash",
command: resetFilters,
},
{
label: "Export Excel",
icon: "pi pi-file-excel",
command: exportExcel,
disabled: !hasData.value,
},
{
label: "Export PDF",
icon: "pi pi-file-pdf",
command: exportPDF,
disabled: !hasData.value,
},
{ separator: true },
{
label: "Actualizează",
icon: "pi pi-refresh",
command: refreshData,
},
]);
// Handle company change from dropdown
const handleCompanyChange = async () => {
if (!selectedCompanyId.value) return;
const company = companyStore.getCompanyById(selectedCompanyId.value);
if (company) {
companyStore.setSelectedCompany(company);
await loadData();
}
};
// Handle filter change
const handleFilterChange = async () => {
pagination.value.page = 0;
await loadData();
};
// Debounced search handler
const handleSearchChange = (() => {
let timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(async () => {
pagination.value.page = 0;
await loadData();
}, 500);
};
})();
// Refresh data with toast notification
const refreshData = async () => {
await loadData();
toast.add({
severity: "success",
summary: "Actualizare reușită",
detail: "Registrul a fost actualizat cu succes",
life: 3000,
});
};
// Fetch ALL data for export (not just current page)
const fetchAllData = async () => {
if (!companyStore.selectedCompany) return [];
if (!periodStore.selectedPeriod) return [];
try {
// Get luna/an from period store
const { luna, an } = periodStore.selectedPeriod;
const params = {
company: companyStore.selectedCompany.id_firma,
page: 1,
page_size: 999999, // Get all data
luna: luna,
an: an,
};
// Add register_type filter
if (filters.value.registerType) {
params.register_type = filters.value.registerType;
}
if (filters.value.partnerName) {
params.partner_name = filters.value.partnerName;
}
if (filters.value.bankAccount) {
params.bank_account = filters.value.bankAccount;
}
const apiService = (await import("../services/api")).apiService;
const response = await api.get("/treasury/bank-cash-register", {
params,
});
return response.data.registers || [];
} catch (error) {
console.error("Failed to fetch all data:", error);
return [];
}
};
// Export to Excel
const exportExcel = async () => {
if (!hasData.value) {
toast.add({
severity: "warn",
summary: "Nu există date",
detail: "Nu există înregistrări de exportat",
life: 3000,
});
return;
}
toast.add({
severity: "info",
summary: "Se pregătește exportul",
detail: "Se încarcă toate datele...",
life: 2000,
});
const allData = await fetchAllData();
if (allData.length === 0) {
toast.add({
severity: "error",
summary: "Eroare",
detail: "Nu s-au putut prelua datele pentru export",
life: 3000,
});
return;
}
// Prepare data for export - conditionally include Valuta column only for VALUTA types
// Column order: Data, Nr., Casă/Bancă, Partener, [Valuta], Încasări, Plăți, Sold Cumulat, Explicație
const exportData = allData.map((row) => {
const baseData = {
Data: row.dataact ? formatDate(row.dataact) : "",
"Nr.": row.nract || "",
};
// Use dynamic column name (Casă/Bancă) - BEFORE Partener
baseData[contColumnHeader.value] = row.nume_cont_bancar || "";
baseData["Partener"] = row.nume || "";
// Add Valuta column only for VALUTA register types
if (isValutaType.value) {
baseData["Valuta"] = row.valuta || "";
}
// Add numeric columns
baseData["Încasări"] = parseFloat(row.incasari) || 0;
baseData["Plăți"] = parseFloat(row.plati) || 0;
baseData["Sold Cumulat"] = parseFloat(row.sold) || 0;
baseData["Explicație"] = truncateText(row.explicatia, 100);
return baseData;
});
const result = exportToExcel(
exportData,
`registru_casa_banca_${companyStore.selectedCompany.name.replace(/\s+/g, "_")}`,
"Registru Casă și Bancă",
);
if (result.success) {
toast.add({
severity: "success",
summary: "Export reușit",
detail: `${allData.length} înregistrări exportate cu succes`,
life: 3000,
});
} else {
toast.add({
severity: "error",
summary: "Eroare la export",
detail: "Nu s-a putut genera fișierul Excel",
life: 3000,
});
}
};
// Export to PDF
const exportPDF = async () => {
if (!hasData.value) {
toast.add({
severity: "warn",
summary: "Nu există date",
detail: "Nu există înregistrări de exportat",
life: 3000,
});
return;
}
toast.add({
severity: "info",
summary: "Se pregătește exportul",
detail: "Se încarcă toate datele...",
life: 2000,
});
const allData = await fetchAllData();
if (allData.length === 0) {
toast.add({
severity: "error",
summary: "Eroare",
detail: "Nu s-au putut prelua datele pentru export",
life: 3000,
});
return;
}
// Dynamic title based on register type
const pdfTitle = getPdfTitle(filters.value.registerType);
// Use the specialized Bank Cash Register PDF export
const result = exportBankCashRegisterPDF(
allData,
{
companyName: removeDiacritics(companyStore.selectedCompany?.name || ""),
title: pdfTitle,
luna: treasuryStore.accountingPeriod.luna,
an: treasuryStore.accountingPeriod.an,
isBanca: isBancaType.value,
},
`registru-casa-banca-${companyStore.selectedCompany.name.replace(/\s+/g, "-")}`,
);
if (result.success) {
toast.add({
severity: "success",
summary: "Export reușit",
detail: `${allData.length} înregistrări exportate cu succes`,
life: 3000,
});
} else {
toast.add({
severity: "error",
summary: "Eroare la export",
detail: "Nu s-a putut genera fișierul PDF",
life: 3000,
});
}
};
const loadData = async () => {
if (!companyStore.selectedCompany) return;
if (!periodStore.selectedPeriod) return; // Wait for period to be loaded
treasuryStore.setPagination(pagination.value);
// Get luna/an from period store
const { luna, an } = periodStore.selectedPeriod;
// Build filter params with luna/an instead of date_from/date_to
const filterParams = {
partner_name: filters.value.partnerName || undefined,
register_type: filters.value.registerType || undefined,
bank_account: filters.value.bankAccount || undefined,
luna: luna,
an: an,
};
await treasuryStore.loadBankCashRegister(
companyStore.selectedCompany.id_firma,
filterParams,
);
};
onMounted(async () => {
// Add resize listener for mobile detection
window.addEventListener("resize", handleResize);
// Load companies if not loaded
if (!companyStore.hasCompanies) {
await companyStore.loadCompanies();
}
// Load bank accounts for initial register type if company is selected
if (companyStore.selectedCompany) {
await loadBankAccounts();
}
// Don't load data here - let period watch handle it with immediate: true
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
// Watch for company changes
watch(
() => companyStore.selectedCompany,
async (newCompany) => {
if (newCompany && periodStore.selectedPeriod) {
await loadBankAccounts();
await loadData();
}
},
);
// Watch for period changes - reload data when period changes
watch(
() => periodStore.selectedPeriod,
async (newPeriod) => {
if (newPeriod && companyStore.selectedCompany) {
await loadData();
}
},
{ immediate: true },
);
</script>
<style scoped>
/* ===== Page-Specific Styles Only ===== */
/* Uses shared CSS: dashboard.css (.page-header, .page-title, .page-subtitle) */
/* Uses shared CSS: forms.css (.form-actions) */
/* Uses shared CSS: tables.css (.table-empty, .loading-state, .negative) */
/* Uses shared CSS: stats.css (.summary-stats-inline) */
/* Uses shared CSS: primevue-overrides.css (DataTable striped rows, hover, compact) */
/* Page Container */
.register-view {
max-width: 1400px;
margin: 0 auto;
padding: var(--space-xl);
}
/* Card Spacing */
.company-selection-card,
.filters-card,
.data-card {
margin-bottom: var(--space-xl);
}
/* Numeric Values - Page-specific formatting */
.numeric-value {
display: block;
text-align: right;
font-variant-numeric: tabular-nums;
font-family: var(--font-mono, "Roboto Mono", "Consolas", monospace);
}
.numeric-value.zero {
color: var(--color-text-muted);
}
.numeric-value.negative {
color: var(--color-error);
}
/* Responsive */
@media (max-width: 768px) {
.register-view {
padding: var(--space-md);
}
}
</style>