Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1314 lines
37 KiB
Vue
1314 lines
37 KiB
Vue
<template>
|
|
<div class="app-container" :class="{ 'mobile-layout': isMobile }">
|
|
<!-- US-501: Mobile Material Design Top Bar -->
|
|
<MobileTopBar
|
|
v-if="isMobile"
|
|
title="Trezorerie"
|
|
:show-menu="true"
|
|
:actions="mobileTopBarActions"
|
|
@menu-click="showDrawer = true"
|
|
@action-click="handleTopBarAction"
|
|
/>
|
|
|
|
<!-- US-501: Export Menu (for mobile export dropdown) -->
|
|
<Menu ref="exportMenu" :model="exportMenuItems" :popup="true" />
|
|
|
|
<!-- Mobile Drawer Menu (replaces old Sidebar) -->
|
|
<MobileDrawerMenu
|
|
v-model="showDrawer"
|
|
:user="authStore.user"
|
|
:companies-store="companyStore"
|
|
:period-store="periodStore"
|
|
:available-servers="authStore.availableServers"
|
|
:current-server-id="authStore.selectedServerId"
|
|
:auth-store="authStore"
|
|
@logout="handleLogout"
|
|
@server-switched="handleServerSwitched"
|
|
/>
|
|
|
|
<!-- US-109: Filter BottomSheet for mobile -->
|
|
<BottomSheet v-model="showFilters">
|
|
<h3 class="bottom-sheet-title">Filtre</h3>
|
|
<div class="bottom-sheet-filters">
|
|
<!-- Register Type -->
|
|
<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>
|
|
|
|
<!-- Bank/Cash Account -->
|
|
<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>
|
|
|
|
<!-- Partner Search -->
|
|
<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>
|
|
|
|
<!-- Bottom sheet actions -->
|
|
<div class="bottom-sheet-actions">
|
|
<Button
|
|
icon="pi pi-filter-slash"
|
|
label="Resetează"
|
|
class="p-button-outlined p-button-secondary"
|
|
@click="resetFilters(); showFilters = false"
|
|
/>
|
|
<Button
|
|
icon="pi pi-check"
|
|
label="Aplică"
|
|
@click="showFilters = false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</BottomSheet>
|
|
|
|
<div class="register-view">
|
|
<!-- Header - Desktop only -->
|
|
<div class="page-header" v-if="!isMobile">
|
|
<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>
|
|
|
|
<!-- US-109: Mobile Totals Bar (replaces old two-row toolbar) -->
|
|
<div v-if="isMobile && companyStore.selectedCompany && hasData" class="mobile-totals-bar">
|
|
<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>
|
|
|
|
<!-- Filters - Desktop only (mobile uses BottomSheet) -->
|
|
<Card v-if="companyStore.selectedCompany && !isMobile" 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>
|
|
|
|
<!-- US-501: Desktop Action buttons with Export dropdown -->
|
|
<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"
|
|
/>
|
|
<SplitButton
|
|
label="Export"
|
|
icon="pi pi-download"
|
|
:model="desktopExportItems"
|
|
@click="exportPDF"
|
|
class="p-button-outlined"
|
|
: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>
|
|
|
|
<!-- US-109/US-307: Mobile Bottom Navigation (using default nav items) -->
|
|
<MobileBottomNav v-if="isMobile" />
|
|
</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, useAuthStore } from "@reports/stores/sharedStores";
|
|
import { useAccountingPeriodStore } from "@reports/stores/sharedStores";
|
|
import { format } from "date-fns";
|
|
import { exportToExcel, exportBankCashRegisterPDF } from "@reports/utils/exportUtils";
|
|
|
|
// US-501: Mobile Material Design components
|
|
import MobileTopBar from "@shared/components/mobile/MobileTopBar.vue";
|
|
import MobileBottomNav from "@shared/components/mobile/MobileBottomNav.vue";
|
|
import BottomSheet from "@shared/components/mobile/BottomSheet.vue";
|
|
import MobileDrawerMenu from "@shared/components/mobile/MobileDrawerMenu.vue";
|
|
import Menu from "primevue/menu";
|
|
import SplitButton from "primevue/splitbutton";
|
|
import { useRouter } from "vue-router";
|
|
|
|
const toast = useToast();
|
|
const router = useRouter();
|
|
const treasuryStore = useTreasuryStore();
|
|
const companyStore = useCompanyStore();
|
|
const periodStore = useAccountingPeriodStore();
|
|
const authStore = useAuthStore();
|
|
|
|
// 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);
|
|
const showDrawer = ref(false);
|
|
|
|
// Handle logout from drawer menu
|
|
const handleLogout = async () => {
|
|
await authStore.logout();
|
|
router.push('/login');
|
|
};
|
|
|
|
// Handle server switch completed - reload data for new server
|
|
const handleServerSwitched = async (newServerId) => {
|
|
// Server switch already completed in MobileDrawerMenu modal
|
|
// Reload data for the new server
|
|
await companyStore.loadCompanies();
|
|
if (companyStore.selectedCompany?.id_firma) {
|
|
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
|
|
}
|
|
};
|
|
|
|
// US-501: Mobile TopBar actions (filter, reset, export dropdown)
|
|
const mobileTopBarActions = computed(() => [
|
|
{
|
|
icon: "pi pi-filter",
|
|
label: "Filtre",
|
|
tooltip: "Filtre",
|
|
active: hasActiveFilters.value
|
|
},
|
|
{
|
|
icon: "pi pi-filter-slash",
|
|
label: "Resetează",
|
|
tooltip: "Resetează Filtrele"
|
|
},
|
|
{
|
|
icon: "pi pi-download",
|
|
label: "Export",
|
|
tooltip: "Export"
|
|
}
|
|
]);
|
|
|
|
// US-501: Export menu ref for mobile
|
|
const exportMenu = ref(null);
|
|
const exportMenuItems = ref([
|
|
{
|
|
label: "Export PDF",
|
|
icon: "pi pi-file-pdf",
|
|
command: () => exportPDF()
|
|
},
|
|
{
|
|
label: "Export XLSX",
|
|
icon: "pi pi-file-excel",
|
|
command: () => exportExcel()
|
|
}
|
|
]);
|
|
|
|
// US-501: Desktop export dropdown items (SplitButton)
|
|
const desktopExportItems = ref([
|
|
{
|
|
label: "Export PDF",
|
|
icon: "pi pi-file-pdf",
|
|
command: () => exportPDF()
|
|
},
|
|
{
|
|
label: "Export XLSX",
|
|
icon: "pi pi-file-excel",
|
|
command: () => exportExcel()
|
|
}
|
|
]);
|
|
|
|
// US-501: Handle top bar action clicks
|
|
const handleTopBarAction = (action, event) => {
|
|
if (action.icon === "pi pi-filter") {
|
|
showFilters.value = !showFilters.value;
|
|
} else if (action.icon === "pi pi-filter-slash") {
|
|
resetFilters();
|
|
} else if (action.icon === "pi pi-download") {
|
|
exportMenu.value.toggle(event);
|
|
}
|
|
};
|
|
|
|
// US-109: Bottom nav items for MobileBottomNav component
|
|
// US-307: Removed custom mobileBottomNavItems - using MobileBottomNav defaults
|
|
|
|
// 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")).default;
|
|
const response = await apiService.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")).default;
|
|
const response = await apiService.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>
|
|
/* ================================================
|
|
US-109: BankCashRegisterView Mobile Material Design Styles
|
|
================================================ */
|
|
|
|
/* Page Container */
|
|
.register-view {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: var(--space-xl);
|
|
}
|
|
|
|
/* Mobile layout adjustments for top/bottom bars */
|
|
.mobile-layout .register-view {
|
|
padding-top: calc(56px + var(--space-md)); /* Account for fixed MobileTopBar */
|
|
padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */
|
|
padding-left: var(--space-md);
|
|
padding-right: var(--space-md);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* ================================================
|
|
US-109: Mobile Totals Bar
|
|
================================================ */
|
|
|
|
.mobile-totals-bar {
|
|
background: var(--surface-card);
|
|
border: 1px solid var(--surface-border);
|
|
padding: var(--space-sm) var(--space-md);
|
|
margin-bottom: var(--space-md);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.mobile-totals-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: var(--space-xs) var(--space-md);
|
|
}
|
|
|
|
.mobile-totals-bar .total-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.mobile-totals-bar .total-label {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-color-secondary);
|
|
font-weight: var(--font-medium);
|
|
}
|
|
|
|
.mobile-totals-bar .total-value {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-bold);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.mobile-totals-bar .total-value.incasari {
|
|
color: var(--green-600);
|
|
}
|
|
|
|
.mobile-totals-bar .total-value.plati {
|
|
color: var(--red-600);
|
|
}
|
|
|
|
/* ================================================
|
|
US-109: Mobile Card List (Treasury Cards)
|
|
================================================ */
|
|
|
|
.mobile-card-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.mobile-data-card {
|
|
background: var(--surface-card);
|
|
border: 1px solid var(--surface-border);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.mobile-data-card .card-header {
|
|
font-weight: var(--font-semibold);
|
|
color: var(--text-color);
|
|
margin-bottom: var(--space-xs);
|
|
font-size: var(--text-base);
|
|
}
|
|
|
|
.mobile-data-card .card-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: var(--text-sm);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.mobile-data-card .card-meta {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.mobile-data-card .card-amount {
|
|
font-weight: var(--font-semibold);
|
|
color: var(--text-color);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.mobile-data-card .card-amount.positive {
|
|
color: var(--green-600);
|
|
}
|
|
|
|
.mobile-data-card .card-amount.negative {
|
|
color: var(--red-600);
|
|
}
|
|
|
|
.mobile-empty {
|
|
text-align: center;
|
|
padding: var(--space-xl);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.mobile-empty i {
|
|
font-size: var(--text-3xl);
|
|
margin-bottom: var(--space-sm);
|
|
display: block;
|
|
}
|
|
|
|
/* ================================================
|
|
US-109: Bottom Sheet Styles
|
|
================================================ */
|
|
|
|
.bottom-sheet-title {
|
|
font-size: var(--text-lg);
|
|
font-weight: var(--font-semibold);
|
|
color: var(--text-color);
|
|
margin: 0 0 var(--space-md) 0;
|
|
}
|
|
|
|
.bottom-sheet-filters {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.bottom-sheet-actions {
|
|
display: flex;
|
|
gap: var(--space-sm);
|
|
justify-content: flex-end;
|
|
margin-top: var(--space-md);
|
|
padding-top: var(--space-md);
|
|
border-top: 1px solid var(--surface-border);
|
|
}
|
|
|
|
/* ================================================
|
|
US-109: Mobile Sidebar Menu Styles
|
|
================================================ */
|
|
|
|
.sidebar-header {
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.sidebar-title {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--font-bold);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.sidebar-menu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: var(--space-sm);
|
|
}
|
|
|
|
.sidebar-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-md);
|
|
padding: var(--space-md);
|
|
color: var(--text-color);
|
|
text-decoration: none;
|
|
border-radius: var(--radius-md);
|
|
font-weight: var(--font-medium);
|
|
transition: background var(--transition-fast);
|
|
}
|
|
|
|
.sidebar-item:hover {
|
|
background: var(--surface-hover);
|
|
}
|
|
|
|
.sidebar-item.active {
|
|
background: var(--blue-50);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.sidebar-item i {
|
|
font-size: var(--text-xl);
|
|
width: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ================================================
|
|
Dark Mode Support
|
|
================================================ */
|
|
|
|
[data-theme="dark"] .mobile-totals-bar .total-value.incasari {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
[data-theme="dark"] .mobile-totals-bar .total-value.plati {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
[data-theme="dark"] .mobile-data-card .card-amount.positive {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
[data-theme="dark"] .mobile-data-card .card-amount.negative {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
[data-theme="dark"] .sidebar-item.active {
|
|
background: var(--blue-900);
|
|
color: var(--blue-400);
|
|
}
|
|
|
|
/* Auto dark mode */
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme]) .mobile-totals-bar .total-value.incasari {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .mobile-totals-bar .total-value.plati {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .mobile-data-card .card-amount.positive {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .mobile-data-card .card-amount.negative {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .sidebar-item.active {
|
|
background: var(--blue-900);
|
|
color: var(--blue-400);
|
|
}
|
|
}
|
|
|
|
/* ================================================
|
|
Responsive Design
|
|
================================================ */
|
|
|
|
@media (max-width: 768px) {
|
|
.register-view {
|
|
padding: var(--space-md);
|
|
}
|
|
}
|
|
</style>
|