Implemented by Ralph autonomous loop. Iteration: 6 Co-Authored-By: Claude <noreply@anthropic.com>
1302 lines
34 KiB
Vue
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, vă rugăm să 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>
|