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:
917
src/modules/reports/views/InvoicesView.vue
Normal file
917
src/modules/reports/views/InvoicesView.vue
Normal file
@@ -0,0 +1,917 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="invoices">
|
||||
<!-- Header Section -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">
|
||||
<i class="pi pi-file-text"></i>
|
||||
Facturi
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Company Selection -->
|
||||
<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 facturile:
|
||||
</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="clearFilters"
|
||||
v-tooltip.bottom="'Resetează'"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-excel"
|
||||
class="p-button-text p-button-success"
|
||||
@click="exportExcel"
|
||||
:disabled="!invoicesStore.hasInvoices"
|
||||
v-tooltip.bottom="'Excel'"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-pdf"
|
||||
class="p-button-text p-button-danger"
|
||||
@click="exportPDF"
|
||||
:disabled="!invoicesStore.hasInvoices"
|
||||
v-tooltip.bottom="'PDF'"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
class="p-button-text"
|
||||
:loading="invoicesStore.isLoading"
|
||||
@click="refreshData"
|
||||
v-tooltip.bottom="'Actualizează'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Totals (unified grid format) -->
|
||||
<div class="mobile-toolbar-totals">
|
||||
<div class="mobile-totals-grid single-total">
|
||||
<div class="total-item">
|
||||
<span class="total-label">Sold Total:</span>
|
||||
<span class="total-value" :class="invoicesStore.totalSoldAll > 0 ? 'positive' : 'negative'">
|
||||
{{ formatCompact(invoicesStore.totalSoldAll) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters and Controls -->
|
||||
<Card v-if="companyStore.selectedCompany && (!isMobile || showFilters)" class="filters-card">
|
||||
<template #content>
|
||||
<div class="form">
|
||||
<div class="form-row">
|
||||
<!-- Invoice Type -->
|
||||
<div class="form-col">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tip Factură</label>
|
||||
<Dropdown
|
||||
v-model="filters.type"
|
||||
:options="invoiceTypes"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
placeholder="Tip factură"
|
||||
class="w-full"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Status -->
|
||||
<div class="form-col">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status Plată</label>
|
||||
<Dropdown
|
||||
v-model="filters.paymentStatus"
|
||||
:options="paymentStatusOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
placeholder="Status plată"
|
||||
class="w-full"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="form-col">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Căutare</label>
|
||||
<InputText
|
||||
v-model="filters.searchTerm"
|
||||
placeholder="Căutați după număr, partener..."
|
||||
class="w-full"
|
||||
@input="handleSearchChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cont Filter -->
|
||||
<div class="form-col">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Cont</label>
|
||||
<InputText
|
||||
v-model="filters.cont"
|
||||
placeholder="Filtru cont (ex: 4111)"
|
||||
class="w-full"
|
||||
@input="handleSearchChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Action buttons -->
|
||||
<div v-if="!isMobile" class="filters-actions">
|
||||
<Button
|
||||
icon="pi pi-filter-slash"
|
||||
label="Resetează Filtre"
|
||||
class="p-button-outlined p-button-secondary"
|
||||
@click="clearFilters"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-excel"
|
||||
label="Export Excel"
|
||||
class="p-button-outlined p-button-success"
|
||||
@click="exportExcel"
|
||||
:disabled="!invoicesStore.hasInvoices"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-pdf"
|
||||
label="Export PDF"
|
||||
class="p-button-outlined p-button-danger"
|
||||
@click="exportPDF"
|
||||
:disabled="!invoicesStore.hasInvoices"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
label="Actualizează"
|
||||
:loading="invoicesStore.isLoading"
|
||||
@click="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- Summary Stats - Compact, right aligned (hidden on mobile - shown in toolbar) -->
|
||||
<!-- Total sold din TOATE facturile filtrate (nu doar pagina curentă) -->
|
||||
<div v-if="!isMobile && companyStore.selectedCompany && invoicesStore.hasInvoices" class="summary-stats-inline">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Sold:</span>
|
||||
<span class="stat-value" :class="invoicesStore.totalSoldAll > 0 ? 'plati' : 'incasari'">
|
||||
{{ formatCurrency(invoicesStore.totalSoldAll) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoices Table -->
|
||||
<Card v-if="companyStore.selectedCompany" class="table-card">
|
||||
<template #content>
|
||||
<!-- Mobile: Card Layout -->
|
||||
<div v-if="isMobile" class="mobile-card-list">
|
||||
<div
|
||||
v-for="invoice in invoicesStore.invoiceList"
|
||||
:key="invoice.nract"
|
||||
class="mobile-data-card"
|
||||
>
|
||||
<div class="card-header">{{ invoice.nume }}</div>
|
||||
<div class="card-row">
|
||||
<span>{{ formatDate(invoice.dataact) }} · {{ invoice.nract }}</span>
|
||||
<span
|
||||
class="card-amount"
|
||||
:class="{ positive: invoice.soldfinal > 0 }"
|
||||
>
|
||||
{{ formatNumber(invoice.soldfinal) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="invoicesStore.invoiceList.length === 0" class="mobile-empty">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<p>Nu au fost găsite facturi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: DataTable -->
|
||||
<DataTable
|
||||
v-if="!isMobile"
|
||||
:value="invoicesStore.invoiceList"
|
||||
:loading="invoicesStore.isLoading"
|
||||
:paginator="true"
|
||||
:rows="pagination.rows"
|
||||
:total-records="invoicesStore.totalInvoices"
|
||||
: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="onPageChange"
|
||||
@sort="onSort"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="no-data">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<p>Nu au fost găsite facturi</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="loading-table">
|
||||
<ProgressSpinner />
|
||||
<p>Se încarcă facturile...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="cont" header="Cont" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.cont || "-" }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="nract" header="Numar Doc." sortable>
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.nract }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="dataact" header="Data Doc." sortable>
|
||||
<template #body="slotProps">
|
||||
{{ formatDate(slotProps.data.dataact) }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="datascad" header="Data Scadenta" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ formatDate(slotProps.data.datascad) }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="nume" header="Partener" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.nume }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="totctva" header="Facturat" sortable>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">
|
||||
{{ formatNumber(slotProps.data.totctva) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="achitat" header="Achitat" sortable>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">
|
||||
{{ formatNumber(slotProps.data.achitat) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="soldfinal" header="Sold" sortable>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">
|
||||
{{ formatNumber(slotProps.data.soldfinal) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column
|
||||
field="valuta"
|
||||
header="Valuta"
|
||||
sortable
|
||||
:style="{ width: '8%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-center">
|
||||
{{ slotProps.data.valuta || "RON" }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { useCompanyStore } from "@reports/stores/sharedStores";
|
||||
import { useInvoicesStore } from "@reports/stores/invoices";
|
||||
import { useAccountingPeriodStore } from "@reports/stores/sharedStores";
|
||||
import { format } from "date-fns";
|
||||
import { ro } from "date-fns/locale";
|
||||
import { exportToExcel, exportToPDF } from "@reports/utils/exportUtils";
|
||||
|
||||
const toast = useToast();
|
||||
const companyStore = useCompanyStore();
|
||||
const invoicesStore = useInvoicesStore();
|
||||
const periodStore = useAccountingPeriodStore();
|
||||
|
||||
// State
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
const filters = ref({
|
||||
type: "CLIENTI",
|
||||
paymentStatus: "neachitate", // Default to unpaid invoices
|
||||
searchTerm: "",
|
||||
cont: "",
|
||||
});
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
rows: 100, // Changed from 50 to 100
|
||||
});
|
||||
|
||||
// Computed
|
||||
const accountingPeriodText = computed(() => {
|
||||
// Use the global period store
|
||||
return periodStore.selectedPeriod?.display_name || "";
|
||||
});
|
||||
|
||||
// Mobile: Check if any filter is active (non-default value)
|
||||
const hasActiveFilters = computed(() => {
|
||||
return (
|
||||
filters.value.type !== "CLIENTI" ||
|
||||
filters.value.paymentStatus !== "neachitate" ||
|
||||
filters.value.searchTerm !== "" ||
|
||||
filters.value.cont !== ""
|
||||
);
|
||||
});
|
||||
|
||||
// Mobile: Actions menu items
|
||||
const actionMenuItems = computed(() => [
|
||||
{
|
||||
label: "Resetează Filtre",
|
||||
icon: "pi pi-filter-slash",
|
||||
command: clearFilters,
|
||||
},
|
||||
{
|
||||
label: "Export Excel",
|
||||
icon: "pi pi-file-excel",
|
||||
command: exportExcel,
|
||||
disabled: !invoicesStore.hasInvoices,
|
||||
},
|
||||
{
|
||||
label: "Export PDF",
|
||||
icon: "pi pi-file-pdf",
|
||||
command: exportPDF,
|
||||
disabled: !invoicesStore.hasInvoices,
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: "Actualizează",
|
||||
icon: "pi pi-refresh",
|
||||
command: refreshData,
|
||||
},
|
||||
]);
|
||||
|
||||
// Options
|
||||
const invoiceTypes = [
|
||||
{ label: "Clienți", value: "CLIENTI" },
|
||||
{ label: "Furnizori", value: "FURNIZORI" },
|
||||
];
|
||||
|
||||
const paymentStatusOptions = [
|
||||
{ label: "Neachitate", value: "neachitate" },
|
||||
{ label: "Toate", value: "toate" },
|
||||
];
|
||||
|
||||
// Methods
|
||||
const formatCurrency = (amount) => {
|
||||
if (!amount) return "0,00 RON";
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
style: "currency",
|
||||
currency: "RON",
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatNumber = (amount) => {
|
||||
if (!amount || amount === 0) return "0,00";
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Compact format for mobile totals (e.g., "34.922" instead of "34.922,02 RON")
|
||||
const formatCompact = (amount) => {
|
||||
if (!amount || amount === 0) return "0";
|
||||
const absAmount = Math.abs(amount);
|
||||
if (absAmount >= 1000000) {
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
maximumFractionDigits: 1,
|
||||
}).format(amount / 1000000) + "M";
|
||||
}
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
try {
|
||||
return format(new Date(dateString), "dd/MM/yyyy", { locale: ro });
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompanyChange = async () => {
|
||||
if (!selectedCompanyId.value) return;
|
||||
|
||||
const company = companyStore.getCompanyById(selectedCompanyId.value);
|
||||
if (company) {
|
||||
companyStore.setSelectedCompany(company);
|
||||
await loadInvoices();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = async () => {
|
||||
pagination.value.page = 1;
|
||||
await loadInvoices();
|
||||
};
|
||||
|
||||
const handleSearchChange = (() => {
|
||||
let timeout;
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
pagination.value.page = 1;
|
||||
await loadInvoices();
|
||||
}, 500);
|
||||
};
|
||||
})();
|
||||
|
||||
const clearFilters = async () => {
|
||||
filters.value = {
|
||||
type: "CLIENTI",
|
||||
paymentStatus: "neachitate",
|
||||
searchTerm: "",
|
||||
cont: "",
|
||||
};
|
||||
pagination.value.page = 1;
|
||||
await loadInvoices();
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await loadInvoices();
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "Actualizare reușită",
|
||||
detail: "Facturile au fost actualizate cu succes",
|
||||
life: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
const loadInvoices = async () => {
|
||||
if (!companyStore.selectedCompany) return;
|
||||
if (!periodStore.selectedPeriod) return; // Wait for period to be loaded
|
||||
|
||||
try {
|
||||
// Set filters in store FIRST
|
||||
invoicesStore.setFilters(filters.value);
|
||||
invoicesStore.setPagination(pagination.value);
|
||||
|
||||
// Use luna/an from period store directly
|
||||
const { luna, an } = periodStore.selectedPeriod;
|
||||
|
||||
const params = {
|
||||
partner_type: filters.value.type,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.rows,
|
||||
only_unpaid: filters.value.paymentStatus === "neachitate",
|
||||
luna: luna,
|
||||
an: an,
|
||||
};
|
||||
|
||||
if (filters.value.searchTerm) {
|
||||
params.partner_name = filters.value.searchTerm;
|
||||
}
|
||||
if (filters.value.cont) {
|
||||
params.cont = filters.value.cont;
|
||||
}
|
||||
|
||||
await invoicesStore.loadInvoices(
|
||||
companyStore.selectedCompany.id_firma,
|
||||
params,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load invoices:", error);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Eroare",
|
||||
detail: "Nu s-au putut încărca facturile",
|
||||
life: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onPageChange = async (event) => {
|
||||
// PrimeVue pagination is 0-indexed, backend expects 1-indexed
|
||||
pagination.value.page = event.page + 1;
|
||||
pagination.value.rows = event.rows;
|
||||
await loadInvoices();
|
||||
};
|
||||
|
||||
const onSort = async (event) => {
|
||||
// Handle sorting if needed
|
||||
await loadInvoices();
|
||||
};
|
||||
|
||||
// Export methods - Fetch ALL data (not just current page)
|
||||
const fetchAllInvoicesData = async () => {
|
||||
if (!companyStore.selectedCompany) return [];
|
||||
if (!periodStore.selectedPeriod) return [];
|
||||
|
||||
try {
|
||||
// Use luna/an from period store
|
||||
const { luna, an } = periodStore.selectedPeriod;
|
||||
|
||||
const params = {
|
||||
company: companyStore.selectedCompany.id_firma,
|
||||
partner_type: filters.value.type,
|
||||
page: 1,
|
||||
page_size: 999999, // Get all data
|
||||
only_unpaid: filters.value.paymentStatus === "neachitate",
|
||||
luna: luna,
|
||||
an: an,
|
||||
};
|
||||
|
||||
if (filters.value.searchTerm) {
|
||||
params.partner_name = filters.value.searchTerm;
|
||||
}
|
||||
if (filters.value.cont) {
|
||||
params.cont = filters.value.cont;
|
||||
}
|
||||
|
||||
const apiService = (await import("../services/api")).apiService;
|
||||
const response = await api.get("/invoices/", { params });
|
||||
|
||||
return response.data.invoices || [];
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch all invoices data:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const exportExcel = async () => {
|
||||
if (!invoicesStore.hasInvoices) {
|
||||
toast.add({
|
||||
severity: "warn",
|
||||
summary: "Nu există date",
|
||||
detail: "Nu există facturi de exportat",
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch ALL data for export (not just current page)
|
||||
toast.add({
|
||||
severity: "info",
|
||||
summary: "Se pregătește exportul",
|
||||
detail: "Se încarcă toate datele...",
|
||||
life: 2000,
|
||||
});
|
||||
|
||||
const allData = await fetchAllInvoicesData();
|
||||
|
||||
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 - Format dates as strings for Excel
|
||||
const exportData = allData.map((row) => ({
|
||||
Cont: row.cont || "",
|
||||
"Numar Doc.": row.nract,
|
||||
"Data Doc.": row.dataact ? formatDate(row.dataact) : "",
|
||||
"Data Scadenta": row.datascad ? formatDate(row.datascad) : "",
|
||||
Partener: row.nume,
|
||||
Facturat: parseFloat(row.totctva) || 0,
|
||||
Achitat: parseFloat(row.achitat) || 0,
|
||||
Sold: parseFloat(row.soldfinal) || 0,
|
||||
Valuta: row.valuta || "RON",
|
||||
}));
|
||||
|
||||
const invoiceType =
|
||||
filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
|
||||
const result = exportToExcel(
|
||||
exportData,
|
||||
`facturi_${invoiceType}_${companyStore.selectedCompany.name.replace(/\s+/g, "_")}`,
|
||||
`Facturi ${invoiceType}`,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "Export reușit",
|
||||
detail: `${allData.length} facturi 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exportPDF = async () => {
|
||||
if (!invoicesStore.hasInvoices) {
|
||||
toast.add({
|
||||
severity: "warn",
|
||||
summary: "Nu există date",
|
||||
detail: "Nu există facturi de exportat",
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch ALL data for export (not just current page)
|
||||
toast.add({
|
||||
severity: "info",
|
||||
summary: "Se pregătește exportul",
|
||||
detail: "Se încarcă toate datele...",
|
||||
life: 2000,
|
||||
});
|
||||
|
||||
const allData = await fetchAllInvoicesData();
|
||||
|
||||
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 - Format dates as strings for PDF
|
||||
const exportData = allData.map((row) => ({
|
||||
cont: row.cont || "",
|
||||
nract: row.nract,
|
||||
dataact: row.dataact ? formatDate(row.dataact) : "",
|
||||
datascad: row.datascad ? formatDate(row.datascad) : "",
|
||||
nume: row.nume,
|
||||
totctva: row.totctva,
|
||||
achitat: row.achitat,
|
||||
soldfinal: row.soldfinal,
|
||||
valuta: row.valuta || "RON",
|
||||
}));
|
||||
|
||||
// Define columns for PDF with optimized widths (proportional percentages)
|
||||
// Compact numeric columns, more space for Partener (company names)
|
||||
const columns = [
|
||||
{ field: "cont", header: "Cont", type: "text", width: 0.06 }, // 6% - Compact account numbers
|
||||
{ field: "nract", header: "Numar Doc.", type: "text", width: 0.08 }, // 8% - Document numbers
|
||||
{ field: "dataact", header: "Data Doc.", type: "text", width: 0.08 }, // 8% - Dates
|
||||
{ field: "datascad", header: "Data Scadenta", type: "text", width: 0.09 }, // 9% - Due dates
|
||||
{ field: "nume", header: "Partener", type: "text", width: 0.37 }, // 37% - MORE SPACE for company names
|
||||
{ field: "totctva", header: "Facturat", type: "number", width: 0.09 }, // 9% - Compact numbers
|
||||
{ field: "achitat", header: "Achitat", type: "number", width: 0.09 }, // 9% - Compact numbers
|
||||
{ field: "soldfinal", header: "Sold", type: "number", width: 0.09 }, // 9% - Compact numbers
|
||||
{ field: "valuta", header: "Valuta", type: "text", width: 0.05 }, // 5% - Very compact (just "RON")
|
||||
];
|
||||
|
||||
const invoiceType =
|
||||
filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
|
||||
|
||||
// Build period string - ALWAYS show accounting period (like Trial Balance)
|
||||
let periodText = accountingPeriodText.value || "";
|
||||
|
||||
// Optionally add date filter range if applied
|
||||
if (filters.value.dateFrom || filters.value.dateTo) {
|
||||
const fromDate = filters.value.dateFrom
|
||||
? formatDate(filters.value.dateFrom)
|
||||
: "început";
|
||||
const toDate = filters.value.dateTo
|
||||
? formatDate(filters.value.dateTo)
|
||||
: "prezent";
|
||||
periodText += periodText
|
||||
? ` | Filtru dată: ${fromDate} - ${toDate}`
|
||||
: `Filtru dată: ${fromDate} - ${toDate}`;
|
||||
}
|
||||
|
||||
const result = exportToPDF(
|
||||
exportData,
|
||||
columns,
|
||||
`facturi-${invoiceType.toLowerCase()}-${companyStore.selectedCompany.name.replace(/\s+/g, "-")}`,
|
||||
{
|
||||
companyName: companyStore.selectedCompany?.name || "",
|
||||
title: `Facturi ${invoiceType}`,
|
||||
period: periodText,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "Export reușit",
|
||||
detail: `${allData.length} facturi 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
// Add resize listener for mobile detection
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Load companies if not loaded
|
||||
if (!companyStore.hasCompanies) {
|
||||
await companyStore.loadCompanies();
|
||||
}
|
||||
// Don't load 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 loadInvoices();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Watch for period changes - reload data when period changes
|
||||
watch(
|
||||
() => periodStore.selectedPeriod,
|
||||
async (newPeriod) => {
|
||||
if (newPeriod && companyStore.selectedCompany) {
|
||||
await loadInvoices();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.invoices {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.company-selection-card,
|
||||
.filters-card {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-col {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.filters-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.no-data,
|
||||
.loading-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.no-data i,
|
||||
.loading-table i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Enhanced striped rows with better contrast - same as Trial Balance */
|
||||
.table-card :deep(.p-datatable .p-datatable-tbody > tr) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.table-card :deep(.p-datatable .p-datatable-tbody > tr:nth-child(odd)) {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.table-card :deep(.p-datatable .p-datatable-tbody > tr:nth-child(even)) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.table-card :deep(.p-datatable .p-datatable-tbody > tr:hover) {
|
||||
background-color: #e3f2fd !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.invoices {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.search-col {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.filters-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user