feat: Add mobile-optimized card layout and compact UI for all report views

- Add mobile card layout for Invoices, Treasury, and Trial Balance views
- Implement two-row mobile toolbar with icon-only action buttons
- Add uniform totals grid across all views with compact number formatting
- Move profile menu to hamburger menu on mobile devices
- Fix company selector and period selector truncation on mobile
- Add mobile-specific CSS with responsive breakpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-10 11:24:05 +02:00
parent 0d1cdbfdff
commit fca6908543
9 changed files with 1000 additions and 44 deletions

View File

@@ -86,6 +86,9 @@
transition: transform var(--transition-normal); transition: transform var(--transition-normal);
z-index: var(--z-modal); z-index: var(--z-modal);
overflow-y: auto; overflow-y: auto;
/* Flex container for profile section at bottom */
display: flex;
flex-direction: column;
} }
.slide-menu.open { .slide-menu.open {

View File

@@ -693,3 +693,401 @@
min-height: 44px; /* Touch-friendly height */ min-height: 44px; /* Touch-friendly height */
} }
} }
/* ============================================
Mobile Compact Toolbar
Ultra-compact header for mobile views
- Filter toggle (funnel icon)
- Actions dropdown menu
- Single compact total display
============================================ */
/* Mobile Toolbar - compact single line (~50px) */
.mobile-toolbar {
display: flex;
align-items: center;
gap: var(--space-sm, 0.5rem);
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
background: var(--surface-card, #ffffff);
border: 1px solid var(--surface-border, #e2e8f0);
border-radius: var(--border-radius, 6px);
margin-bottom: var(--space-md, 1rem);
min-height: 50px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Filter toggle button - colored when filters are active */
.mobile-toolbar .filter-active {
color: var(--primary-color, #2563eb) !important;
background: rgba(37, 99, 235, 0.1) !important;
}
.mobile-toolbar .filter-active:hover {
background: rgba(37, 99, 235, 0.2) !important;
}
/* Actions button - compact */
.mobile-toolbar .p-button-outlined {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
/* Compact total display - pushed to right */
.mobile-toolbar .mobile-total {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-xs, 0.25rem);
font-size: var(--text-sm, 0.875rem);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 50%;
}
.mobile-toolbar .total-label {
color: var(--text-color-secondary, #64748b);
font-weight: 500;
}
.mobile-toolbar .total-value {
font-weight: var(--font-semibold, 600);
color: var(--text-color, #1e293b);
font-variant-numeric: tabular-nums;
}
/* Color classes for positive/negative values */
.mobile-toolbar .total-value.incasari {
color: var(--green-600, #16a34a);
}
.mobile-toolbar .total-value.plati {
color: var(--red-600, #dc2626);
}
/* Mobile-only visibility - show toolbar only on mobile */
@media (min-width: 769px) {
.mobile-toolbar {
display: none !important;
}
}
/* Extra compact on very small screens */
@media (max-width: 400px) {
.mobile-toolbar {
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
gap: var(--space-xs, 0.25rem);
}
.mobile-toolbar .mobile-total {
font-size: var(--text-xs, 0.75rem);
}
.mobile-toolbar .p-button-outlined {
padding: 0.375rem 0.5rem;
font-size: 0.8125rem;
}
/* Hide label on very small screens, show only icon */
.mobile-toolbar .p-button-outlined .p-button-label {
display: none;
}
.mobile-toolbar .p-button-outlined .p-button-icon {
margin-right: 0;
}
}
/* Filters card - more compact when visible on mobile */
@media (max-width: 768px) {
.filters-card {
margin-bottom: var(--space-sm, 0.5rem);
}
.filters-card .p-card-body {
padding: var(--space-sm, 0.5rem);
}
.filters-card .form-row {
gap: var(--space-sm, 0.5rem);
}
.filters-card .form-group {
margin-bottom: var(--space-xs, 0.25rem);
}
.filters-card .form-label {
font-size: var(--text-sm, 0.875rem);
margin-bottom: var(--space-xs, 0.25rem);
}
}
/* ============================================
Mobile Data Cards
Compact card layout for table data on mobile
============================================ */
.mobile-card-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.mobile-data-card {
background: var(--surface-card, #ffffff);
border: 1px solid var(--surface-border, #e2e8f0);
border-radius: 8px;
padding: 0.75rem 1rem;
}
.mobile-data-card .card-header {
font-weight: 600;
font-size: 0.9375rem;
color: var(--text-color, #1e293b);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mobile-data-card .card-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: var(--text-color-secondary, #64748b);
margin-top: 0.25rem;
}
.mobile-data-card .card-meta {
font-size: 0.8125rem;
}
.mobile-data-card .card-amount {
font-weight: 600;
font-variant-numeric: tabular-nums;
font-size: 0.9375rem;
}
.mobile-data-card .card-amount.positive {
color: var(--green-600, #16a34a);
}
.mobile-data-card .card-amount.negative {
color: var(--red-600, #dc2626);
}
/* Mobile empty state */
.mobile-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
color: var(--text-color-secondary);
}
.mobile-empty i {
font-size: 2rem;
margin-bottom: 0.5rem;
}
/* ============================================
Mobile Responsive Totals
Unified grid layout for totals on mobile
Supports 1, 2, or 4 totals uniformly
============================================ */
.mobile-totals-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.375rem 1rem;
padding: 0.5rem 0.75rem;
background: var(--surface-ground, #f8fafc);
border-radius: 6px;
font-size: 0.8125rem;
width: 100%;
}
/* Single total - center it */
.mobile-totals-grid.single-total {
grid-template-columns: 1fr;
justify-items: center;
}
/* Two totals - side by side */
.mobile-totals-grid.two-totals {
grid-template-columns: 1fr 1fr;
}
.mobile-totals-grid .total-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.mobile-totals-grid .total-label {
color: var(--text-color-secondary, #64748b);
font-size: 0.75rem;
white-space: nowrap;
}
.mobile-totals-grid .total-value {
font-weight: 600;
font-variant-numeric: tabular-nums;
font-size: 0.875rem;
}
.mobile-totals-grid .total-value.incasari,
.mobile-totals-grid .total-value.positive {
color: var(--green-600, #16a34a);
}
.mobile-totals-grid .total-value.plati,
.mobile-totals-grid .total-value.negative {
color: var(--red-600, #dc2626);
}
/* Backward compatibility - stack totals (deprecated, use grid) */
.mobile-totals-stack {
display: flex;
flex-direction: column;
gap: 0.125rem;
font-size: 0.75rem;
margin-left: auto;
}
.mobile-totals-stack .total-row {
display: flex;
gap: 0.5rem;
}
.mobile-totals-stack .total-label {
color: var(--text-color-secondary, #64748b);
}
.mobile-totals-stack .total-value {
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* ============================================
Mobile Toolbar v2 - Two-Row Layout
Row 1: Icon-only action buttons
Row 2: Totals display
============================================ */
.mobile-toolbar-container {
display: flex;
flex-direction: column;
gap: var(--space-sm, 0.5rem);
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
background: var(--surface-card, #ffffff);
border: 1px solid var(--surface-border, #e2e8f0);
border-radius: var(--border-radius, 6px);
margin-bottom: var(--space-md, 1rem);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Row 1: Icon-only action buttons */
.mobile-toolbar-buttons {
display: flex;
justify-content: space-around;
align-items: center;
gap: var(--space-xs, 0.25rem);
}
/* Icon-only buttons - no labels */
.mobile-toolbar-buttons .p-button {
padding: var(--space-sm, 0.5rem);
min-width: 44px;
min-height: 44px;
justify-content: center;
}
.mobile-toolbar-buttons .p-button .p-button-label {
display: none !important;
}
.mobile-toolbar-buttons .p-button .p-button-icon {
margin-right: 0 !important;
font-size: 1.125rem;
}
/* Filter active state */
.mobile-toolbar-buttons .filter-active {
color: var(--primary-color, #2563eb) !important;
background: rgba(37, 99, 235, 0.1) !important;
border-color: var(--primary-color, #2563eb) !important;
}
.mobile-toolbar-buttons .filter-active:hover {
background: rgba(37, 99, 235, 0.2) !important;
}
/* Row 2: Totals display */
.mobile-toolbar-totals {
display: flex;
justify-content: center;
align-items: center;
padding-top: var(--space-xs, 0.25rem);
border-top: 1px solid var(--surface-border, #e2e8f0);
}
/* Center the totals grid/stack in row 2 */
.mobile-toolbar-totals .mobile-totals-grid,
.mobile-toolbar-totals .mobile-totals-stack,
.mobile-toolbar-totals .mobile-total {
margin-left: 0;
width: auto;
}
/* Hide on desktop */
@media (min-width: 769px) {
.mobile-toolbar-container {
display: none !important;
}
}
/* Extra compact on very small screens */
@media (max-width: 400px) {
.mobile-toolbar-container {
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
gap: var(--space-xs, 0.25rem);
}
.mobile-toolbar-buttons .p-button {
padding: var(--space-xs, 0.25rem);
min-width: 40px;
min-height: 40px;
}
.mobile-toolbar-buttons .p-button .p-button-icon {
font-size: 1rem;
}
}
/* ============================================
Hamburger Menu Profile Section
Profile options at bottom of slide menu
============================================ */
.menu-profile {
margin-top: auto;
border-top: 1px solid var(--color-border, #e2e8f0);
padding-top: var(--space-md, 1rem);
}
.menu-profile .profile-info {
display: flex;
align-items: center;
gap: var(--space-sm, 0.5rem);
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
font-weight: 600;
color: var(--color-text, #1e293b);
font-size: var(--text-sm, 0.875rem);
}
.menu-profile .profile-info .pi-user {
font-size: 1.25rem;
color: var(--color-primary, #2563eb);
}

View File

@@ -497,18 +497,55 @@ export default {
/* Mobile adjustments */ /* Mobile adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.company-selector-mini { .company-selector-mini {
max-width: none; max-width: 200px;
width: 100%; width: auto;
} }
.company-trigger { .company-trigger {
min-width: auto; min-width: auto;
max-width: 200px;
padding: var(--space-xs) var(--space-sm);
}
.company-info {
max-width: 140px;
}
.company-name {
font-size: var(--text-xs);
max-width: 140px;
}
.company-code {
font-size: 10px;
} }
.company-dropdown-panel { .company-dropdown-panel {
left: -16px; position: fixed;
right: -16px; left: 8px;
width: calc(100% + 32px); right: 8px;
top: 60px;
width: auto;
max-height: 70vh;
}
}
/* Extra small screens */
@media (max-width: 400px) {
.company-selector-mini {
max-width: 160px;
}
.company-trigger {
max-width: 160px;
}
.company-info {
max-width: 110px;
}
.company-name {
max-width: 110px;
} }
} }
</style> </style>

View File

@@ -349,18 +349,36 @@ export default {
/* Mobile adjustments */ /* Mobile adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.period-selector-mini { .period-selector-mini {
max-width: none; max-width: 140px;
width: 100%; width: auto;
} }
.period-trigger { .period-trigger {
min-width: auto; min-width: auto;
padding: var(--space-xs) var(--space-sm);
}
.period-info {
flex-direction: row;
align-items: center;
gap: var(--space-xs);
}
.period-label {
display: none;
}
.period-name {
font-size: var(--text-xs);
} }
.period-dropdown-panel { .period-dropdown-panel {
left: -16px; position: fixed;
right: -16px; left: 8px;
width: calc(100% + 32px); right: 8px;
top: 60px;
width: auto;
max-height: 70vh;
} }
} }
</style> </style>

View File

@@ -29,7 +29,7 @@
v-model="selectedCompany" v-model="selectedCompany"
@company-changed="onCompanyChanged" @company-changed="onCompanyChanged"
/> />
<div class="user-menu-container"> <div class="user-menu-container mobile-hide">
<div class="header-user" @click="toggleUserMenu"> <div class="header-user" @click="toggleUserMenu">
<i class="pi pi-user"></i> <i class="pi pi-user"></i>
<span class="desktop-only">{{ <span class="desktop-only">{{
@@ -311,5 +311,10 @@ export default {
.user-dropdown-item { .user-dropdown-item {
padding: var(--space-sm); padding: var(--space-sm);
} }
/* Hide profile menu on mobile - use hamburger menu instead */
.mobile-hide {
display: none !important;
}
} }
</style> </style>

View File

@@ -75,6 +75,16 @@
<span>Statistici cache</span> <span>Statistici cache</span>
</router-link> </router-link>
</li> </li>
</ul>
</div>
<!-- Profile Section (at bottom) -->
<div class="menu-section menu-profile">
<div class="profile-info">
<i class="pi pi-user"></i>
<span>{{ currentUser?.username || 'Utilizator' }}</span>
</div>
<ul class="menu-list">
<li class="menu-item"> <li class="menu-item">
<router-link <router-link
to="/telegram" to="/telegram"
@@ -86,6 +96,12 @@
<span>Telegram Bot</span> <span>Telegram Bot</span>
</router-link> </router-link>
</li> </li>
<li class="menu-item">
<a href="#" class="menu-link" @click.prevent="handleLogout">
<i class="menu-icon pi pi-sign-out"></i>
<span>Deconectare</span>
</a>
</li>
</ul> </ul>
</div> </div>
</nav> </nav>
@@ -93,6 +109,10 @@
</template> </template>
<script> <script>
import { computed } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "../../stores/auth";
export default { export default {
name: "HamburgerMenu", name: "HamburgerMenu",
props: { props: {
@@ -103,12 +123,29 @@ export default {
}, },
emits: ["close"], emits: ["close"],
setup(props, { emit }) { setup(props, { emit }) {
const router = useRouter();
const authStore = useAuthStore();
const currentUser = computed(() => authStore.currentUser);
const closeMenu = () => { const closeMenu = () => {
emit("close"); emit("close");
}; };
const handleLogout = async () => {
try {
authStore.logout();
closeMenu();
await router.push("/login");
} catch (error) {
console.error("Logout error:", error);
}
};
return { return {
currentUser,
closeMenu, closeMenu,
handleLogout,
}; };
}, },
}; };

View File

@@ -7,9 +7,6 @@
<i class="pi pi-wallet"></i> <i class="pi pi-wallet"></i>
Registru Casă / Bancă Registru Casă / Bancă
</h1> </h1>
<p class="page-subtitle">
Selectați tipul de registru pentru a vizualiza mișcările
</p>
</div> </div>
<!-- Company Selection (when no company selected) --> <!-- Company Selection (when no company selected) -->
@@ -33,8 +30,71 @@
</template> </template>
</Card> </Card>
<!-- Mobile: Two-row toolbar -->
<div v-if="isMobile && companyStore.selectedCompany" class="mobile-toolbar-container">
<!-- Row 1: Icon-only action buttons -->
<div class="mobile-toolbar-buttons">
<Button
icon="pi pi-filter"
:class="{ 'filter-active': hasActiveFilters }"
class="p-button-text"
@click="showFilters = !showFilters"
v-tooltip.bottom="'Filtre'"
/>
<Button
icon="pi pi-filter-slash"
class="p-button-text"
@click="resetFilters"
v-tooltip.bottom="'Resetează'"
/>
<Button
icon="pi pi-file-excel"
class="p-button-text p-button-success"
@click="exportExcel"
:disabled="!hasData"
v-tooltip.bottom="'Excel'"
/>
<Button
icon="pi pi-file-pdf"
class="p-button-text p-button-danger"
@click="exportPDF"
:disabled="!hasData"
v-tooltip.bottom="'PDF'"
/>
<Button
icon="pi pi-refresh"
class="p-button-text"
:loading="treasuryStore.isLoading"
@click="refreshData"
v-tooltip.bottom="'Actualizează'"
/>
</div>
<!-- Row 2: Totals grid -->
<div class="mobile-toolbar-totals">
<div class="mobile-totals-grid">
<div class="total-item">
<span class="total-label">Sold Prec:</span>
<span class="total-value">{{ formatCompact(treasuryStore.totals.sold_precedent_all) }}</span>
</div>
<div class="total-item">
<span class="total-label">Încasări:</span>
<span class="total-value incasari">{{ formatCompact(treasuryStore.totals.total_incasari_all) }}</span>
</div>
<div class="total-item">
<span class="total-label">Plăți:</span>
<span class="total-value plati">{{ formatCompact(treasuryStore.totals.total_plati_all) }}</span>
</div>
<div class="total-item">
<span class="total-label">Sold Final:</span>
<span class="total-value">{{ formatCompact(treasuryStore.totals.sold_final_all) }}</span>
</div>
</div>
</div>
</div>
<!-- Filters --> <!-- Filters -->
<Card v-if="companyStore.selectedCompany" class="filters-card"> <Card v-if="companyStore.selectedCompany && (!isMobile || showFilters)" class="filters-card">
<template #content> <template #content>
<div class="form"> <div class="form">
<div class="form-row"> <div class="form-row">
@@ -81,8 +141,8 @@
</div> </div>
</div> </div>
<!-- Separate action buttons row --> <!-- Desktop: Action buttons -->
<div class="form-actions"> <div v-if="!isMobile" class="form-actions">
<Button <Button
icon="pi pi-filter-slash" icon="pi pi-filter-slash"
label="Resetează Filtre" label="Resetează Filtre"
@@ -114,9 +174,9 @@
</template> </template>
</Card> </Card>
<!-- Summary Stats - Compact, right aligned --> <!-- Summary Stats - Compact, right aligned (hidden on mobile - only Sold Final in toolbar) -->
<!-- Folosește totaluri din TOATE înregistrările (backend) nu doar pagina curentă --> <!-- Folosește totaluri din TOATE înregistrările (backend) nu doar pagina curentă -->
<div v-if="companyStore.selectedCompany" class="summary-stats-inline"> <div v-if="!isMobile && companyStore.selectedCompany" class="summary-stats-inline">
<div class="stat-item"> <div class="stat-item">
<span class="stat-label">Sold Precedent:</span> <span class="stat-label">Sold Precedent:</span>
<span <span
@@ -150,7 +210,35 @@
<!-- Data Table --> <!-- Data Table -->
<Card v-if="companyStore.selectedCompany" class="data-card"> <Card v-if="companyStore.selectedCompany" class="data-card">
<template #content> <template #content>
<!-- Mobile: Card Layout -->
<div v-if="isMobile" class="mobile-card-list">
<div
v-for="reg in treasuryStore.registers"
:key="`${reg.dataact}-${reg.nract}`"
class="mobile-data-card"
>
<div class="card-header">{{ reg.nume || 'Fără partener' }}</div>
<div class="card-row">
<span class="card-meta">{{ formatDateShort(reg.dataact) }} · {{ reg.nume_cont_bancar }}</span>
<span
class="card-amount"
:class="reg.incasari > 0 ? 'positive' : (reg.plati > 0 ? 'negative' : '')"
>
<template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template>
<template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template>
<template v-else>{{ formatNumber(0) }}</template>
</span>
</div>
</div>
<div v-if="treasuryStore.registers.length === 0" class="mobile-empty">
<i class="pi pi-info-circle"></i>
<p>Nu au fost găsite înregistrări</p>
</div>
</div>
<!-- Desktop: DataTable -->
<DataTable <DataTable
v-if="!isMobile"
:value="treasuryStore.registers" :value="treasuryStore.registers"
:loading="treasuryStore.isLoading" :loading="treasuryStore.isLoading"
:paginator="true" :paginator="true"
@@ -260,7 +348,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from "vue"; import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useToast } from "primevue/usetoast"; import { useToast } from "primevue/usetoast";
import { useTreasuryStore } from "../stores/treasury"; import { useTreasuryStore } from "../stores/treasury";
import { useCompanyStore } from "../stores/companies"; import { useCompanyStore } from "../stores/companies";
@@ -276,6 +364,19 @@ const periodStore = useAccountingPeriodStore();
// State for company selection // State for company selection
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null); const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
// Mobile state
const isMobile = ref(window.innerWidth < 768);
const showFilters = ref(false);
const actionsMenu = ref(null);
// Handle window resize
const handleResize = () => {
isMobile.value = window.innerWidth < 768;
if (!isMobile.value) {
showFilters.value = false; // Reset when switching to desktop
}
};
// Register type options for dropdown - doar cele 4 tipuri, fără "Toate" // Register type options for dropdown - doar cele 4 tipuri, fără "Toate"
const registerTypeOptions = [ const registerTypeOptions = [
{ label: "Casă LEI", value: "CASA_LEI" }, { label: "Casă LEI", value: "CASA_LEI" },
@@ -319,6 +420,27 @@ const formatDate = (dateString) => {
return format(new Date(dateString), "dd.MM.yyyy"); return format(new Date(dateString), "dd.MM.yyyy");
}; };
// Short date format for mobile cards (DD/MM)
const formatDateShort = (dateString) => {
if (!dateString) return "";
const date = new Date(dateString);
return `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}`;
};
// Compact number format (no decimals for large numbers)
const formatCompact = (amount) => {
if (!amount) return "0";
if (Math.abs(amount) >= 10000) {
return new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 0,
}).format(amount);
}
return new Intl.NumberFormat("ro-RO", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
};
// Truncate text to maxLength characters // Truncate text to maxLength characters
const truncateText = (text, maxLength = 100) => { const truncateText = (text, maxLength = 100) => {
if (!text) return ""; if (!text) return "";
@@ -447,6 +569,42 @@ const resetFilters = async () => {
// Computed // Computed
const hasData = computed(() => treasuryStore.registers.length > 0); const hasData = computed(() => treasuryStore.registers.length > 0);
// Mobile: Check if any filter is active (non-default value)
const hasActiveFilters = computed(() => {
return (
filters.value.registerType !== "BANCA_LEI" ||
filters.value.partnerName !== "" ||
filters.value.bankAccount !== null
);
});
// Mobile: Actions menu items
const actionMenuItems = computed(() => [
{
label: "Resetează Filtre",
icon: "pi pi-filter-slash",
command: resetFilters,
},
{
label: "Export Excel",
icon: "pi pi-file-excel",
command: exportExcel,
disabled: !hasData.value,
},
{
label: "Export PDF",
icon: "pi pi-file-pdf",
command: exportPDF,
disabled: !hasData.value,
},
{ separator: true },
{
label: "Actualizează",
icon: "pi pi-refresh",
command: refreshData,
},
]);
// Handle company change from dropdown // Handle company change from dropdown
const handleCompanyChange = async () => { const handleCompanyChange = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
@@ -696,6 +854,9 @@ const loadData = async () => {
}; };
onMounted(async () => { onMounted(async () => {
// Add resize listener for mobile detection
window.addEventListener("resize", handleResize);
// Load companies if not loaded // Load companies if not loaded
if (!companyStore.hasCompanies) { if (!companyStore.hasCompanies) {
await companyStore.loadCompanies(); await companyStore.loadCompanies();
@@ -708,6 +869,10 @@ onMounted(async () => {
// Don't load data here - let period watch handle it with immediate: true // Don't load data here - let period watch handle it with immediate: true
}); });
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
// Watch for company changes // Watch for company changes
watch( watch(
() => companyStore.selectedCompany, () => companyStore.selectedCompany,

View File

@@ -7,9 +7,6 @@
<i class="pi pi-file-text"></i> <i class="pi pi-file-text"></i>
Facturi Facturi
</h1> </h1>
<p class="page-subtitle">
Vizualizați și gestionați facturile pentru compania selectată
</p>
</div> </div>
<!-- Company Selection --> <!-- Company Selection -->
@@ -32,8 +29,61 @@
</template> </template>
</Card> </Card>
<!-- Mobile: Two-row toolbar -->
<div v-if="isMobile && companyStore.selectedCompany" class="mobile-toolbar-container">
<!-- Row 1: Icon-only action buttons -->
<div class="mobile-toolbar-buttons">
<Button
icon="pi pi-filter"
:class="{ 'filter-active': hasActiveFilters }"
class="p-button-text"
@click="showFilters = !showFilters"
v-tooltip.bottom="'Filtre'"
/>
<Button
icon="pi pi-filter-slash"
class="p-button-text"
@click="clearFilters"
v-tooltip.bottom="'Resetează'"
/>
<Button
icon="pi pi-file-excel"
class="p-button-text p-button-success"
@click="exportExcel"
:disabled="!invoicesStore.hasInvoices"
v-tooltip.bottom="'Excel'"
/>
<Button
icon="pi pi-file-pdf"
class="p-button-text p-button-danger"
@click="exportPDF"
:disabled="!invoicesStore.hasInvoices"
v-tooltip.bottom="'PDF'"
/>
<Button
icon="pi pi-refresh"
class="p-button-text"
:loading="invoicesStore.isLoading"
@click="refreshData"
v-tooltip.bottom="'Actualizează'"
/>
</div>
<!-- Row 2: Totals (unified grid format) -->
<div class="mobile-toolbar-totals">
<div class="mobile-totals-grid single-total">
<div class="total-item">
<span class="total-label">Sold Total:</span>
<span class="total-value" :class="invoicesStore.totalSoldAll > 0 ? 'positive' : 'negative'">
{{ formatCompact(invoicesStore.totalSoldAll) }}
</span>
</div>
</div>
</div>
</div>
<!-- Filters and Controls --> <!-- Filters and Controls -->
<Card v-if="companyStore.selectedCompany" class="filters-card"> <Card v-if="companyStore.selectedCompany && (!isMobile || showFilters)" class="filters-card">
<template #content> <template #content>
<div class="form"> <div class="form">
<div class="form-row"> <div class="form-row">
@@ -96,7 +146,8 @@
</div> </div>
</div> </div>
<div class="filters-actions"> <!-- Desktop: Action buttons -->
<div v-if="!isMobile" class="filters-actions">
<Button <Button
icon="pi pi-filter-slash" icon="pi pi-filter-slash"
label="Resetează Filtre" label="Resetează Filtre"
@@ -128,9 +179,9 @@
</template> </template>
</Card> </Card>
<!-- Summary Stats - Compact, right aligned --> <!-- Summary Stats - Compact, right aligned (hidden on mobile - shown in toolbar) -->
<!-- Total sold din TOATE facturile filtrate (nu doar pagina curentă) --> <!-- Total sold din TOATE facturile filtrate (nu doar pagina curentă) -->
<div v-if="companyStore.selectedCompany && invoicesStore.hasInvoices" class="summary-stats-inline"> <div v-if="!isMobile && companyStore.selectedCompany && invoicesStore.hasInvoices" class="summary-stats-inline">
<div class="stat-item"> <div class="stat-item">
<span class="stat-label">Total Sold:</span> <span class="stat-label">Total Sold:</span>
<span class="stat-value" :class="invoicesStore.totalSoldAll > 0 ? 'plati' : 'incasari'"> <span class="stat-value" :class="invoicesStore.totalSoldAll > 0 ? 'plati' : 'incasari'">
@@ -142,7 +193,33 @@
<!-- Invoices Table --> <!-- Invoices Table -->
<Card v-if="companyStore.selectedCompany" class="table-card"> <Card v-if="companyStore.selectedCompany" class="table-card">
<template #content> <template #content>
<!-- Mobile: Card Layout -->
<div v-if="isMobile" class="mobile-card-list">
<div
v-for="invoice in invoicesStore.invoiceList"
:key="invoice.nract"
class="mobile-data-card"
>
<div class="card-header">{{ invoice.nume }}</div>
<div class="card-row">
<span>{{ formatDate(invoice.dataact) }} · {{ invoice.nract }}</span>
<span
class="card-amount"
:class="{ positive: invoice.soldfinal > 0 }"
>
{{ formatNumber(invoice.soldfinal) }}
</span>
</div>
</div>
<div v-if="invoicesStore.invoiceList.length === 0" class="mobile-empty">
<i class="pi pi-info-circle"></i>
<p>Nu au fost găsite facturi</p>
</div>
</div>
<!-- Desktop: DataTable -->
<DataTable <DataTable
v-if="!isMobile"
:value="invoicesStore.invoiceList" :value="invoicesStore.invoiceList"
:loading="invoicesStore.isLoading" :loading="invoicesStore.isLoading"
:paginator="true" :paginator="true"
@@ -245,7 +322,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from "vue"; import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useToast } from "primevue/usetoast"; import { useToast } from "primevue/usetoast";
import { useCompanyStore } from "../stores/companies"; import { useCompanyStore } from "../stores/companies";
import { useInvoicesStore } from "../stores/invoices"; import { useInvoicesStore } from "../stores/invoices";
@@ -262,6 +339,19 @@ const periodStore = useAccountingPeriodStore();
// State // State
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null); const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
// Mobile state
const isMobile = ref(window.innerWidth < 768);
const showFilters = ref(false);
const actionsMenu = ref(null);
// Handle window resize
const handleResize = () => {
isMobile.value = window.innerWidth < 768;
if (!isMobile.value) {
showFilters.value = false; // Reset when switching to desktop
}
};
const filters = ref({ const filters = ref({
type: "CLIENTI", type: "CLIENTI",
paymentStatus: "neachitate", // Default to unpaid invoices paymentStatus: "neachitate", // Default to unpaid invoices
@@ -280,6 +370,43 @@ const accountingPeriodText = computed(() => {
return periodStore.selectedPeriod?.display_name || ""; return periodStore.selectedPeriod?.display_name || "";
}); });
// Mobile: Check if any filter is active (non-default value)
const hasActiveFilters = computed(() => {
return (
filters.value.type !== "CLIENTI" ||
filters.value.paymentStatus !== "neachitate" ||
filters.value.searchTerm !== "" ||
filters.value.cont !== ""
);
});
// Mobile: Actions menu items
const actionMenuItems = computed(() => [
{
label: "Resetează Filtre",
icon: "pi pi-filter-slash",
command: clearFilters,
},
{
label: "Export Excel",
icon: "pi pi-file-excel",
command: exportExcel,
disabled: !invoicesStore.hasInvoices,
},
{
label: "Export PDF",
icon: "pi pi-file-pdf",
command: exportPDF,
disabled: !invoicesStore.hasInvoices,
},
{ separator: true },
{
label: "Actualizează",
icon: "pi pi-refresh",
command: refreshData,
},
]);
// Options // Options
const invoiceTypes = [ const invoiceTypes = [
{ label: "Clienți", value: "CLIENTI" }, { label: "Clienți", value: "CLIENTI" },
@@ -308,6 +435,20 @@ const formatNumber = (amount) => {
}).format(amount); }).format(amount);
}; };
// Compact format for mobile totals (e.g., "34.922" instead of "34.922,02 RON")
const formatCompact = (amount) => {
if (!amount || amount === 0) return "0";
const absAmount = Math.abs(amount);
if (absAmount >= 1000000) {
return new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 1,
}).format(amount / 1000000) + "M";
}
return new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 0,
}).format(amount);
};
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return ""; if (!dateString) return "";
try { try {
@@ -631,6 +772,9 @@ const exportPDF = async () => {
// Lifecycle // Lifecycle
onMounted(async () => { onMounted(async () => {
// Add resize listener for mobile detection
window.addEventListener("resize", handleResize);
// Load companies if not loaded // Load companies if not loaded
if (!companyStore.hasCompanies) { if (!companyStore.hasCompanies) {
await companyStore.loadCompanies(); await companyStore.loadCompanies();
@@ -638,6 +782,10 @@ onMounted(async () => {
// Don't load here - let period watch handle it with immediate: true // Don't load here - let period watch handle it with immediate: true
}); });
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
// Watch for company changes // Watch for company changes
watch( watch(
() => companyStore.selectedCompany, () => companyStore.selectedCompany,
@@ -675,18 +823,12 @@ watch(
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-color); color: var(--text-color);
margin: 0 0 0.5rem 0; margin: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
} }
.page-subtitle {
font-size: 1.1rem;
color: var(--text-color-secondary);
margin: 0;
}
.company-selection-card, .company-selection-card,
.filters-card { .filters-card {
margin-bottom: 2rem; margin-bottom: 2rem;

View File

@@ -7,10 +7,6 @@
<i class="pi pi-calculator"></i> <i class="pi pi-calculator"></i>
Balanță de Verificare Balanță de Verificare
</h1> </h1>
<p class="page-subtitle">
{{ currentPeriodText }} -
{{ companyStore.selectedCompany?.name || "Selectați companie" }}
</p>
</div> </div>
<!-- Company Selection --> <!-- Company Selection -->
@@ -33,8 +29,63 @@
</template> </template>
</Card> </Card>
<!-- Mobile: Two-row toolbar -->
<div v-if="isMobile && companyStore.selectedCompany" class="mobile-toolbar-container">
<!-- Row 1: Icon-only action buttons -->
<div class="mobile-toolbar-buttons">
<Button
icon="pi pi-filter"
:class="{ 'filter-active': hasActiveFilters }"
class="p-button-text"
@click="showFilters = !showFilters"
v-tooltip.bottom="'Filtre'"
/>
<Button
icon="pi pi-filter-slash"
class="p-button-text"
@click="clearFilters"
v-tooltip.bottom="'Resetează'"
/>
<Button
icon="pi pi-file-excel"
class="p-button-text p-button-success"
@click="exportExcel"
:disabled="!trialBalanceStore.hasData"
v-tooltip.bottom="'Excel'"
/>
<Button
icon="pi pi-file-pdf"
class="p-button-text p-button-danger"
@click="exportPDF"
:disabled="!trialBalanceStore.hasData"
v-tooltip.bottom="'PDF'"
/>
<Button
icon="pi pi-refresh"
class="p-button-text"
:loading="trialBalanceStore.isLoading"
@click="refreshData"
v-tooltip.bottom="'Actualizează'"
/>
</div>
<!-- Row 2: Totals (unified grid format) -->
<div class="mobile-toolbar-totals">
<div class="mobile-totals-grid two-totals">
<div class="total-item">
<span class="total-label">Sold D:</span>
<span class="total-value">{{ formatCompact(trialBalanceStore.totals.total_sold_final_debit) }}</span>
</div>
<div class="total-item">
<span class="total-label">Sold C:</span>
<span class="total-value">{{ formatCompact(trialBalanceStore.totals.total_sold_final_credit) }}</span>
</div>
</div>
</div>
</div>
<!-- Filters Section --> <!-- Filters Section -->
<Card v-if="companyStore.selectedCompany" class="filters-card"> <Card v-if="companyStore.selectedCompany && (!isMobile || showFilters)" class="filters-card">
<template #content> <template #content>
<div class="form"> <div class="form">
<div class="form-row"> <div class="form-row">
@@ -65,7 +116,8 @@
</div> </div>
</div> </div>
<div class="form-actions"> <!-- Desktop: Action buttons -->
<div v-if="!isMobile" class="form-actions">
<Button <Button
icon="pi pi-filter-slash" icon="pi pi-filter-slash"
label="Resetează Filtre" label="Resetează Filtre"
@@ -97,9 +149,9 @@
</template> </template>
</Card> </Card>
<!-- Summary Totals - Uses shared stats.css --> <!-- Summary Totals - Uses shared stats.css (hidden on mobile - compact in toolbar) -->
<!-- Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă) --> <!-- Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă) -->
<div v-if="companyStore.selectedCompany && trialBalanceStore.hasData" class="summary-stats-inline"> <div v-if="!isMobile && companyStore.selectedCompany && trialBalanceStore.hasData" class="summary-stats-inline">
<div class="stat-item"> <div class="stat-item">
<span class="stat-label">Sume Prec. D:</span> <span class="stat-label">Sume Prec. D:</span>
<span class="stat-value">{{ formatCurrency(trialBalanceStore.totals.total_sold_precedent_debit) }}</span> <span class="stat-value">{{ formatCurrency(trialBalanceStore.totals.total_sold_precedent_debit) }}</span>
@@ -129,7 +181,34 @@
<!-- Trial Balance Table --> <!-- Trial Balance Table -->
<Card v-if="companyStore.selectedCompany" class="table-card"> <Card v-if="companyStore.selectedCompany" class="table-card">
<template #content> <template #content>
<!-- Mobile: Card Layout -->
<div v-if="isMobile" class="mobile-card-list">
<div
v-for="account in trialBalanceStore.trialBalanceData.filter(a => a.sold_final_debit > 0 || a.sold_final_credit > 0)"
:key="account.cont"
class="mobile-data-card"
>
<div class="card-header">
<strong>{{ account.cont }}</strong>&nbsp;&nbsp;{{ truncate(account.denumire, 30) }}
</div>
<div class="card-row">
<span></span>
<span class="card-amount">
{{ account.sold_final_debit > 0
? formatCurrency(account.sold_final_debit) + ' D'
: formatCurrency(account.sold_final_credit) + ' C' }}
</span>
</div>
</div>
<div v-if="trialBalanceStore.trialBalanceData.filter(a => a.sold_final_debit > 0 || a.sold_final_credit > 0).length === 0" class="mobile-empty">
<i class="pi pi-info-circle"></i>
<p>Nu au fost găsite date</p>
</div>
</div>
<!-- Desktop: DataTable -->
<DataTable <DataTable
v-if="!isMobile"
:value="trialBalanceStore.trialBalanceData" :value="trialBalanceStore.trialBalanceData"
:loading="trialBalanceStore.isLoading" :loading="trialBalanceStore.isLoading"
:paginator="true" :paginator="true"
@@ -263,7 +342,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from "vue"; import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useToast } from "primevue/usetoast"; import { useToast } from "primevue/usetoast";
import { useCompanyStore } from "../stores/companies"; import { useCompanyStore } from "../stores/companies";
import { useTrialBalanceStore } from "../stores/trialBalance"; import { useTrialBalanceStore } from "../stores/trialBalance";
@@ -278,6 +357,19 @@ const periodStore = useAccountingPeriodStore();
// State // State
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null); const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
// Mobile state
const isMobile = ref(window.innerWidth < 768);
const showFilters = ref(false);
const actionsMenu = ref(null);
// Handle window resize
const handleResize = () => {
isMobile.value = window.innerWidth < 768;
if (!isMobile.value) {
showFilters.value = false; // Reset when switching to desktop
}
};
const localFilters = ref({ const localFilters = ref({
cont: "", cont: "",
denumire: "", denumire: "",
@@ -289,6 +381,38 @@ const currentPeriodText = computed(() => {
return periodStore.selectedPeriod?.display_name || ""; return periodStore.selectedPeriod?.display_name || "";
}); });
// Mobile: Check if any filter is active (non-default value)
const hasActiveFilters = computed(() => {
return localFilters.value.cont !== "" || localFilters.value.denumire !== "";
});
// Mobile: Actions menu items
const actionMenuItems = computed(() => [
{
label: "Resetează Filtre",
icon: "pi pi-filter-slash",
command: clearFilters,
},
{
label: "Export Excel",
icon: "pi pi-file-excel",
command: exportExcel,
disabled: !trialBalanceStore.hasData,
},
{
label: "Export PDF",
icon: "pi pi-file-pdf",
command: exportPDF,
disabled: !trialBalanceStore.hasData,
},
{ separator: true },
{
label: "Actualizează",
icon: "pi pi-refresh",
command: refreshData,
},
]);
// Methods // Methods
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
if (!amount || amount === 0) return "0,00"; if (!amount || amount === 0) return "0,00";
@@ -298,6 +422,26 @@ const formatCurrency = (amount) => {
}).format(amount); }).format(amount);
}; };
// Compact format for mobile totals (e.g., "449.881" instead of "449.881,12")
const formatCompact = (amount) => {
if (!amount || amount === 0) return "0";
const absAmount = Math.abs(amount);
if (absAmount >= 1000000) {
return new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 1,
}).format(amount / 1000000) + "M";
}
return new Intl.NumberFormat("ro-RO", {
maximumFractionDigits: 0,
}).format(amount);
};
// Truncate text for mobile cards
const truncate = (text, maxLength) => {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
};
const handleCompanyChange = async () => { const handleCompanyChange = async () => {
if (!selectedCompanyId.value) return; if (!selectedCompanyId.value) return;
@@ -715,6 +859,9 @@ const exportPDF = async () => {
// Lifecycle // Lifecycle
onMounted(async () => { onMounted(async () => {
// Add resize listener for mobile detection
window.addEventListener("resize", handleResize);
// Load companies if not loaded // Load companies if not loaded
if (!companyStore.hasCompanies) { if (!companyStore.hasCompanies) {
await companyStore.loadCompanies(); await companyStore.loadCompanies();
@@ -734,6 +881,10 @@ onMounted(async () => {
} }
}); });
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
// Watch for company changes // Watch for company changes
watch( watch(
() => companyStore.selectedCompany, () => companyStore.selectedCompany,