Files
roa2web-service-auto/src/modules/reports/views/InvoicesView.vue
Claude Agent a5740eaf78 feat(mobile-fixes-phase3): Complete US-307 - Restructurare Footer Nav (4 butoane noi)
Implemented by Ralph autonomous loop.
Iteration: 1

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 13:38:58 +00:00

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>