feat(ui-fixes-phase6): Complete US-701 - Reparare SpeedDial FAB pe Lista Bonuri

Implemented by Ralph autonomous loop.
Iteration: 1

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-13 22:29:57 +00:00
parent 6775565e01
commit cc52ad1850
9 changed files with 804 additions and 668 deletions

View File

@@ -133,6 +133,7 @@
v-model="showDrawer"
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
@logout="handleLogout"
/>
@@ -1078,7 +1079,7 @@ import { useRouter } from 'vue-router'
import { useToast } from 'primevue/usetoast'
import { useConfirm } from 'primevue/useconfirm'
import { useReceiptsStore } from '@data-entry/stores/receiptsStore'
import { useCompanyStore, useAuthStore } from '@data-entry/stores/sharedStores'
import { useCompanyStore, useAuthStore, useAccountingPeriodStore } from '@data-entry/stores/sharedStores'
import { useBatchProgressStore } from '@data-entry/stores/batchProgressStore'
import Menu from 'primevue/menu'
import Dialog from 'primevue/dialog'
@@ -1096,6 +1097,7 @@ import { exportToExcel, exportToPDF } from '@reports/utils/exportUtils'
import BatchGroupHeader from '@data-entry/components/bulk/BatchGroupHeader.vue'
import ProcessingStatusCell from '@data-entry/components/bulk/ProcessingStatusCell.vue'
import Paginator from 'primevue/paginator'
import SpeedDial from 'primevue/speeddial'
import { sseService } from '@data-entry/services/sseService'
const router = useRouter()
@@ -1103,6 +1105,7 @@ const toast = useToast()
const confirm = useConfirm()
const store = useReceiptsStore()
const companyStore = useCompanyStore()
const periodStore = useAccountingPeriodStore()
const batchProgressStore = useBatchProgressStore()
const authStore = useAuthStore()

View File

