Files
roa2web-service-auto/src/modules/reports/views/DetailedInvoicesView.vue
Claude Agent 701cabb8a4 feat(mobile-navigation-improvements): Complete US-205 - Creare Pagină Facturi Detaliate
Implemented by Ralph autonomous loop.
Iteration: 6

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 12:21:23 +00:00

1302 lines
34 KiB
Vue

<template>
<!-- Mobile Top Bar -->
<MobileTopBar
v-if="isMobile"
title="Facturi Detaliate"
:show-back="true"
:actions="topBarActions"
@back-click="goBack"
@action-click="handleTopBarAction"
/>
<main class="main-content" :class="{ 'mobile-layout': isMobile }">
<div class="app-container">
<!-- Page Header - only on desktop -->
<div v-if="!isMobile" class="page-header">
<h1 class="page-title">Facturi Detaliate</h1>
<p class="page-subtitle">Vizualizare detaliată a facturilor clienți și furnizori</p>
</div>
<!-- Loading state when no company selected -->
<div v-if="!companyStore.selectedCompany" class="empty-state">
<i class="pi pi-building empty-icon"></i>
<h2 class="empty-title">Selectați o companie</h2>
<p class="empty-description">
Pentru a vizualiza facturile detaliate, rugăm selectați o companie din Dashboard.
</p>
<Button
label="Mergi la Dashboard"
icon="pi pi-arrow-left"
@click="goBack"
class="empty-action"
/>
</div>
<!-- Main content -->
<div v-else class="invoices-container">
<!-- Desktop Filters Card -->
<div v-if="!isMobile" class="filters-card">
<div class="filters-row">
<div class="filter-group">
<label class="form-label">Tip</label>
<Dropdown
v-model="selectedType"
:options="typeOptions"
optionLabel="label"
optionValue="value"
placeholder="Selectați tipul"
class="w-full"
@change="loadDetailedData"
/>
</div>
<div class="filter-group">
<label class="form-label">Căutare</label>
<InputText
v-model="searchTerm"
placeholder="Căutare..."
class="w-full"
@input="handleSearch"
/>
</div>
<div class="filter-group">
<label class="form-label">Perioadă</label>
<Dropdown
v-model="selectedPeriod"
:options="periodOptions"
optionLabel="label"
optionValue="value"
placeholder="Selectați perioada"
class="w-full"
@change="loadDetailedData"
/>
</div>
</div>
<div class="filters-actions">
<Button
icon="pi pi-filter-slash"
label="Resetează"
severity="secondary"
outlined
@click="resetFilters"
/>
<Button
icon="pi pi-file-excel"
label="Export Excel"
severity="success"
outlined
@click="exportExcel"
:disabled="filteredData.length === 0"
/>
<Button
icon="pi pi-file-pdf"
label="Export PDF"
severity="danger"
outlined
@click="exportPDF"
:disabled="filteredData.length === 0"
/>
<Button
icon="pi pi-refresh"
label="Actualizează"
:loading="isLoading"
@click="loadDetailedData"
/>
</div>
</div>
<!-- Mobile Filter Chips (compact summary) -->
<div v-if="isMobile" class="mobile-filter-summary">
<div class="filter-chip" @click="openFilters">
<i class="pi pi-list"></i>
<span>{{ getTypeLabel(selectedType) }}</span>
</div>
<div v-if="searchTerm" class="filter-chip active" @click="openFilters">
<i class="pi pi-search"></i>
<span>{{ searchTerm }}</span>
<i class="pi pi-times" @click.stop="clearSearch"></i>
</div>
<div class="filter-chip" @click="openFilters">
<i class="pi pi-calendar"></i>
<span>{{ getPeriodLabel(selectedPeriod) }}</span>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner"></div>
<p>Se încarcă facturile...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="error-state">
<i class="pi pi-exclamation-circle"></i>
<p>{{ error }}</p>
<Button label="Încearcă din nou" icon="pi pi-refresh" @click="loadDetailedData" />
</div>
<!-- Data Table / Cards -->
<div v-else class="data-section">
<!-- Desktop Table -->
<div v-if="!isMobile" class="table-wrapper">
<DataTable
:value="selectedType === 'treasury' ? paginatedData : paginatedGroups"
:loading="isLoading"
stripedRows
class="p-datatable-sm"
>
<!-- Treasury columns -->
<template v-if="selectedType === 'treasury'">
<Column field="cont" header="Cont" sortable></Column>
<Column field="nume_cont" header="Nume Cont" sortable></Column>
<Column field="sold" header="Sold" sortable>
<template #body="slotProps">
<span class="font-mono">{{ formatCurrency(slotProps.data.sold) }}</span>
</template>
</Column>
<Column field="valuta" header="Valută" sortable></Column>
<Column field="tip" header="Tip" sortable></Column>
</template>
<!-- Clients/Suppliers columns -->
<template v-else>
<Column :field="selectedType === 'clients' ? 'client' : 'furnizor'" :header="selectedType === 'clients' ? 'Client' : 'Furnizor'" sortable>
<template #body="slotProps">
<div class="name-cell">
<strong>{{ slotProps.data.name }}</strong>
<span v-if="slotProps.data.facturi?.length > 1" class="count-badge">
({{ slotProps.data.facturi.length }})
</span>
</div>
</template>
</Column>
<Column field="numar_document" header="Nr. Document">
<template #body="slotProps">
{{ slotProps.data.facturi?.length === 1 ? slotProps.data.facturi[0].numar_document : '-' }}
</template>
</Column>
<Column field="data_document" header="Data Document">
<template #body="slotProps">
{{ slotProps.data.facturi?.length === 1 ? formatDate(slotProps.data.facturi[0].data_document) : '-' }}
</template>
</Column>
<Column field="data_scadenta" header="Data Scadență">
<template #body="slotProps">
{{ slotProps.data.facturi?.length === 1 ? formatDate(slotProps.data.facturi[0].data_scadenta) : '-' }}
</template>
</Column>
<Column header="Facturat">
<template #body="slotProps">
<span class="font-mono">
{{ slotProps.data.facturi?.length === 1 ? formatCurrency(slotProps.data.facturi[0].facturat) : '-' }}
</span>
</template>
</Column>
<Column :header="selectedType === 'clients' ? 'Încasat' : 'Achitat'">
<template #body="slotProps">
<span class="font-mono">
{{ slotProps.data.facturi?.length === 1
? formatCurrency(slotProps.data.facturi[0][selectedType === 'clients' ? 'incasat' : 'achitat'])
: '-' }}
</span>
</template>
</Column>
<Column field="totalSold" header="Sold" sortable>
<template #body="slotProps">
<span
class="font-mono font-bold"
:class="{ 'sold-restant': slotProps.data.hasRestant }"
>
{{ formatCurrency(slotProps.data.totalSold) }}
</span>
</template>
</Column>
</template>
</DataTable>
</div>
<!-- Mobile Cards -->
<div v-if="isMobile" class="mobile-cards">
<!-- Treasury Cards -->
<template v-if="selectedType === 'treasury'">
<div
v-for="row in paginatedData"
:key="row.id"
class="invoice-card"
>
<div class="card-header-row">
<strong>{{ row.cont }}</strong>
<span class="card-badge">{{ row.valuta }}</span>
</div>
<div class="card-body">
<div class="card-field">
<span class="field-label">Nume Cont</span>
<span class="field-value">{{ row.nume_cont }}</span>
</div>
<div class="card-field highlight">
<span class="field-label">Sold</span>
<span class="field-value sold-value">{{ formatCurrency(row.sold) }}</span>
</div>
</div>
</div>
</template>
<!-- Client/Supplier Cards -->
<template v-else>
<div
v-for="group in paginatedGroups"
:key="group.name"
class="invoice-card"
:class="{ 'has-multiple': group.facturi.length > 1 }"
@click="group.facturi.length > 1 && toggleGroup(group.name)"
>
<div class="card-header-row">
<div class="header-left">
<strong>{{ group.name }}</strong>
<span v-if="group.facturi.length > 1" class="count-badge">
({{ group.facturi.length }})
</span>
</div>
<div class="header-right">
<span
class="sold-value"
:class="{ 'sold-restant': group.hasRestant }"
>
{{ formatCurrency(group.totalSold) }}
</span>
<i
v-if="group.facturi.length > 1"
:class="['pi', isGroupExpanded(group.name) ? 'pi-chevron-up' : 'pi-chevron-down']"
></i>
</div>
</div>
<!-- Single invoice details -->
<div v-if="group.facturi.length === 1" class="card-body">
<div class="card-field">
<span class="field-label">Nr. Document</span>
<span class="field-value">{{ group.facturi[0].numar_document }}</span>
</div>
<div class="card-row-inline">
<div class="card-field">
<span class="field-label">Data Doc.</span>
<span class="field-value">{{ formatDate(group.facturi[0].data_document) }}</span>
</div>
<div class="card-field">
<span class="field-label">Scadență</span>
<span class="field-value">{{ formatDate(group.facturi[0].data_scadenta) }}</span>
</div>
</div>
<div class="card-status" :class="getStatusClass(group.facturi[0].status)">
{{ group.facturi[0].status }}
</div>
</div>
<!-- Multiple invoices expanded -->
<div v-if="group.facturi.length > 1 && isGroupExpanded(group.name)" class="card-sub-items">
<div
v-for="(factura, idx) in group.facturi"
:key="`${group.name}-${idx}`"
class="sub-item"
>
<div class="sub-item-header">
<span>{{ factura.numar_document }}</span>
<span
class="sub-item-sold"
:class="{ 'sold-restant': factura.status === 'Restant' }"
>
{{ formatCurrency(factura.sold) }}
</span>
</div>
<div class="sub-item-dates">
<span>{{ formatDate(factura.data_scadenta) }}</span>
<span :class="getStatusClass(factura.status)">{{ factura.status }}</span>
</div>
</div>
</div>
</div>
</template>
<!-- Empty state -->
<div v-if="(selectedType === 'treasury' ? paginatedData : paginatedGroups).length === 0" class="empty-data">
<i class="pi pi-inbox"></i>
<p>Nu există facturi pentru criteriile selectate</p>
</div>
</div>
<!-- Pagination -->
<div class="pagination-wrapper">
<Paginator
v-model:rows="rowsPerPage"
:totalRecords="totalRecords"
v-model:first="firstRow"
:rowsPerPageOptions="[10, 25, 50, 100]"
@page="handlePageChange"
/>
</div>
<!-- Totals Summary -->
<div v-if="filteredData.length > 0" class="totals-summary">
<div class="total-item">
<span class="total-label">Total Sold:</span>
<span class="total-value">{{ formatCurrency(calculateTotal('sold')) }}</span>
</div>
<div v-if="selectedType !== 'treasury'" class="total-item">
<span class="total-label">Total Facturat:</span>
<span class="total-value">{{ formatCurrency(calculateTotal('facturat')) }}</span>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Mobile Bottom Nav -->
<MobileBottomNav v-if="isMobile" :items="mobileNavItems" />
<!-- Mobile Filters BottomSheet -->
<BottomSheet v-model="isFilterSheetOpen">
<div class="filter-sheet-content">
<h3 class="filter-sheet-title">Filtre</h3>
<div class="filter-sheet-group">
<label class="form-label">Tip</label>
<Dropdown
v-model="selectedType"
:options="typeOptions"
optionLabel="label"
optionValue="value"
placeholder="Selectați tipul"
class="w-full"
/>
</div>
<div class="filter-sheet-group">
<label class="form-label">Căutare</label>
<InputText
v-model="searchTerm"
placeholder="Căutare client/furnizor..."
class="w-full"
/>
</div>
<div class="filter-sheet-group">
<label class="form-label">Perioadă</label>
<Dropdown
v-model="selectedPeriod"
:options="periodOptions"
optionLabel="label"
optionValue="value"
placeholder="Selectați perioada"
class="w-full"
/>
</div>
<div class="filter-sheet-actions">
<Button
label="Resetează"
icon="pi pi-filter-slash"
severity="secondary"
outlined
class="flex-1"
@click="resetFilters"
/>
<Button
label="Aplică"
icon="pi pi-check"
class="flex-1"
@click="applyFilters"
/>
</div>
</div>
</BottomSheet>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import Button from 'primevue/button'
import Dropdown from 'primevue/dropdown'
import InputText from 'primevue/inputtext'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Paginator from 'primevue/paginator'
import { useToast } from 'primevue/usetoast'
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 { useDashboardStore } from '@reports/stores/dashboard'
import { useCompanyStore, useAccountingPeriodStore } from '@reports/stores/sharedStores'
import * as XLSX from 'xlsx'
import jsPDF from 'jspdf'
import 'jspdf-autotable'
const router = useRouter()
const toast = useToast()
const dashboardStore = useDashboardStore()
const companyStore = useCompanyStore()
const periodStore = useAccountingPeriodStore()
// Mobile detection
const windowWidth = ref(window.innerWidth)
const isMobile = computed(() => windowWidth.value < 768)
const handleResize = () => {
windowWidth.value = window.innerWidth
}
// State
const isLoading = ref(false)
const error = ref(null)
const selectedType = ref('clients')
const searchTerm = ref('')
const selectedPeriod = ref('all')
const detailedData = ref([])
const firstRow = ref(0)
const rowsPerPage = ref(25)
const expandedGroups = ref(new Set())
const isFilterSheetOpen = ref(false)
// Options
const typeOptions = [
{ label: 'Clienți', value: 'clients' },
{ label: 'Furnizori', value: 'suppliers' },
{ label: 'Trezorerie', value: 'treasury' }
]
const periodOptions = [
{ label: 'Toate', value: 'all' },
{ label: '7 zile', value: '7d' },
{ label: '1 lună', value: '1m' },
{ label: '3 luni', value: '3m' },
{ label: '6 luni', value: '6m' },
{ label: '12 luni', value: '12m' }
]
// Top bar actions for mobile
const topBarActions = computed(() => [
{
icon: 'pi pi-filter',
label: 'Filtre',
tooltip: 'Deschide filtre',
active: searchTerm.value || selectedPeriod.value !== 'all'
}
])
// Mobile navigation items
const mobileNavItems = computed(() => [
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri' },
{ icon: 'pi pi-cloud-upload', label: 'Upload' },
{ to: '/reports/dashboard', icon: 'pi pi-chart-bar', label: 'Rapoarte', active: true },
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' }
])
// Currency formatter
const formatCurrency = (value) => {
if (value === null || value === undefined) return '0 RON'
return new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(Math.abs(value))
}
// Date formatter
const formatDate = (value) => {
if (!value) return '-'
const date = new Date(value)
if (isNaN(date.getTime())) return value
return date.toLocaleDateString('ro-RO', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
// Computed - Filtered and grouped data
const filteredData = computed(() => {
if (!searchTerm.value) return detailedData.value
return detailedData.value.filter((row) => {
return Object.values(row).some((val) =>
String(val).toLowerCase().includes(searchTerm.value.toLowerCase())
)
})
})
const groupedData = computed(() => {
if (selectedType.value === 'treasury') return []
const groups = {}
const nameField = selectedType.value === 'clients' ? 'client' : 'furnizor'
filteredData.value.forEach((row) => {
const name = row[nameField]
if (!name) return
if (!groups[name]) {
groups[name] = {
name: name,
facturi: [],
totalSold: 0,
hasRestant: false
}
}
groups[name].facturi.push(row)
groups[name].totalSold += row.sold || 0
if (row.status === 'Restant') {
groups[name].hasRestant = true
}
})
return Object.values(groups)
})
const paginatedGroups = computed(() => {
if (selectedType.value === 'treasury') return []
return groupedData.value
})
const paginatedData = computed(() => {
if (selectedType.value !== 'treasury') return []
return filteredData.value
})
const totalRecords = computed(() => {
return dashboardStore.detailedDataTotal || groupedData.value.length
})
// Helper functions
const getTypeLabel = (type) => {
const option = typeOptions.find(o => o.value === type)
return option?.label || type
}
const getPeriodLabel = (period) => {
const option = periodOptions.find(o => o.value === period)
return option?.label || period
}
const getStatusClass = (status) => {
if (status === 'Restant') return 'status-restant'
return 'status-ok'
}
const toggleGroup = (groupName) => {
if (expandedGroups.value.has(groupName)) {
expandedGroups.value.delete(groupName)
} else {
expandedGroups.value.add(groupName)
}
}
const isGroupExpanded = (groupName) => {
return expandedGroups.value.has(groupName)
}
const calculateTotal = (field) => {
return filteredData.value.reduce((sum, row) => sum + (row[field] || 0), 0)
}
// Navigation
const goBack = () => {
router.push('/reports/dashboard')
}
// Top bar action handler
const handleTopBarAction = (action) => {
if (action.icon === 'pi pi-filter') {
openFilters()
}
}
// Filter functions
const openFilters = () => {
isFilterSheetOpen.value = true
}
const clearSearch = () => {
searchTerm.value = ''
handleSearch()
}
const handleSearch = () => {
firstRow.value = 0
expandedGroups.value.clear()
}
const resetFilters = () => {
selectedType.value = 'clients'
searchTerm.value = ''
selectedPeriod.value = 'all'
firstRow.value = 0
expandedGroups.value.clear()
loadDetailedData()
}
const applyFilters = () => {
isFilterSheetOpen.value = false
firstRow.value = 0
expandedGroups.value.clear()
loadDetailedData()
}
const handlePageChange = () => {
loadDetailedData()
}
// Data loading
const loadDetailedData = async () => {
if (!companyStore.selectedCompany) {
return
}
isLoading.value = true
error.value = null
try {
const page = Math.floor(firstRow.value / rowsPerPage.value) + 1
const luna = periodStore.selectedPeriod?.luna || null
const an = periodStore.selectedPeriod?.an || null
const response = await dashboardStore.loadDetailedData(
selectedType.value,
companyStore.selectedCompany.id_firma,
page,
rowsPerPage.value,
searchTerm.value,
luna,
an
)
detailedData.value = response.data
expandedGroups.value.clear()
} catch (err) {
console.error('Failed to load detailed data:', err)
error.value = err.message || 'Nu s-au putut încărca datele'
toast.add({
severity: 'error',
summary: 'Eroare',
detail: 'Nu s-au putut încărca facturile detaliate',
life: 3000
})
} finally {
isLoading.value = false
}
}
// Export functions
const exportExcel = () => {
const ws = XLSX.utils.json_to_sheet(filteredData.value)
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, selectedType.value)
XLSX.writeFile(wb, `facturi_${selectedType.value}_${new Date().toISOString().split('T')[0]}.xlsx`)
toast.add({
severity: 'success',
summary: 'Export reușit',
detail: `${filteredData.value.length} înregistrări exportate`,
life: 3000
})
}
const exportPDF = () => {
const doc = new jsPDF()
const columns = selectedType.value === 'treasury'
? ['Cont', 'Nume Cont', 'Sold', 'Valută', 'Tip']
: selectedType.value === 'clients'
? ['Client', 'Nr. Document', 'Data Doc.', 'Scadență', 'Facturat', 'Încasat', 'Sold']
: ['Furnizor', 'Nr. Document', 'Data Doc.', 'Scadență', 'Facturat', 'Achitat', 'Sold']
const rows = filteredData.value.map((row) => {
if (selectedType.value === 'treasury') {
return [row.cont, row.nume_cont, formatCurrency(row.sold), row.valuta, row.tip]
}
const nameField = selectedType.value === 'clients' ? 'client' : 'furnizor'
const paidField = selectedType.value === 'clients' ? 'incasat' : 'achitat'
return [
row[nameField],
row.numar_document,
formatDate(row.data_document),
formatDate(row.data_scadenta),
formatCurrency(row.facturat),
formatCurrency(row[paidField]),
formatCurrency(row.sold)
]
})
doc.autoTable({
head: [columns],
body: rows,
theme: 'grid',
styles: { fontSize: 8 },
headStyles: { fillColor: [59, 130, 246] }
})
doc.save(`facturi_${selectedType.value}_${new Date().toISOString().split('T')[0]}.pdf`)
toast.add({
severity: 'success',
summary: 'Export reușit',
detail: `PDF generat cu ${filteredData.value.length} înregistrări`,
life: 3000
})
}
// Watchers
watch(
() => companyStore.selectedCompany,
(newCompany) => {
if (newCompany) {
loadDetailedData()
}
}
)
watch(
() => periodStore.selectedPeriod,
(newPeriod, oldPeriod) => {
if (newPeriod && (newPeriod.luna !== oldPeriod?.luna || newPeriod.an !== oldPeriod?.an)) {
loadDetailedData()
}
},
{ deep: true }
)
// Lifecycle
onMounted(() => {
window.addEventListener('resize', handleResize)
if (companyStore.selectedCompany) {
loadDetailedData()
}
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
/* Mobile Layout - Padding for fixed top/bottom bars */
.main-content.mobile-layout {
padding-top: 56px;
padding-bottom: 56px;
}
/* App container */
.app-container {
max-width: 1400px;
margin: 0 auto;
padding: var(--space-lg);
}
@media (max-width: 768px) {
.app-container {
padding: var(--space-md);
}
}
/* Page Header - Desktop only */
.page-header {
margin-bottom: var(--space-xl);
}
.page-title {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
color: var(--text-color);
margin: 0 0 var(--space-xs) 0;
}
.page-subtitle {
font-size: var(--text-base);
color: var(--text-color-secondary);
margin: 0;
}
/* Invoices container */
.invoices-container {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
/* Filters Card - Desktop */
.filters-card {
background: var(--surface-card);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
padding: var(--space-lg);
}
.filters-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-md);
margin-bottom: var(--space-md);
}
.filter-group {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.form-label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-color-secondary);
}
.filters-actions {
display: flex;
gap: var(--space-sm);
justify-content: flex-end;
padding-top: var(--space-md);
border-top: 1px solid var(--surface-border);
}
/* Mobile Filter Summary */
.mobile-filter-summary {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
padding: var(--space-sm) 0;
}
.filter-chip {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
background: var(--surface-hover);
border-radius: var(--radius-full);
font-size: var(--text-sm);
color: var(--text-color);
cursor: pointer;
transition: background var(--transition-fast);
}
.filter-chip:active {
background: var(--surface-border);
}
.filter-chip.active {
background: var(--primary-100);
color: var(--color-primary);
}
.filter-chip .pi-times {
font-size: 0.75rem;
padding: 2px;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--surface-border);
border-top: 3px solid var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: var(--space-md);
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Error State */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
gap: var(--space-md);
}
.error-state .pi {
font-size: 48px;
color: var(--color-error);
}
/* Empty state styles */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
text-align: center;
padding: var(--space-xl);
background: var(--surface-card);
border-radius: var(--radius-lg);
border: 1px solid var(--surface-border);
}
.empty-icon {
font-size: 64px;
color: var(--text-color-secondary);
margin-bottom: var(--space-lg);
opacity: 0.5;
}
.empty-title {
font-size: var(--text-xl);
font-weight: var(--font-semibold);
color: var(--text-color);
margin: 0 0 var(--space-sm) 0;
}
.empty-description {
font-size: var(--text-base);
color: var(--text-color-secondary);
margin: 0 0 var(--space-lg) 0;
max-width: 400px;
}
.empty-action {
margin-top: var(--space-md);
}
/* Data Section */
.data-section {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
/* Table wrapper */
.table-wrapper {
background: var(--surface-card);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
overflow: hidden;
}
.font-mono {
font-family: var(--font-mono);
}
.font-bold {
font-weight: var(--font-bold);
}
.name-cell {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.count-badge {
font-size: var(--text-xs);
color: var(--text-color-secondary);
font-weight: var(--font-normal);
}
.sold-restant {
color: var(--color-error) !important;
}
/* Mobile Cards */
.mobile-cards {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.invoice-card {
background: var(--surface-card);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
overflow: hidden;
}
.invoice-card.has-multiple {
cursor: pointer;
}
.card-header-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-md);
background: var(--surface-hover);
border-bottom: 1px solid var(--surface-border);
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.header-right {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.card-badge {
font-size: var(--text-xs);
padding: 2px var(--space-xs);
background: var(--primary-100);
color: var(--color-primary);
border-radius: var(--radius-sm);
}
.card-body {
padding: var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.card-field {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-field.highlight {
padding-top: var(--space-sm);
border-top: 1px solid var(--surface-border);
margin-top: var(--space-xs);
}
.card-row-inline {
display: flex;
gap: var(--space-md);
}
.card-row-inline .card-field {
flex: 1;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.field-label {
font-size: var(--text-xs);
color: var(--text-color-secondary);
}
.field-value {
font-size: var(--text-sm);
color: var(--text-color);
font-weight: var(--font-medium);
}
.sold-value {
font-family: var(--font-mono);
font-weight: var(--font-bold);
color: var(--text-color);
}
.card-status {
display: inline-flex;
align-self: flex-start;
font-size: var(--text-xs);
padding: 2px var(--space-sm);
border-radius: var(--radius-full);
font-weight: var(--font-medium);
}
.status-ok {
background: var(--green-100);
color: var(--green-600);
}
.status-restant {
background: var(--red-100);
color: var(--red-600);
}
/* Sub items for expanded groups */
.card-sub-items {
border-top: 1px solid var(--surface-border);
padding: var(--space-sm) var(--space-md) var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.sub-item {
padding: var(--space-sm);
background: var(--surface-hover);
border-radius: var(--radius-sm);
}
.sub-item-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--text-sm);
font-weight: var(--font-medium);
margin-bottom: 4px;
}
.sub-item-sold {
font-family: var(--font-mono);
font-weight: var(--font-bold);
}
.sub-item-dates {
display: flex;
justify-content: space-between;
font-size: var(--text-xs);
color: var(--text-color-secondary);
}
/* Empty data state */
.empty-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
color: var(--text-color-secondary);
}
.empty-data .pi {
font-size: 48px;
margin-bottom: var(--space-md);
opacity: 0.5;
}
/* Pagination */
.pagination-wrapper {
display: flex;
justify-content: center;
}
/* Totals Summary */
.totals-summary {
display: flex;
gap: var(--space-lg);
padding: var(--space-md);
background: var(--surface-card);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
}
@media (max-width: 768px) {
.totals-summary {
flex-direction: column;
gap: var(--space-sm);
}
}
.total-item {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.total-label {
font-size: var(--text-sm);
color: var(--text-color-secondary);
}
.total-value {
font-size: var(--text-lg);
font-weight: var(--font-bold);
font-family: var(--font-mono);
color: var(--color-primary);
}
/* Filter Sheet Content */
.filter-sheet-content {
padding: var(--space-sm) 0;
}
.filter-sheet-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-color);
margin: 0 0 var(--space-lg) 0;
}
.filter-sheet-group {
margin-bottom: var(--space-md);
}
.filter-sheet-group .form-label {
display: block;
margin-bottom: var(--space-xs);
}
.filter-sheet-actions {
display: flex;
gap: var(--space-md);
margin-top: var(--space-lg);
padding-top: var(--space-md);
border-top: 1px solid var(--surface-border);
}
.flex-1 {
flex: 1;
}
/* Dark mode support */
[data-theme="dark"] .filter-chip {
background: var(--surface-100);
}
[data-theme="dark"] .filter-chip.active {
background: var(--primary-800);
}
[data-theme="dark"] .card-badge {
background: var(--primary-800);
}
[data-theme="dark"] .status-ok {
background: var(--green-900);
color: var(--green-300);
}
[data-theme="dark"] .status-restant {
background: var(--red-900);
color: var(--red-300);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .filter-chip {
background: var(--surface-100);
}
:root:not([data-theme]) .filter-chip.active {
background: var(--primary-800);
}
:root:not([data-theme]) .card-badge {
background: var(--primary-800);
}
:root:not([data-theme]) .status-ok {
background: var(--green-900);
color: var(--green-300);
}
:root:not([data-theme]) .status-restant {
background: var(--red-900);
color: var(--red-300);
}
}
</style>