Files
roa2web-service-auto/src/modules/reports/views/DetailedInvoicesView.vue
Claude Agent 0868fc4ea3 feat(ui-fixes-phase6): Complete US-705 - Reducere Padding și Margin Top Excesiv
Implemented by Ralph autonomous loop.
Iteration: 5

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-13 22:39:06 +00:00

1811 lines
48 KiB
Vue

<template>
<!-- US-703: Mobile Top Bar with Hamburger Menu (not back button) -->
<MobileTopBar
v-if="isMobile"
title="Facturi Detaliate"
:show-menu="true"
:actions="topBarActions"
@menu-click="showDrawer = true"
@action-click="handleTopBarAction"
/>
<!-- US-703: Mobile Drawer Menu (replaces navigation via back button) -->
<MobileDrawerMenu
v-model="showDrawer"
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
@logout="handleLogout"
@company-changed="handleCompanyChanged"
@period-changed="handlePeriodChanged"
/>
<!-- US-603: Mobile Tabs for Clienți/Furnizori -->
<div v-if="isMobile" class="mobile-tabs-container">
<div class="mobile-tabs">
<button
class="mobile-tab"
:class="{ active: activeTab === 'clients' }"
@click="switchTab('clients')"
>
<span class="tab-label">Clienți</span>
</button>
<button
class="mobile-tab"
:class="{ active: activeTab === 'suppliers' }"
@click="switchTab('suppliers')"
>
<span class="tab-label">Furnizori</span>
</button>
</div>
</div>
<!-- US-501: Export Menu (for mobile export dropdown) -->
<Menu ref="exportMenu" :model="exportMenuItems" :popup="true" />
<main class="main-content" :class="{ 'mobile-layout': isMobile, 'has-tabs': isMobile }">
<div class="app-container">
<!-- Page Header - only on desktop -->
<!-- US-603: Desktop header with tabs -->
<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>
<!-- US-603: Desktop Tabs for Clienți/Furnizori -->
<div class="desktop-tabs">
<button
class="desktop-tab"
:class="{ active: activeTab === 'clients' }"
@click="switchTab('clients')"
>
<i class="pi pi-users"></i>
<span>Clienți</span>
</button>
<button
class="desktop-tab"
:class="{ active: activeTab === 'suppliers' }"
@click="switchTab('suppliers')"
>
<i class="pi pi-truck"></i>
<span>Furnizori</span>
</button>
</div>
</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 -->
<!-- US-510: Removed type dropdown - type is determined by route -->
<div v-if="!isMobile" class="filters-card">
<div class="filters-row">
<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>
<!-- US-501: Desktop Action buttons with Export dropdown -->
<div class="filters-actions">
<Button
icon="pi pi-filter-slash"
label="Resetează"
severity="secondary"
outlined
@click="resetFilters"
/>
<SplitButton
label="Export"
icon="pi pi-download"
:model="desktopExportItems"
@click="exportPDF"
outlined
:disabled="filteredData.length === 0"
/>
<Button
icon="pi pi-refresh"
label="Actualizează"
:loading="isLoading"
@click="loadDetailedData"
/>
</div>
</div>
<!-- US-511: Mobile Filter Chips - display-only (filter action is in MobileTopBar) -->
<div v-if="isMobile && hasActiveFilters" class="mobile-filter-summary">
<div v-if="searchTerm" class="filter-chip active">
<i class="pi pi-search"></i>
<span>{{ searchTerm }}</span>
<i class="pi pi-times" @click="clearSearch"></i>
</div>
<div v-if="selectedPeriod !== 'all'" class="filter-chip active">
<i class="pi pi-calendar"></i>
<span>{{ getPeriodLabel(selectedPeriod) }}</span>
<i class="pi pi-times" @click="clearPeriod"></i>
</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">
<!-- Treasury DataTable (no expansion needed) -->
<DataTable
v-if="invoiceType === 'treasury'"
:value="paginatedData"
:loading="isLoading"
stripedRows
class="p-datatable-sm"
>
<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>
</DataTable>
<!-- Clients/Suppliers Expandable Groups -->
<div v-else class="expandable-groups-table">
<!-- Table Header -->
<div class="groups-table-header">
<div class="header-cell expand-col"></div>
<div class="header-cell name-col">{{ invoiceType === 'clients' ? 'Client' : 'Furnizor' }}</div>
<div class="header-cell">Nr. Document</div>
<div class="header-cell">Data Document</div>
<div class="header-cell">Data Scadență</div>
<div class="header-cell text-right">Facturat</div>
<div class="header-cell text-right">{{ invoiceType === 'clients' ? 'Încasat' : 'Achitat' }}</div>
<div class="header-cell text-right">Sold</div>
</div>
<!-- Table Body - Groups -->
<div class="groups-table-body">
<template v-for="group in paginatedGroups" :key="group.name">
<!-- Group Header Row (clickable for expand) -->
<div
class="group-row"
:class="{
'expandable': group.facturi.length > 1,
'expanded': isGroupExpanded(group.name)
}"
@click="group.facturi.length > 1 && toggleGroup(group.name)"
>
<div class="row-cell expand-col">
<i
v-if="group.facturi.length > 1"
class="expand-icon pi pi-chevron-right"
:class="{ 'rotated': isGroupExpanded(group.name) }"
></i>
</div>
<div class="row-cell name-col">
<strong>{{ group.name }}</strong>
<span v-if="group.facturi.length > 1" class="count-badge">
({{ group.facturi.length }} facturi)
</span>
</div>
<div class="row-cell">
{{ group.facturi.length === 1 ? group.facturi[0].numar_document : '-' }}
</div>
<div class="row-cell">
{{ group.facturi.length === 1 ? formatDate(group.facturi[0].data_document) : '-' }}
</div>
<div class="row-cell">
{{ group.facturi.length === 1 ? formatDate(group.facturi[0].data_scadenta) : '-' }}
</div>
<div class="row-cell text-right">
<span class="font-mono">
{{ group.facturi.length === 1 ? formatCurrency(group.facturi[0].facturat) : '-' }}
</span>
</div>
<div class="row-cell text-right">
<span class="font-mono">
{{ group.facturi.length === 1
? formatCurrency(group.facturi[0][invoiceType === 'clients' ? 'incasat' : 'achitat'])
: '-' }}
</span>
</div>
<div class="row-cell text-right">
<span
class="font-mono font-bold"
:class="{ 'sold-restant': group.hasRestant }"
>
{{ formatCurrency(group.totalSold) }}
</span>
</div>
</div>
<!-- Expanded Invoice Rows -->
<Transition name="expand">
<div
v-if="group.facturi.length > 1 && isGroupExpanded(group.name)"
class="expanded-invoices"
>
<div
v-for="(factura, idx) in group.facturi"
:key="`${group.name}-invoice-${idx}`"
class="invoice-row"
>
<div class="row-cell expand-col"></div>
<div class="row-cell name-col sub-invoice-indicator">
<span class="invoice-connector"></span>
<span class="invoice-doc-label">Factura</span>
</div>
<div class="row-cell">{{ factura.numar_document }}</div>
<div class="row-cell">{{ formatDate(factura.data_document) }}</div>
<div class="row-cell">{{ formatDate(factura.data_scadenta) }}</div>
<div class="row-cell text-right">
<span class="font-mono">{{ formatCurrency(factura.facturat) }}</span>
</div>
<div class="row-cell text-right">
<span class="font-mono">
{{ formatCurrency(factura[invoiceType === 'clients' ? 'incasat' : 'achitat']) }}
</span>
</div>
<div class="row-cell text-right">
<span
class="font-mono"
:class="{ 'sold-restant': factura.status === 'Restant' }"
>
{{ formatCurrency(factura.sold) }}
</span>
</div>
</div>
</div>
</Transition>
</template>
<!-- Empty state -->
<div v-if="paginatedGroups.length === 0" class="empty-table-state">
<i class="pi pi-inbox"></i>
<p>Nu există facturi pentru criteriile selectate</p>
</div>
</div>
</div>
</div>
<!-- Mobile Cards -->
<div v-if="isMobile" class="mobile-cards">
<!-- Treasury Cards -->
<template v-if="invoiceType === '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="(invoiceType === '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="invoiceType !== '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 - US-307: Using default nav items -->
<MobileBottomNav v-if="isMobile" />
<!-- Mobile Filters BottomSheet -->
<!-- US-510: Removed type dropdown - type is determined by route -->
<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">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, Transition } from 'vue'
import { useRouter, useRoute } 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 MobileDrawerMenu from '@shared/components/mobile/MobileDrawerMenu.vue'
import BottomSheet from '@shared/components/mobile/BottomSheet.vue'
import Menu from 'primevue/menu'
import SplitButton from 'primevue/splitbutton'
import { useDashboardStore } from '@reports/stores/dashboard'
import { useCompanyStore, useAccountingPeriodStore, useAuthStore } from '@reports/stores/sharedStores'
import * as XLSX from 'xlsx'
import jsPDF from 'jspdf'
import 'jspdf-autotable'
const router = useRouter()
const route = useRoute()
const toast = useToast()
const dashboardStore = useDashboardStore()
const companyStore = useCompanyStore()
const periodStore = useAccountingPeriodStore()
const authStore = useAuthStore()
// US-703: Drawer state for hamburger menu
const showDrawer = ref(false)
// US-603: Tab state - initialized from route meta or query param
const activeTab = ref(route.query.tab === 'suppliers' ? 'suppliers' : (route.meta.invoiceType || 'clients'))
// US-603: invoiceType is now derived from activeTab (not route meta)
const invoiceType = computed(() => activeTab.value)
// US-603: Switch between Clienți/Furnizori tabs
const switchTab = async (tab) => {
if (tab === activeTab.value) return
activeTab.value = tab
// Update URL query param without full navigation
router.replace({
query: {
...route.query,
tab: tab === 'suppliers' ? 'suppliers' : undefined // Remove param if 'clients' (default)
}
})
// Reset pagination and reload data
firstRow.value = 0
expandedGroups.value.clear()
await loadDetailedData()
}
// 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)
// US-510: Removed selectedType - now using invoiceType from route
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
// US-510: Removed typeOptions - type is now determined by route
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' }
]
// US-501: Check if filters have non-default values
// US-510: Removed selectedType check - type is now route-based
const hasActiveFilters = computed(() => {
return searchTerm.value !== '' ||
selectedPeriod.value !== 'all'
})
// US-501: Mobile TopBar actions (filter, reset, export dropdown)
const topBarActions = computed(() => [
{
icon: 'pi pi-filter',
label: 'Filtre',
tooltip: 'Deschide filtre',
active: hasActiveFilters.value
},
{
icon: 'pi pi-filter-slash',
label: 'Resetează',
tooltip: 'Resetează Filtrele'
},
{
icon: 'pi pi-download',
label: 'Export',
tooltip: 'Export'
}
])
// US-501: Export menu ref for mobile
const exportMenu = ref(null)
const exportMenuItems = ref([
{
label: 'Export PDF',
icon: 'pi pi-file-pdf',
command: () => exportPDF()
},
{
label: 'Export XLSX',
icon: 'pi pi-file-excel',
command: () => exportExcel()
}
])
// US-501: Desktop export dropdown items (SplitButton)
const desktopExportItems = ref([
{
label: 'Export PDF',
icon: 'pi pi-file-pdf',
command: () => exportPDF()
},
{
label: 'Export XLSX',
icon: 'pi pi-file-excel',
command: () => exportExcel()
}
])
// Mobile navigation items
// US-307: Removed custom mobileNavItems - using MobileBottomNav defaults
// 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 (invoiceType.value === 'treasury') return []
const groups = {}
const nameField = invoiceType.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 (invoiceType.value === 'treasury') return []
return groupedData.value
})
const paginatedData = computed(() => {
if (invoiceType.value !== 'treasury') return []
return filteredData.value
})
const totalRecords = computed(() => {
return dashboardStore.detailedDataTotal || groupedData.value.length
})
// Helper functions
// US-510: Removed getTypeLabel - no longer needed
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')
}
// US-703: Handlers for MobileDrawerMenu
const handleLogout = async () => {
await authStore.logout()
router.push('/login')
}
const handleCompanyChanged = (company) => {
// Company store watcher handles the refresh
if (company) {
loadDetailedData()
}
}
const handlePeriodChanged = () => {
// Period store watcher handles the refresh
}
// US-501: Top bar action handler
const handleTopBarAction = (action, event) => {
if (action.icon === 'pi pi-filter') {
openFilters()
} else if (action.icon === 'pi pi-filter-slash') {
resetFilters()
} else if (action.icon === 'pi pi-download') {
exportMenu.value.toggle(event)
}
}
// Filter functions
const openFilters = () => {
isFilterSheetOpen.value = true
}
const clearSearch = () => {
searchTerm.value = ''
handleSearch()
}
// US-511: Clear period filter from chip
const clearPeriod = () => {
selectedPeriod.value = 'all'
loadDetailedData()
}
const handleSearch = () => {
firstRow.value = 0
expandedGroups.value.clear()
}
const resetFilters = () => {
// US-510: Removed type reset - type is now determined by route
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(
invoiceType.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, invoiceType.value)
XLSX.writeFile(wb, `facturi_${invoiceType.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 = invoiceType.value === 'treasury'
? ['Cont', 'Nume Cont', 'Sold', 'Valută', 'Tip']
: invoiceType.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 (invoiceType.value === 'treasury') {
return [row.cont, row.nume_cont, formatCurrency(row.sold), row.valuta, row.tip]
}
const nameField = invoiceType.value === 'clients' ? 'client' : 'furnizor'
const paidField = invoiceType.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_${invoiceType.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;
}
/* US-603: Extra padding when tabs are visible on mobile */
.main-content.mobile-layout.has-tabs {
padding-top: calc(56px + 48px); /* MobileTopBar + tabs */
}
/* ================================================
US-603: Mobile Tabs (Clienți/Furnizori)
Material Design 3 inspired full-width tabs
================================================ */
.mobile-tabs-container {
position: fixed;
top: 56px; /* Below MobileTopBar */
left: 0;
right: 0;
z-index: var(--z-sticky);
background: var(--surface-card);
border-bottom: 1px solid var(--surface-border);
}
.mobile-tabs {
display: flex;
width: 100%;
}
.mobile-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-md);
min-height: 48px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all var(--transition-fast);
color: var(--text-color-secondary);
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
.mobile-tab:active {
background: var(--surface-hover);
}
.mobile-tab.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: var(--font-semibold);
}
.tab-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ================================================
US-603: Desktop Tabs (Clienți/Furnizori)
================================================ */
/* US-705: Reduced margin-top from var(--space-md) to var(--space-sm) for less excessive top spacing */
.desktop-tabs {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-sm);
}
.desktop-tab {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-lg);
background: var(--surface-hover);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
color: var(--text-color-secondary);
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
.desktop-tab:hover {
background: var(--surface-card);
border-color: var(--color-primary);
color: var(--color-primary);
}
.desktop-tab.active {
background: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-text-inverse);
}
.desktop-tab i {
font-size: var(--text-base);
}
/* 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 */
/* US-705: Reduced from var(--space-md) to var(--space-sm) for less excessive top spacing */
.page-header {
margin-bottom: var(--space-sm);
}
.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 - US-511: Display-only chips showing active filters */
.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);
/* US-511: Removed cursor:pointer - chips are display-only, only close icon is clickable */
}
.filter-chip.active {
background: var(--primary-100);
color: var(--color-primary);
}
/* US-511: Close icon on filter chips - clickable to remove that filter */
.filter-chip .pi-times {
font-size: 0.75rem;
padding: var(--space-xs);
cursor: pointer;
border-radius: var(--radius-full);
transition: background var(--transition-fast);
/* Ensure touch target is adequate */
min-width: 24px;
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: calc(-1 * var(--space-xs)) calc(-1 * var(--space-xs)) calc(-1 * var(--space-xs)) 0;
}
.filter-chip .pi-times:hover {
background: var(--surface-hover);
}
.filter-chip .pi-times:active {
background: var(--surface-border);
}
/* 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;
}
/* ============================================
US-509: Expandable Groups Table (Desktop)
============================================ */
.expandable-groups-table {
width: 100%;
}
/* Table Header */
.groups-table-header {
display: grid;
grid-template-columns: 40px 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--space-xs);
padding: var(--space-md);
background: var(--surface-hover);
border-bottom: 1px solid var(--surface-border);
font-weight: var(--font-semibold);
font-size: var(--text-sm);
color: var(--text-color);
}
.header-cell {
padding: var(--space-xs) var(--space-sm);
}
.header-cell.expand-col {
padding: 0;
}
.header-cell.text-right {
text-align: right;
}
/* Table Body */
.groups-table-body {
display: flex;
flex-direction: column;
}
/* Group Row (Parent) */
.group-row {
display: grid;
grid-template-columns: 40px 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--surface-border);
font-size: var(--text-sm);
color: var(--text-color);
transition: background var(--transition-fast);
}
.group-row:last-child {
border-bottom: none;
}
.group-row.expandable {
cursor: pointer;
}
.group-row.expandable:hover {
background: var(--surface-hover);
}
.group-row.expanded {
background: var(--primary-50);
border-bottom-color: var(--primary-100);
}
/* Row cells */
.row-cell {
display: flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
min-height: 40px;
}
.row-cell.expand-col {
justify-content: center;
padding: 0;
}
.row-cell.name-col {
gap: var(--space-sm);
}
.row-cell.text-right {
justify-content: flex-end;
}
/* Expand Icon with rotation animation */
.expand-icon {
font-size: var(--text-sm);
color: var(--text-color-secondary);
transition: transform var(--transition-normal);
}
.expand-icon.rotated {
transform: rotate(90deg);
}
/* Expanded Invoices Container */
.expanded-invoices {
background: var(--surface-ground);
border-bottom: 1px solid var(--surface-border);
overflow: hidden;
}
/* Individual Invoice Row */
.invoice-row {
display: grid;
grid-template-columns: 40px 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-md);
font-size: var(--text-sm);
color: var(--text-color);
border-bottom: 1px solid var(--surface-border);
}
.invoice-row:last-child {
border-bottom: none;
}
.invoice-row:hover {
background: var(--surface-hover);
}
/* Sub-invoice indicator (connector line + label) */
.sub-invoice-indicator {
position: relative;
padding-left: var(--space-lg) !important;
}
.invoice-connector {
position: absolute;
left: var(--space-sm);
top: 50%;
width: var(--space-md);
height: 1px;
background: var(--surface-border);
}
.invoice-connector::before {
content: '';
position: absolute;
left: 0;
top: -8px;
width: 1px;
height: 16px;
background: var(--surface-border);
}
.invoice-doc-label {
font-size: var(--text-xs);
color: var(--text-color-secondary);
font-style: italic;
}
/* Expand/Collapse Animation */
.expand-enter-active,
.expand-leave-active {
transition: all var(--transition-normal);
transform-origin: top;
}
.expand-enter-from,
.expand-leave-to {
opacity: 0;
max-height: 0;
transform: scaleY(0);
}
.expand-enter-to,
.expand-leave-from {
opacity: 1;
max-height: 1000px;
transform: scaleY(1);
}
/* Empty table state */
.empty-table-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
color: var(--text-color-secondary);
}
.empty-table-state .pi {
font-size: 48px;
margin-bottom: var(--space-md);
opacity: 0.5;
}
/* Dark mode support for expandable groups */
[data-theme="dark"] .group-row.expanded {
background: var(--primary-900);
border-bottom-color: var(--primary-800);
}
[data-theme="dark"] .expanded-invoices {
background: var(--surface-100);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .group-row.expanded {
background: var(--primary-900);
border-bottom-color: var(--primary-800);
}
:root:not([data-theme]) .expanded-invoices {
background: var(--surface-100);
}
}
.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>