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:
943
src/modules/reports/views/BankCashRegisterView.vue
Normal file
943
src/modules/reports/views/BankCashRegisterView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user