@@ -1,8 +1,8 @@
<template>
<!-- US-602: TabView for Clienți/Furnizori -->
<!-- US-608: Flat content structure (tabs controlled by parent view) -->
<div class="maturity-card">
<div class="card-header">
<h3>Analiză Scadențe</h3>
<!-- Period selector (only shown on desktop, mobile uses view header) -->
<div class="card-controls">
<select
v-model="selectedPeriod"
@change="handlePeriodChange"
@@ -29,123 +29,105 @@
<button @click="loadData" class="retry-btn">Încearcă din nou</button>
</div>
<!-- US-602: TabView for Clienți/Furnizori -->
<TabView v-else v-model:activeIndex="activeTabIndex" class="maturity-tabs">
<!-- Tab Clienți -->
<TabPanel>
<template #header>
<span class="tab-header">
<i class="pi pi-users"></i>
<span class="tab-title">Clienți</span>
<span class="tab-total">{{ formatCurrency(clientsTotal) }}</span>
</span>
</template>
<div class="tab-content">
<div class="maturity-list">
<div
v-for="(client, index) in clientsData"
:key="`client-${index}`"
class="maturity-item"
:class="{
overdue: client.daysOverdue > 0,
critical: client.daysOverdue > 30,
}"
>
<div class="item-info">
<span class="entity-name">{{ client.name }}</span>
<span class="due-info">
<span v-if="client.daysOverdue > 0" class="overdue-days">
Restant {{ client.daysOverdue }} zile
</span>
<span v-else class="due-date">
Scadent în {{ Math.abs(client.daysOverdue) }} zile
</span>
<!-- US-608: Content based on activeTab prop (no TabView) -->
<div v-else class="maturity-content">
<!-- Clienți Content -->
<div v-if="activeTab === 'clients'" class="tab-content">
<div class="maturity-list">
<div
v-for="(client, index) in clientsData"
:key="`client-${index}`"
class="maturity-item"
:class="{
overdue: client.daysOverdue > 0,
critical: client.daysOverdue > 30,
}"
>
<div class="item-info">
<span class="entity-name">{{ client.name }}</span>
<span class="due-info">
<span v-if="client.daysOverdue > 0" class="overdue-days">
Restant {{ client.daysOverdue }} zile
</span>
</div>
<div class="amount-bar">
<div class="bar-container">
<div
class="bar-fill clients-bar"
:style="{
width: getBarWidth(client.amount, maxClientAmount) + '%',
}"
></div>
</div>
<span class="amount-value">{{
formatCurrency(client.amount)
}}</span>
</div>
<span v-else class="due-date">
Scadent în {{ Math.abs(client.daysOverdue) }} zile
</span>
</span>
</div>
<div v-if="clientsData.length === 0" class="empty-state">
<i class="pi pi-inbox empty-icon"></i>
<p>Nu există facturi de încasat pentru această perioadă</p>
<div class="amount-bar">
<div class="bar-container">
<div
class="bar-fill clients-bar"
:style="{
width: getBarWidth(client.amount, maxClientAmount) + '%',
}"
></div>
</div>
<span class="amount-value">{{
formatCurrency(client.amount)
}}</span>
</div>
</div>
<div class="tab-summary">
<span class="summary-label">Total de încasat:</span>
<span class="summary-value clients-value">{{ formatCurrency(clientsTotal) }}</span>
<div v-if="clientsData.length === 0" class="empty-state">
<i class="pi pi-inbox empty-icon"></i>
<p>Nu există facturi de încasat pentru această perioadă</p>
</div>
</div>
</TabPanel>
<div class="tab-summary">
<span class="summary-label">Total de încasat:</span>
<span class="summary-value clients-value">{{ formatCurrency(clientsTotal) }}</span>
</div>
</div>
<!-- Tab Furnizori -->
<TabPanel>
<template #header>
<span class="tab-header">
<i class="pi pi-building"></i>
<span class="tab-title">Furnizori</span>
<span class="tab-total">{{ formatCurrency(suppliersTotal) }}</span>
</span>
</template>
<div class="tab-content">
<div class="maturity-list">
<div
v-for="(supplier, index) in suppliersData"
:key="`supplier-${index}`"
class="maturity-item"
:class="{
overdue: supplier.daysOverdue > 0,
critical: supplier.daysOverdue > 30,
}"
>
<div class="item-info">
<span class="entity-name">{{ supplier.name }}</span>
<span class="due-info">
<span v-if="supplier.daysOverdue > 0" class="overdue-days">
Restant {{ supplier.daysOverdue }} zile
</span>
<span v-else class="due-date">
Scadent în {{ Math.abs(supplier.daysOverdue) }} zile
</span>
<!-- Furnizori Content -->
<div v-else-if="activeTab === 'suppliers'" class="tab-content">
<div class="maturity-list">
<div
v-for="(supplier, index) in suppliersData"
:key="`supplier-${index}`"
class="maturity-item"
:class="{
overdue: supplier.daysOverdue > 0,
critical: supplier.daysOverdue > 30,
}"
>
<div class="item-info">
<span class="entity-name">{{ supplier.name }}</span>
<span class="due-info">
<span v-if="supplier.daysOverdue > 0" class="overdue-days">
Restant {{ supplier.daysOverdue }} zile
</span>
</div>
<div class="amount-bar">
<div class="bar-container">
<div
class="bar-fill suppliers-bar"
:style="{
width:
getBarWidth(supplier.amount, maxSupplierAmount) + '%',
}"
></div>
</div>
<span class="amount-value">{{
formatCurrency(supplier.amount)
}}</span>
</div>
<span v-else class="due-date">
Scadent în {{ Math.abs(supplier.daysOverdue) }} zile
</span>
</span>
</div>
<div v-if="suppliersData.length === 0" class="empty-state">
<i class="pi pi-inbox empty-icon"></i>
<p>Nu există facturi de plătit pentru această perioadă</p>
<div class="amount-bar">
<div class="bar-container">
<div
class="bar-fill suppliers-bar"
:style="{
width:
getBarWidth(supplier.amount, maxSupplierAmount) + '%',
}"
></div>
</div>
<span class="amount-value">{{
formatCurrency(supplier.amount)
}}</span>
</div>
</div>
<div class="tab-summary">
<span class="summary-label">Total de plătit:</span>
<span class="summary-value suppliers-value">{{ formatCurrency(suppliersTotal) }}</span>
<div v-if="suppliersData.length === 0" class="empty-state">
<i class="pi pi-inbox empty-icon"></i>
<p>Nu există facturi de plătit pentru această perioadă</p>
</div>
</div>
</TabPanel>
</TabView>
<div class="tab-summary">
<span class="summary-label">Total de plătit:</span>
<span class="summary-value suppliers-value">{{ formatCurrency(suppliersTotal) }}</span>
</div>
</div>
</div>
<!-- Balance Indicator -->
<div v-if="!isLoading && !error" class="balance-indicator">
@@ -196,54 +178,34 @@
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import TabView from "primevue/tabview";
import TabPanel from "primevue/tabpanel";
import { useDashboardStore } from "@reports/stores/dashboard";
// US-602: Tab state storage key
const TAB_STORAGE_KEY = "maturity_analysis_active_tab";
// Props
const props = defineProps({
companyId: {
type: [Number, String],
required: true,
},
// US-602: Allow external tab control (e.g., from route query)
initialTab: {
type: Number,
default: null,
// US-608: Tab is now controlled by parent view
activeTab: {
type: String,
default: 'clients',
validator: (value) => ['clients', 'suppliers'].includes(value)
},
});
// Emits
const emit = defineEmits(["periodChanged", "tabChanged"]);
const emit = defineEmits(["periodChanged"]);
// Store
const dashboardStore = useDashboardStore();
// US-602: Initialize tab from prop, localStorage, or default to 0 (Clienți)
const getInitialTabIndex = () => {
if (props.initialTab !== null) {
return props.initialTab;
}
const stored = localStorage.getItem(TAB_STORAGE_KEY);
return stored !== null ? parseInt(stored, 10) : 0;
};
// Reactive state
const activeTabIndex = ref(getInitialTabIndex());
const selectedPeriod = ref("1m");
const isLoading = ref(false);
const error = ref(null);
const lastUpdated = ref(null);
// US-602: Watch tab changes and persist to localStorage
watch(activeTabIndex, (newIndex) => {
localStorage.setItem(TAB_STORAGE_KEY, newIndex.toString());
emit("tabChanged", newIndex);
});
// Mock data structure - in production this would come from API
const maturityData = ref({
clients: [],
@@ -384,48 +346,31 @@ onMounted(() => {
</script>
<style scoped>
/* Base Card Styles */
/* US-608: Flat content structure (no card wrapper) */
.maturity-card {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--card-radius, 8px);
padding: 0;
box-shadow: var(--shadow-sm);
transition: all var(--transition-fast, 0.3s ease);
overflow: hidden;
}
.maturity-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--color-primary);
}
/* Card Header */
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg, 1rem);
border-bottom: 1px solid var(--color-border);
background: transparent;
padding: 0;
}
.card-header h3 {
margin: 0;
font-size: var(--text-lg, 1.125rem);
font-weight: var(--font-semibold, 600);
color: var(--color-text);
/* Card Controls (period selector) */
.card-controls {
display: flex;
justify-content: flex-end;
padding: var(--space-md);
background: var(--surface-card);
border-radius: var(--radius-md);
margin-bottom: var(--space-md);
}
.period-selector {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-sm, 0.875rem);
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--surface-border);
border-radius: var(--radius-sm);
background: var(--surface-card);
color: var(--text-color);
font-size: var(--text-sm);
cursor: pointer;
transition: border-color 0.2s ease;
transition: border-color var(--transition-fast);
}
.period-selector:hover {
@@ -437,6 +382,13 @@ onMounted(() => {
cursor: not-allowed;
}
/* Maturity Content Container */
.maturity-content {
background: var(--surface-card);
border-radius: var(--radius-md);
overflow: hidden;
}
/* Loading and Error States */
.loading-state,
.error-state {
@@ -478,38 +430,6 @@ onMounted(() => {
background: var(--color-primary-dark);
}
/* US-602: TabView Styles */
.maturity-tabs {
border: none;
}
/* Tab header styles */
.tab-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xs) 0;
}
.tab-header i {
font-size: var(--text-base);
color: var(--text-color-secondary);
}
.tab-title {
font-weight: var(--font-medium);
color: var(--text-color);
}
.tab-total {
font-size: var(--text-xs);
font-weight: var(--font-bold);
padding: var(--space-xs) var(--space-sm);
background: var(--surface-hover);
border-radius: var(--radius-sm);
color: var(--text-color);
}
/* Tab content */
.tab-content {
padding: var(--space-md);
@@ -820,37 +740,21 @@ onMounted(() => {
/* Responsive Design */
@media (max-width: 768px) {
.card-header {
flex-direction: column;
gap: var(--space-sm);
align-items: stretch;
}
.card-header h3 {
text-align: center;
font-size: var(--text-base);
.card-controls {
justify-content: center;
padding: var(--space-sm);
}
.period-selector {
width: 100%;
}
/* US-602: Mobile tab adjustments */
/* US-608: Mobile tab adjustments */
.tab-content {
padding: var(--space-sm);
min-height: 250px;
}
.tab-header {
flex-wrap: wrap;
gap: var(--space-xs);
}
.tab-total {
font-size: var(--text-xs);
padding: 2px var(--space-xs);
}
.tab-summary {
padding: var(--space-sm);
flex-direction: column;
@@ -876,22 +780,13 @@ onMounted(() => {
}
@media (max-width: 480px) {
.maturity-card {
margin: 0 calc(-1 * var(--space-sm));
}
.card-header,
.balance-indicator,
.card-footer {
padding: var(--space-md);
}
.maturity-list {
max-height: 200px;
}
.tab-title {
font-size: var(--text-sm);
.balance-indicator,
.card-footer {
padding: var(--space-md);
}
}
</style>

View File

@@ -1,23 +1,15 @@
<template>
<div class="cache-stats-view" :class="{ 'mobile-layout': isMobile }">
<!-- US-111: Mobile Material Design Top Bar -->
<!-- US-608: Mobile Material Design Top Bar with Back Button -->
<MobileTopBar
v-if="isMobile"
title="Statistici Cache"
:show-menu="true"
:show-back="true"
:actions="mobileTopBarActions"
@menu-click="showDrawer = true"
@back-click="router.push('/settings')"
@action-click="handleTopBarAction"
/>
<!-- Mobile Drawer Menu (replaces old Sidebar) -->
<MobileDrawerMenu
v-model="showDrawer"
:user="authStore.user"
:companies-store="companyStore"
@logout="handleLogout"
/>
<!-- Desktop Header -->
<div class="stats-header" v-if="!isMobile">
<h1>Cache Statistics</h1>
@@ -206,7 +198,7 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useCacheStore } from "@reports/stores/cacheStore";
import { useCompanyStore, useAuthStore } from "@reports/stores/sharedStores";
import { useCompanyStore, useAuthStore, useAccountingPeriodStore } from "@reports/stores/sharedStores";
import { useToast } from "primevue/usetoast";
import Button from "primevue/button";
import Card from "primevue/card";
@@ -218,15 +210,15 @@ import InputSwitch from "primevue/inputswitch";
import Dialog from "primevue/dialog";
import RadioButton from "primevue/radiobutton";
import Message from "primevue/message";
// US-111: Mobile Material Design components
// US-111/US-608: Mobile Material Design components
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 { useRouter } from "vue-router";
const router = useRouter();
const cacheStore = useCacheStore();
const companyStore = useCompanyStore();
const periodStore = useAccountingPeriodStore();
const toast = useToast();
const authStore = useAuthStore();
@@ -238,15 +230,8 @@ const userCacheEnabled = ref(true);
const showClearDialog = ref(false);
const clearScope = ref("current");
// US-111: Mobile state
// US-111/US-608: Mobile state
const isMobile = ref(window.innerWidth < 768);
const showDrawer = ref(false);
// Handle logout from drawer menu
const handleLogout = async () => {
await authStore.logout();
router.push('/login');
};
// US-111: Mobile TopBar actions (refresh only for cache stats)
const mobileTopBarActions = computed(() => [

View File

@@ -1,5 +1,5 @@
<template>
<!-- US-513: Simplified Mobile Top Bar - no filters/export since no detailed data -->
<!-- US-608: Mobile Top Bar with Back Button -->
<MobileTopBar
v-if="isMobile"
title="Scadențe"
@@ -7,12 +7,55 @@
@back-click="goBack"
/>
<main class="main-content" :class="{ 'mobile-layout': isMobile }">
<!-- US-608: Mobile Tabs (sticky below MobileTopBar) - like DetailedInvoicesView -->
<div v-if="isMobile && companyStore.selectedCompany" 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>
<main class="main-content" :class="{ 'mobile-layout': isMobile, 'has-tabs': isMobile && companyStore.selectedCompany }">
<div class="app-container">
<!-- Page Header - only on desktop -->
<div v-if="!isMobile" class="page-header">
<h1 class="page-title">Analiză Scadențe</h1>
<p class="page-subtitle">Analiza scadențelor clienți și furnizori</p>
<div class="header-top">
<div>
<h1 class="page-title">Analiză Scadențe</h1>
<p class="page-subtitle">Analiza scadențelor clienți și furnizori</p>
</div>
</div>
<!-- US-608: Desktop Tabs (in header) -->
<div v-if="companyStore.selectedCompany" 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-building"></i>
<span>Furnizori</span>
</button>
</div>
</div>
<!-- Loading state when no company selected -->
@@ -30,14 +73,13 @@
/>
</div>
<!-- Main content - MaturityAnalysisCard (US-513: doar analiza scadențelor, fără facturi detaliate) -->
<!-- US-602: Now with TabView for Clienți/Furnizori -->
<div v-else class="maturity-container">
<!-- US-608: Flat content structure - no card wrapper -->
<div v-else class="maturity-content">
<MaturityAnalysisCard
ref="maturityCardRef"
:companyId="companyStore.selectedCompany?.id_firma"
:activeTab="activeTab"
@periodChanged="handlePeriodChange"
@tabChanged="handleTabChange"
/>
</div>
</div>
@@ -49,7 +91,7 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import Button from 'primevue/button'
import MobileTopBar from '@shared/components/mobile/MobileTopBar.vue'
import MobileBottomNav from '@shared/components/mobile/MobileBottomNav.vue'
@@ -57,6 +99,7 @@ import MaturityAnalysisCard from '@reports/components/dashboard/cards/MaturityAn
import { useCompanyStore } from '@reports/stores/sharedStores'
const router = useRouter()
const route = useRoute()
const companyStore = useCompanyStore()
// Detectare mobile - reactive with resize listener
@@ -66,6 +109,30 @@ const isMobile = computed(() => windowWidth.value < 768)
// Ref to MaturityAnalysisCard
const maturityCardRef = ref(null)
// US-608: Tab state (persisted via URL query)
const TAB_STORAGE_KEY = 'maturity_analysis_active_tab'
const getInitialTab = () => {
// Check URL query first
if (route.query.tab === 'suppliers') return 'suppliers'
// Then localStorage
const stored = localStorage.getItem(TAB_STORAGE_KEY)
if (stored === 'suppliers') return 'suppliers'
return 'clients'
}
const activeTab = ref(getInitialTab())
// US-608: Switch tab and update URL
const switchTab = (tab) => {
activeTab.value = tab
localStorage.setItem(TAB_STORAGE_KEY, tab)
// Update URL without full navigation
router.replace({
query: tab === 'suppliers' ? { tab: 'suppliers' } : {}
})
}
// Handle window resize for mobile detection
const handleResize = () => {
windowWidth.value = window.innerWidth
@@ -81,11 +148,6 @@ const handlePeriodChange = (period) => {
console.log('Maturity period changed:', period)
}
// US-602: Handle tab change
const handleTabChange = (tabIndex) => {
console.log('Tab changed:', tabIndex === 0 ? 'Clienți' : 'Furnizori')
}
// Lifecycle
onMounted(() => {
window.addEventListener('resize', handleResize)
@@ -103,6 +165,11 @@ onUnmounted(() => {
padding-bottom: 56px;
}
/* US-608: Extra padding when tabs are visible on mobile */
.main-content.mobile-layout.has-tabs {
padding-top: calc(56px + 48px);
}
/* App container */
.app-container {
max-width: 1400px;
@@ -117,11 +184,17 @@ onUnmounted(() => {
}
/* Page Header - Desktop only */
/* US-514: Reduced from var(--space-xl) to var(--space-md) for less excessive top spacing */
.page-header {
margin-bottom: var(--space-md);
}
.header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-md);
}
.page-title {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
@@ -135,8 +208,102 @@ onUnmounted(() => {
margin: 0;
}
/* Maturity container */
.maturity-container {
/* ================================================
US-608: Mobile Tabs (sticky below MobileTopBar)
Copied from DetailedInvoicesView for consistency
================================================ */
.mobile-tabs-container {
position: fixed;
top: 56px;
left: 0;
right: 0;
z-index: var(--z-sticky, 100);
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 {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: var(--font-semibold);
}
.mobile-tab:hover:not(.active) {
background: var(--surface-hover);
}
.tab-label {
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ================================================
US-608: Desktop Tabs (in page-header)
================================================ */
.desktop-tabs {
display: flex;
gap: 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:not(.active) {
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, white);
}
.desktop-tab i {
font-size: var(--text-base);
}
/* ================================================
US-608: Flat content (no card wrapper)
================================================ */
.maturity-content {
display: flex;
flex-direction: column;
gap: var(--space-lg);
@@ -180,4 +347,40 @@ onUnmounted(() => {
.empty-action {
margin-top: var(--space-md);
}
/* ================================================
Dark Mode Support
================================================ */
[data-theme="dark"] .mobile-tabs-container {
background: var(--surface-card);
border-bottom-color: var(--surface-border);
}
[data-theme="dark"] .mobile-tab.active {
color: var(--blue-400);
border-bottom-color: var(--blue-400);
}
[data-theme="dark"] .desktop-tab.active {
background: var(--blue-600);
border-color: var(--blue-600);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .mobile-tabs-container {
background: var(--surface-card);
border-bottom-color: var(--surface-border);
}
:root:not([data-theme]) .mobile-tab.active {
color: var(--blue-400);
border-bottom-color: var(--blue-400);
}
:root:not([data-theme]) .desktop-tab.active {
background: var(--blue-600);
border-color: var(--blue-600);
}
}
</style>

View File

@@ -1,22 +1,15 @@
<template>
<div class="server-logs-view" :class="{ 'mobile-layout': isMobile }">
<!-- US-110: Mobile Material Design Top Bar -->
<!-- US-608: Mobile Material Design Top Bar with Back Button -->
<MobileTopBar
v-if="isMobile"
title="Loguri Server"
:show-menu="true"
:show-back="true"
:actions="mobileTopBarActions"
@menu-click="showDrawer = true"
@back-click="router.push('/settings')"
@action-click="handleTopBarAction"
/>
<!-- Mobile Drawer Menu (replaces old Sidebar) -->
<MobileDrawerMenu
v-model="showDrawer"
:user="authStore.user"
@logout="handleLogout"
/>
<!-- Desktop Header -->
<div class="stats-header" v-if="!isMobile">
<h1>
@@ -140,11 +133,9 @@ import Tag from 'primevue/tag'
import axios from 'axios'
import { useRouter } from 'vue-router'
// US-110: Mobile Material Design components
// US-110/US-608: Mobile Material Design components
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 { useAuthStore } from '@reports/stores/sharedStores'
// System API - endpoint separat de reports
const systemApi = axios.create({
@@ -162,7 +153,6 @@ systemApi.interceptors.request.use((config) => {
})
const router = useRouter()
const authStore = useAuthStore()
// State
const logs = ref([])
@@ -182,15 +172,8 @@ const debugInfo = ref({
let refreshTimer = null
// US-110: Mobile state
// US-110/US-608: Mobile state
const isMobile = ref(window.innerWidth < 768)
const showDrawer = ref(false)
// Handle logout from drawer menu
const handleLogout = async () => {
await authStore.logout()
router.push('/login')
}
// US-110: Mobile TopBar actions (refresh, export)
const mobileTopBarActions = computed(() => [

View File

@@ -12,111 +12,85 @@
</div>
<!-- Company & Period Selection (below header, above navigation) -->
<!-- US-605: Collapsible sections with localStorage persistence -->
<!-- US-608: Direct dropdowns (no collapsible sections) -->
<div v-if="companiesStore" class="drawer-selectors">
<!-- Company Selector (Collapsible) -->
<div class="selector-group" :class="{ 'is-collapsed': companyCollapsed }">
<!-- Collapsible Header -->
<!-- Company Selector (Direct Dropdown) -->
<div class="selector-group">
<label class="selector-label">Firma</label>
<button
class="collapsible-header"
@click="toggleCompanyCollapsed"
:aria-expanded="!companyCollapsed"
class="selector-trigger"
@click="toggleCompanyDropdown"
:aria-expanded="companyDropdownOpen"
>
<span class="collapsible-label">Firma</span>
<span v-if="companyCollapsed" class="collapsible-value">{{ selectedCompanyName }}</span>
<i class="pi" :class="companyCollapsed ? 'pi-chevron-down' : 'pi-chevron-up'"></i>
<div class="selector-value">
<span class="selector-main">{{ selectedCompanyName }}</span>
<span v-if="selectedCompanyCode" class="selector-sub">{{ selectedCompanyCode }}</span>
</div>
<i class="pi pi-chevron-down" :class="{ 'rotate-180': companyDropdownOpen }"></i>
</button>
<!-- Expanded Content -->
<div v-if="!companyCollapsed" class="collapsible-content">
<button
class="selector-trigger"
@click="toggleCompanyDropdown"
:aria-expanded="companyDropdownOpen"
>
<div class="selector-value">
<span class="selector-main">{{ selectedCompanyName }}</span>
<span v-if="selectedCompanyCode" class="selector-sub">{{ selectedCompanyCode }}</span>
</div>
<i class="pi pi-chevron-down" :class="{ 'rotate-180': companyDropdownOpen }"></i>
</button>
<!-- Company Dropdown Panel -->
<div v-if="companyDropdownOpen" class="selector-panel">
<div class="selector-search">
<i class="pi pi-search"></i>
<input
ref="companySearchInput"
type="text"
v-model="companySearchQuery"
placeholder="Caută firmă..."
class="selector-search-input"
/>
</div>
<div class="selector-list">
<div
v-for="company in filteredCompanies"
:key="company.id_firma"
class="selector-item"
:class="{ active: company.id_firma === companiesStore.selectedCompany?.id_firma }"
@click="selectCompany(company)"
>
<div class="selector-item-content">
<span class="selector-item-name">{{ company.name }}</span>
<span v-if="company.fiscal_code" class="selector-item-sub">CUI: {{ company.fiscal_code }}</span>
</div>
<i v-if="company.id_firma === companiesStore.selectedCompany?.id_firma" class="pi pi-check"></i>
</div>
<div v-if="filteredCompanies.length === 0" class="selector-empty">
<i class="pi pi-info-circle"></i>
<span>Nu s-au găsit firme</span>
<!-- Company Dropdown Panel -->
<div v-if="companyDropdownOpen" class="selector-panel">
<div class="selector-search">
<i class="pi pi-search"></i>
<input
ref="companySearchInput"
type="text"
v-model="companySearchQuery"
placeholder="Caută firmă..."
class="selector-search-input"
/>
</div>
<div class="selector-list">
<div
v-for="company in filteredCompanies"
:key="company.id_firma"
class="selector-item"
:class="{ active: company.id_firma === companiesStore.selectedCompany?.id_firma }"
@click="selectCompany(company)"
>
<div class="selector-item-content">
<span class="selector-item-name">{{ company.name }}</span>
<span v-if="company.fiscal_code" class="selector-item-sub">CUI: {{ company.fiscal_code }}</span>
</div>
<i v-if="company.id_firma === companiesStore.selectedCompany?.id_firma" class="pi pi-check"></i>
</div>
<div v-if="filteredCompanies.length === 0" class="selector-empty">
<i class="pi pi-info-circle"></i>
<span>Nu s-au găsit firme</span>
</div>
</div>
</div>
</div>
<!-- Period Selector (Collapsible) -->
<div v-if="periodStore && companiesStore.selectedCompany" class="selector-group" :class="{ 'is-collapsed': periodCollapsed }">
<!-- Collapsible Header -->
<!-- Period Selector (Direct Dropdown) -->
<div v-if="periodStore && companiesStore.selectedCompany" class="selector-group">
<label class="selector-label">Perioada</label>
<button
class="collapsible-header"
@click="togglePeriodCollapsed"
:aria-expanded="!periodCollapsed"
class="selector-trigger"
@click="togglePeriodDropdown"
:aria-expanded="periodDropdownOpen"
>
<span class="collapsible-label">Perioada</span>
<span v-if="periodCollapsed" class="collapsible-value">{{ selectedPeriodDisplay }}</span>
<i class="pi" :class="periodCollapsed ? 'pi-chevron-down' : 'pi-chevron-up'"></i>
<div class="selector-value">
<span class="selector-main">{{ selectedPeriodDisplay }}</span>
</div>
<i class="pi pi-chevron-down" :class="{ 'rotate-180': periodDropdownOpen }"></i>
</button>
<!-- Expanded Content -->
<div v-if="!periodCollapsed" class="collapsible-content">
<button
class="selector-trigger"
@click="togglePeriodDropdown"
:aria-expanded="periodDropdownOpen"
>
<div class="selector-value">
<span class="selector-main">{{ selectedPeriodDisplay }}</span>
<!-- Period Dropdown Panel -->
<div v-if="periodDropdownOpen" class="selector-panel">
<div class="selector-list">
<div
v-for="period in availablePeriods"
:key="`${period.an}-${period.luna}`"
class="selector-item"
:class="{ active: isPeriodSelected(period) }"
@click="selectPeriod(period)"
>
<span class="selector-item-name">{{ period.display_name }}</span>
<i v-if="isPeriodSelected(period)" class="pi pi-check"></i>
</div>
<i class="pi pi-chevron-down" :class="{ 'rotate-180': periodDropdownOpen }"></i>
</button>
<!-- Period Dropdown Panel -->
<div v-if="periodDropdownOpen" class="selector-panel">
<div class="selector-list">
<div
v-for="period in availablePeriods"
:key="`${period.an}-${period.luna}`"
class="selector-item"
:class="{ active: isPeriodSelected(period) }"
@click="selectPeriod(period)"
>
<span class="selector-item-name">{{ period.display_name }}</span>
<i v-if="isPeriodSelected(period)" class="pi pi-check"></i>
</div>
<div v-if="availablePeriods.length === 0" class="selector-empty">
<i class="pi pi-info-circle"></i>
<span>Nu sunt perioade disponibile</span>
</div>
<div v-if="availablePeriods.length === 0" class="selector-empty">
<i class="pi pi-info-circle"></i>
<span>Nu sunt perioade disponibile</span>
</div>
</div>
</div>
@@ -252,8 +226,8 @@ import { computed, ref, watch, nextTick, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
/**
* MobileDrawerMenu - Material Design 3 inspired navigation drawer for mobile (v3)
* US-605: Added collapsible Firma/Perioada sections
* MobileDrawerMenu - Material Design 3 inspired navigation drawer for mobile (v4)
* US-608: Direct dropdown selectors (removed collapsible sections)
*
* Props:
* - modelValue (v-model): Controls visibility of the drawer
@@ -271,15 +245,16 @@ import { useRoute, useRouter } from 'vue-router'
* Features:
* - Slide-in animation from left
* - Header with ROA2WEB logo
* - Company & Period selectors (below header, like desktop)
* - Company & Period selectors as direct dropdowns (1-tap interaction)
* - Navigation organized into 4 category sections:
* - PRINCIPALE: Dashboard, Bonuri
* - RAPOARTE: Facturi, Balanță, Casă, Bancă (US-519: separate pages)
* - RAPOARTE: Facturi, Balanță, Casă, Bancă
* - ANALIZE: Scadențe, Facturi Detaliate
* - ADMINISTRARE: Setări
* - Visual separators between sections
* - Active state highlighting based on current route
* - Profile section with user name and logout button (footer)
* - Profile section with user name, logout button, and theme toggle
* - Theme toggle cycles through: Auto → Light → Dark
* - Close on tap outside or on link click
* - Full dark mode support
* - Teleported to body to avoid z-index issues
@@ -341,55 +316,7 @@ const companySearchInput = ref(null)
// Period selector state
const periodDropdownOpen = ref(false)
// Collapsible section state (US-605)
// Default to collapsed, persisted in localStorage
const COLLAPSED_STORAGE_KEY = 'mobile-drawer-sections-collapsed'
const loadCollapsedState = () => {
try {
const saved = localStorage.getItem(COLLAPSED_STORAGE_KEY)
if (saved) {
return JSON.parse(saved)
}
} catch (e) {
// Ignore parse errors
}
// Default: both sections collapsed
return { company: true, period: true }
}
const savedCollapsedState = loadCollapsedState()
const companyCollapsed = ref(savedCollapsedState.company)
const periodCollapsed = ref(savedCollapsedState.period)
const saveCollapsedState = () => {
try {
localStorage.setItem(COLLAPSED_STORAGE_KEY, JSON.stringify({
company: companyCollapsed.value,
period: periodCollapsed.value
}))
} catch (e) {
// Ignore storage errors
}
}
const toggleCompanyCollapsed = () => {
companyCollapsed.value = !companyCollapsed.value
// Close dropdown if collapsing
if (companyCollapsed.value) {
companyDropdownOpen.value = false
}
saveCollapsedState()
}
const togglePeriodCollapsed = () => {
periodCollapsed.value = !periodCollapsed.value
// Close dropdown if collapsing
if (periodCollapsed.value) {
periodDropdownOpen.value = false
}
saveCollapsedState()
}
// US-608: Removed collapsible state management - using direct dropdowns now
// Computed properties for company selector
const selectedCompanyName = computed(() => {
@@ -855,67 +782,9 @@ onMounted(() => {
}
/* ================================================
Collapsible Sections (US-605)
US-608: Collapsible styles removed - using direct dropdowns
================================================ */
.collapsible-header {
display: flex;
align-items: center;
width: 100%;
padding: var(--space-sm) var(--space-md);
background: transparent;
border: none;
cursor: pointer;
min-height: 44px;
gap: var(--space-sm);
transition: background var(--transition-fast);
border-radius: var(--radius-md);
}
.collapsible-header:hover {
background: var(--surface-hover);
}
.collapsible-header:active {
background: var(--surface-hover);
}
.collapsible-label {
font-size: var(--text-xs);
font-weight: var(--font-medium);
color: var(--text-color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.collapsible-value {
flex: 1;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
.collapsible-header .pi {
font-size: var(--text-xs);
color: var(--text-color-secondary);
flex-shrink: 0;
transition: transform var(--transition-fast);
}
.collapsible-content {
padding-top: var(--space-xs);
}
/* When collapsed, the selector-group has minimal padding */
.selector-group.is-collapsed {
padding-bottom: 0;
}
/* ================================================
Navigation Sections Container
US-606: No longer independently scrollable, part of unified scroll
@@ -1022,9 +891,11 @@ onMounted(() => {
gap: var(--space-xs);
}
/* US-607: Compact variant - reduced spacing */
/* US-607/US-608: Compact variant - reduced spacing but with MobileBottomNav clearance */
.drawer-profile--compact {
padding: var(--space-sm) var(--space-md);
/* US-608: Ensure theme toggle is visible above MobileBottomNav (56px) */
padding-bottom: calc(56px + var(--space-md));
gap: var(--space-xs);
}
@@ -1279,23 +1150,6 @@ onMounted(() => {
color: var(--text-color-secondary);
}
/* Dark mode: Collapsible Sections (US-605) */
[data-theme="dark"] .collapsible-header:hover {
background: var(--surface-hover);
}
[data-theme="dark"] .collapsible-label {
color: var(--text-color-secondary);
}
[data-theme="dark"] .collapsible-value {
color: var(--text-color);
}
[data-theme="dark"] .collapsible-header .pi {
color: var(--text-color-secondary);
}
[data-theme="dark"] .drawer-link {
color: var(--text-color);
}
@@ -1479,23 +1333,6 @@ onMounted(() => {
color: var(--text-color-secondary);
}
/* Auto dark mode: Collapsible Sections (US-605) */
:root:not([data-theme]) .collapsible-header:hover {
background: var(--surface-hover);
}
:root:not([data-theme]) .collapsible-label {
color: var(--text-color-secondary);
}
:root:not([data-theme]) .collapsible-value {
color: var(--text-color);
}
:root:not([data-theme]) .collapsible-header .pi {
color: var(--text-color-secondary);
}
:root:not([data-theme]) .drawer-link {
color: var(--text-color);
}