Implemented by Ralph autonomous loop. Iteration: 1 Co-Authored-By: Claude <noreply@anthropic.com>
1258 lines
34 KiB
Vue
1258 lines
34 KiB
Vue
<template>
|
|
<div class="app-container" :class="{ 'mobile-layout': isMobile }">
|
|
<!-- US-107: Mobile Material Design Top Bar -->
|
|
<MobileTopBar
|
|
v-if="isMobile"
|
|
title="Facturi"
|
|
:show-menu="true"
|
|
:actions="mobileTopBarActions"
|
|
@menu-click="showDrawer = true"
|
|
@action-click="handleTopBarAction"
|
|
/>
|
|
|
|
<!-- Mobile Drawer Menu (replaces old Sidebar) -->
|
|
<MobileDrawerMenu
|
|
v-model="showDrawer"
|
|
:user="authStore.user"
|
|
@logout="handleLogout"
|
|
/>
|
|
|
|
<!-- US-107: Filter BottomSheet for mobile -->
|
|
<BottomSheet v-model="showFilters">
|
|
<h3 class="bottom-sheet-title">Filtre</h3>
|
|
<div class="bottom-sheet-filters">
|
|
<!-- Invoice Type -->
|
|
<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>
|
|
|
|
<!-- Payment Status -->
|
|
<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>
|
|
|
|
<!-- Search -->
|
|
<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>
|
|
|
|
<!-- Cont Filter -->
|
|
<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>
|
|
|
|
<!-- Bottom sheet actions -->
|
|
<div class="bottom-sheet-actions">
|
|
<Button
|
|
icon="pi pi-filter-slash"
|
|
label="Resetează"
|
|
class="p-button-outlined p-button-secondary"
|
|
@click="clearFilters; showFilters = false"
|
|
/>
|
|
<Button
|
|
icon="pi pi-check"
|
|
label="Aplică"
|
|
@click="showFilters = false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</BottomSheet>
|
|
|
|
<div class="invoices">
|
|
<!-- Header Section - Desktop only -->
|
|
<div class="page-header" v-if="!isMobile">
|
|
<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>
|
|
|
|
<!-- US-107: Mobile Totals Summary (actions moved to MobileTopBar) -->
|
|
<div v-if="isMobile && companyStore.selectedCompany && invoicesStore.hasInvoices" class="mobile-totals-bar">
|
|
<div class="mobile-totals-content">
|
|
<span class="total-label">Sold Total:</span>
|
|
<span class="total-value" :class="invoicesStore.totalSoldAll > 0 ? 'positive' : 'negative'">
|
|
{{ formatCompact(invoicesStore.totalSoldAll) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters and Controls - Desktop only (mobile uses BottomSheet) -->
|
|
<Card v-if="companyStore.selectedCompany && !isMobile" 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>
|
|
|
|
<!-- US-107/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 { useCompanyStore, useAuthStore } 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";
|
|
|
|
// US-107: 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 { useRouter } from "vue-router";
|
|
|
|
const toast = useToast();
|
|
const router = useRouter();
|
|
const companyStore = useCompanyStore();
|
|
const invoicesStore = useInvoicesStore();
|
|
const periodStore = useAccountingPeriodStore();
|
|
const authStore = useAuthStore();
|
|
|
|
// State
|
|
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
|
|
|
|
// Mobile state
|
|
const isMobile = ref(window.innerWidth < 768);
|
|
const showFilters = ref(false);
|
|
const showDrawer = ref(false);
|
|
const actionsMenu = ref(null);
|
|
|
|
// Handle logout from drawer menu
|
|
const handleLogout = async () => {
|
|
await authStore.logout();
|
|
router.push('/login');
|
|
};
|
|
|
|
// US-107: Mobile TopBar actions (refresh + export)
|
|
const mobileTopBarActions = computed(() => [
|
|
{
|
|
icon: "pi pi-filter",
|
|
label: "Filtre",
|
|
tooltip: "Filtre",
|
|
active: hasActiveFilters.value
|
|
},
|
|
{
|
|
icon: "pi pi-refresh",
|
|
label: "Actualizează",
|
|
tooltip: "Actualizează"
|
|
},
|
|
{
|
|
icon: "pi pi-download",
|
|
label: "Export",
|
|
tooltip: "Export Excel"
|
|
}
|
|
]);
|
|
|
|
// US-107: Handle top bar action clicks
|
|
const handleTopBarAction = (action) => {
|
|
if (action.icon === "pi pi-filter") {
|
|
showFilters.value = !showFilters.value;
|
|
} else if (action.icon === "pi pi-refresh") {
|
|
refreshData();
|
|
} else if (action.icon === "pi pi-download") {
|
|
exportExcel();
|
|
}
|
|
};
|
|
|
|
// 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
|
|
}
|
|
};
|
|
|
|
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>
|
|
/* ================================================
|
|
US-107: InvoicesView Mobile Material Design Styles
|
|
================================================ */
|
|
|
|
.invoices {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: var(--space-xl);
|
|
}
|
|
|
|
/* Mobile layout adjustments for top/bottom bars */
|
|
.mobile-layout .invoices {
|
|
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);
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
.page-title {
|
|
font-size: var(--text-4xl);
|
|
font-weight: var(--font-bold);
|
|
color: var(--text-color);
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.company-selection-card,
|
|
.filters-card {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
.search-col {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
.filters-actions {
|
|
display: flex;
|
|
gap: var(--space-md);
|
|
justify-content: flex-end;
|
|
padding-top: var(--space-md);
|
|
border-top: 1px solid var(--surface-border);
|
|
}
|
|
|
|
.table-card {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
.no-data,
|
|
.loading-table {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-3xl);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.no-data i,
|
|
.loading-table i {
|
|
font-size: var(--text-3xl);
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
|
|
.text-right {
|
|
text-align: right;
|
|
width: 100%;
|
|
display: block;
|
|
}
|
|
|
|
.text-center {
|
|
text-align: center;
|
|
width: 100%;
|
|
display: block;
|
|
}
|
|
|
|
/* ================================================
|
|
US-107: Mobile Totals Bar
|
|
================================================ */
|
|
|
|
.mobile-totals-bar {
|
|
background: var(--surface-card);
|
|
border-bottom: 1px solid var(--surface-border);
|
|
padding: var(--space-sm) var(--space-md);
|
|
margin-bottom: var(--space-md);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.mobile-totals-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.mobile-totals-bar .total-label {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-color-secondary);
|
|
font-weight: var(--font-medium);
|
|
}
|
|
|
|
.mobile-totals-bar .total-value {
|
|
font-size: var(--text-lg);
|
|
font-weight: var(--font-bold);
|
|
}
|
|
|
|
.mobile-totals-bar .total-value.positive {
|
|
color: var(--green-600);
|
|
}
|
|
|
|
.mobile-totals-bar .total-value.negative {
|
|
color: var(--red-600);
|
|
}
|
|
|
|
/* ================================================
|
|
US-107: Mobile Card List (Invoice 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-amount {
|
|
font-weight: var(--font-semibold);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.mobile-data-card .card-amount.positive {
|
|
color: var(--green-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-107: 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-107: 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;
|
|
}
|
|
|
|
/* ================================================
|
|
Summary Stats
|
|
================================================ */
|
|
|
|
.summary-stats-inline {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-bottom: var(--space-md);
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: var(--text-lg);
|
|
font-weight: var(--font-bold);
|
|
}
|
|
|
|
.stat-value.plati {
|
|
color: var(--red-600);
|
|
}
|
|
|
|
.stat-value.incasari {
|
|
color: var(--green-600);
|
|
}
|
|
|
|
/* ================================================
|
|
Dark Mode Support
|
|
================================================ */
|
|
|
|
[data-theme="dark"] .mobile-totals-bar .total-value.positive {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
[data-theme="dark"] .mobile-totals-bar .total-value.negative {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
[data-theme="dark"] .mobile-data-card .card-amount.positive {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
[data-theme="dark"] .sidebar-item.active {
|
|
background: var(--blue-900);
|
|
color: var(--blue-400);
|
|
}
|
|
|
|
[data-theme="dark"] .stat-value.plati {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
[data-theme="dark"] .stat-value.incasari {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
/* Auto dark mode */
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme]) .mobile-totals-bar .total-value.positive {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .mobile-totals-bar .total-value.negative {
|
|
color: var(--red-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .mobile-data-card .card-amount.positive {
|
|
color: var(--green-400);
|
|
}
|
|
|
|
:root:not([data-theme]) .sidebar-item.active {
|
|
background: var(--blue-900);
|
|
color: var(--blue-400);
|
|
}
|
|
}
|
|
|
|
/* ================================================
|
|
Responsive Design
|
|
================================================ */
|
|
|
|
@media (max-width: 768px) {
|
|
.invoices {
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.page-title {
|
|
font-size: var(--text-3xl);
|
|
}
|
|
|
|
.search-col {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.filters-actions {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|