Files
roa2web-service-auto/src/modules/reports/views/BankCashRegisterView.vue
Claude Agent b137e80b71 feat: multi-Oracle server support with runtime switching
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>
2026-01-26 22:39:06 +00:00

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>