feat(ui-fixes-phase6): Complete US-602 - Tab-uri Clienți/Furnizori în Pagina Scadențe

Implemented by Ralph autonomous loop.
Iteration: 2

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-13 16:08:47 +00:00
parent 395eb2bc9c
commit ea82f61a74
4 changed files with 251 additions and 154 deletions

View File

@@ -67,8 +67,8 @@
"npm run typecheck passes", "npm run typecheck passes",
"Verify in browser: Tabs switch between Clienți and Furnizori correctly" "Verify in browser: Tabs switch between Clienți and Furnizori correctly"
], ],
"passes": false, "passes": true,
"notes": "Folosește PrimeVue TabView, modifică src/modules/reports/views/MaturityAnalysisView.vue" "notes": "Completed in iteration 2"
}, },
{ {
"id": "US-603", "id": "US-603",

View File

@@ -249,3 +249,9 @@ PRD: tasks/prd-ui-fixes-phase6.md
[2026-01-13 16:00:01] Working on story: US-601 [2026-01-13 16:00:01] Working on story: US-601
[2026-01-13 16:00:01] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_1_US-601.log) [2026-01-13 16:00:01] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_1_US-601.log)
[2026-01-13 16:02:32] SUCCESS: Story US-601 passed! [2026-01-13 16:02:32] SUCCESS: Story US-601 passed!
[2026-01-13 16:02:32] Changes committed
[2026-01-13 16:02:32] Progress: 1/10 stories completed
[2026-01-13 16:02:34] === Iteration 2/30 ===
[2026-01-13 16:02:34] Working on story: US-602
[2026-01-13 16:02:34] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_2_US-602.log)
[2026-01-13 16:08:47] SUCCESS: Story US-602 passed!

View File

@@ -1,7 +1,8 @@
<template> <template>
<!-- US-602: TabView for Clienți/Furnizori -->
<div class="maturity-card"> <div class="maturity-card">
<div class="card-header"> <div class="card-header">
<h3>Analiză Comparativă Scadențe</h3> <h3>Analiză Scadențe</h3>
<select <select
v-model="selectedPeriod" v-model="selectedPeriod"
@change="handlePeriodChange" @change="handlePeriodChange"
@@ -28,105 +29,123 @@
<button @click="loadData" class="retry-btn">Încearcă din nou</button> <button @click="loadData" class="retry-btn">Încearcă din nou</button>
</div> </div>
<div v-else class="maturity-comparison"> <!-- US-602: TabView for Clienți/Furnizori -->
<!-- Clients Side --> <TabView v-else v-model:activeIndex="activeTabIndex" class="maturity-tabs">
<div class="clients-side"> <!-- Tab Clienți -->
<h4 class="side-title clients-title"> <TabPanel>
Clienți - De încasat <template #header>
<span class="total-amount">{{ formatCurrency(clientsTotal) }}</span> <span class="tab-header">
</h4> <i class="pi pi-users"></i>
<div class="maturity-list"> <span class="tab-title">Clienți</span>
<div <span class="tab-total">{{ formatCurrency(clientsTotal) }}</span>
v-for="(client, index) in clientsData" </span>
:key="`client-${index}`" </template>
class="maturity-item" <div class="tab-content">
:class="{ <div class="maturity-list">
overdue: client.daysOverdue > 0, <div
critical: client.daysOverdue > 30, v-for="(client, index) in clientsData"
}" :key="`client-${index}`"
> class="maturity-item"
<div class="item-info"> :class="{
<span class="client-name">{{ client.name }}</span> overdue: client.daysOverdue > 0,
<span class="due-info"> critical: client.daysOverdue > 30,
<span v-if="client.daysOverdue > 0" class="overdue-days"> }"
Restant {{ client.daysOverdue }} zile >
<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>
</span> </span>
<span v-else class="due-date">
Scadent în {{ Math.abs(client.daysOverdue) }} zile
</span>
</span>
</div>
<div class="amount-bar">
<div class="bar-container">
<div
class="bar-fill clients-bar"
:style="{
width: getBarWidth(client.amount, maxClientAmount) + '%',
}"
></div>
</div> </div>
<span class="amount-value">{{ <div class="amount-bar">
formatCurrency(client.amount) <div class="bar-container">
}}</span> <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 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>
</div> </div>
<div v-if="clientsData.length === 0" class="empty-state"> <div class="tab-summary">
<p>Nu există facturi de încasat pentru această perioadă</p> <span class="summary-label">Total de încasat:</span>
<span class="summary-value clients-value">{{ formatCurrency(clientsTotal) }}</span>
</div> </div>
</div> </div>
</div> </TabPanel>
<!-- Divider --> <!-- Tab Furnizori -->
<div class="comparison-divider"></div> <TabPanel>
<template #header>
<!-- Suppliers Side --> <span class="tab-header">
<div class="suppliers-side"> <i class="pi pi-building"></i>
<h4 class="side-title suppliers-title"> <span class="tab-title">Furnizori</span>
Furnizori - De plătit <span class="tab-total">{{ formatCurrency(suppliersTotal) }}</span>
<span class="total-amount">{{ formatCurrency(suppliersTotal) }}</span> </span>
</h4> </template>
<div class="maturity-list"> <div class="tab-content">
<div <div class="maturity-list">
v-for="(supplier, index) in suppliersData" <div
:key="`supplier-${index}`" v-for="(supplier, index) in suppliersData"
class="maturity-item" :key="`supplier-${index}`"
:class="{ class="maturity-item"
overdue: supplier.daysOverdue > 0, :class="{
critical: supplier.daysOverdue > 30, overdue: supplier.daysOverdue > 0,
}" critical: supplier.daysOverdue > 30,
> }"
<div class="item-info"> >
<span class="supplier-name">{{ supplier.name }}</span> <div class="item-info">
<span class="due-info"> <span class="entity-name">{{ supplier.name }}</span>
<span v-if="supplier.daysOverdue > 0" class="overdue-days"> <span class="due-info">
Restant {{ supplier.daysOverdue }} zile <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>
</span> </span>
<span v-else class="due-date">
Scadent în {{ Math.abs(supplier.daysOverdue) }} zile
</span>
</span>
</div>
<div class="amount-bar">
<div class="bar-container">
<div
class="bar-fill suppliers-bar"
:style="{
width:
getBarWidth(supplier.amount, maxSupplierAmount) + '%',
}"
></div>
</div> </div>
<span class="amount-value">{{ <div class="amount-bar">
formatCurrency(supplier.amount) <div class="bar-container">
}}</span> <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 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>
</div> </div>
<div v-if="suppliersData.length === 0" class="empty-state"> <div class="tab-summary">
<p>Nu există facturi de plătit pentru această perioadă</p> <span class="summary-label">Total de plătit:</span>
<span class="summary-value suppliers-value">{{ formatCurrency(suppliersTotal) }}</span>
</div> </div>
</div> </div>
</div> </TabPanel>
</div> </TabView>
<!-- Balance Indicator --> <!-- Balance Indicator -->
<div v-if="!isLoading && !error" class="balance-indicator"> <div v-if="!isLoading && !error" class="balance-indicator">
@@ -177,28 +196,54 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch } from "vue"; import { ref, computed, onMounted, watch } from "vue";
import TabView from "primevue/tabview";
import TabPanel from "primevue/tabpanel";
import { useDashboardStore } from "@reports/stores/dashboard"; import { useDashboardStore } from "@reports/stores/dashboard";
// US-602: Tab state storage key
const TAB_STORAGE_KEY = "maturity_analysis_active_tab";
// Props // Props
const props = defineProps({ const props = defineProps({
companyId: { companyId: {
type: [Number, String], type: [Number, String],
required: true, required: true,
}, },
// US-602: Allow external tab control (e.g., from route query)
initialTab: {
type: Number,
default: null,
},
}); });
// Emits // Emits
const emit = defineEmits(["periodChanged"]); const emit = defineEmits(["periodChanged", "tabChanged"]);
// Store // Store
const dashboardStore = useDashboardStore(); 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 // Reactive state
const activeTabIndex = ref(getInitialTabIndex());
const selectedPeriod = ref("1m"); const selectedPeriod = ref("1m");
const isLoading = ref(false); const isLoading = ref(false);
const error = ref(null); const error = ref(null);
const lastUpdated = 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 // Mock data structure - in production this would come from API
const maturityData = ref({ const maturityData = ref({
clients: [], clients: [],
@@ -433,46 +478,89 @@ onMounted(() => {
background: var(--color-primary-dark); background: var(--color-primary-dark);
} }
/* Comparison Layout */ /* US-602: TabView Styles */
.maturity-comparison { .maturity-tabs {
display: grid; border: none;
grid-template-columns: 1fr 1px 1fr;
gap: var(--space-lg, 1rem);
padding: var(--space-lg, 1rem);
min-height: 300px;
} }
.comparison-divider { /* Tab header styles */
background: var(--color-border); .tab-header {
margin: var(--space-md, 1rem) 0;
}
/* Side Headers */
.side-title {
display: flex; display: flex;
align-items: center; 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);
min-height: 280px;
display: flex;
flex-direction: column;
}
/* Tab summary */
.tab-summary {
display: flex;
justify-content: space-between; justify-content: space-between;
margin: 0 0 var(--space-md, 1rem) 0; align-items: center;
font-size: var(--text-base, 1rem); padding: var(--space-md);
font-weight: var(--font-semibold, 600); margin-top: auto;
padding-bottom: var(--space-sm, 0.5rem); background: var(--surface-hover);
border-bottom: 1px solid var(--color-border); border-radius: var(--radius-md);
} }
.clients-title { .summary-label {
color: var(--color-text); font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-color-secondary);
} }
.suppliers-title { .summary-value {
color: var(--color-text); font-size: var(--text-lg);
font-weight: var(--font-bold);
} }
.total-amount { .summary-value.clients-value {
font-size: var(--text-sm, 0.875rem); color: var(--color-primary);
font-weight: var(--font-bold, 700); }
padding: 0.25rem 0.5rem;
background: var(--color-bg-secondary, #f8f9fa); .summary-value.suppliers-value {
border-radius: var(--radius-sm, 4px); color: var(--color-secondary, #6b7280);
}
/* Entity name (unified for clients and suppliers) */
.entity-name {
font-weight: var(--font-medium);
color: var(--text-color);
font-size: var(--text-sm);
}
/* Empty state icon */
.empty-icon {
font-size: var(--text-4xl);
color: var(--text-color-secondary);
opacity: 0.5;
margin-bottom: var(--space-md);
} }
/* Maturity Lists */ /* Maturity Lists */
@@ -528,12 +616,7 @@ onMounted(() => {
margin-bottom: var(--space-xs, 0.25rem); margin-bottom: var(--space-xs, 0.25rem);
} }
.client-name, /* entity-name defined in TabView styles above */
.supplier-name {
font-weight: var(--font-medium, 500);
color: var(--color-text);
font-size: var(--text-sm, 0.875rem);
}
.due-info { .due-info {
font-size: var(--text-xs, 0.75rem); font-size: var(--text-xs, 0.75rem);
@@ -736,35 +819,43 @@ onMounted(() => {
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 1024px) {
.maturity-comparison {
grid-template-columns: 1fr;
gap: var(--space-md, 1rem);
}
.comparison-divider {
display: none;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.card-header { .card-header {
flex-direction: column; flex-direction: column;
gap: var(--space-sm, 0.5rem); gap: var(--space-sm);
align-items: stretch; align-items: stretch;
} }
.card-header h3 { .card-header h3 {
text-align: center; text-align: center;
font-size: var(--text-base, 1rem); font-size: var(--text-base);
} }
.period-selector { .period-selector {
width: 100%; width: 100%;
} }
.maturity-comparison { /* US-602: Mobile tab adjustments */
padding: var(--space-md, 0.75rem); .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;
gap: var(--space-xs);
text-align: center;
} }
.balance-content { .balance-content {
@@ -779,35 +870,28 @@ onMounted(() => {
.card-footer { .card-footer {
flex-direction: column; flex-direction: column;
gap: var(--space-sm, 0.5rem); gap: var(--space-sm);
align-items: center; align-items: center;
} }
.side-title {
flex-direction: column;
align-items: flex-start;
gap: var(--space-xs, 0.25rem);
}
.total-amount {
align-self: flex-end;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.maturity-card { .maturity-card {
margin: 0 -var(--space-sm, 0.5rem); margin: 0 calc(-1 * var(--space-sm));
} }
.card-header, .card-header,
.maturity-comparison,
.balance-indicator, .balance-indicator,
.card-footer { .card-footer {
padding: var(--space-md, 0.75rem); padding: var(--space-md);
} }
.maturity-list { .maturity-list {
max-height: 200px; max-height: 200px;
} }
.tab-title {
font-size: var(--text-sm);
}
} }
</style> </style>

View File

@@ -31,11 +31,13 @@
</div> </div>
<!-- Main content - MaturityAnalysisCard (US-513: doar analiza scadențelor, fără facturi detaliate) --> <!-- 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"> <div v-else class="maturity-container">
<MaturityAnalysisCard <MaturityAnalysisCard
ref="maturityCardRef" ref="maturityCardRef"
:companyId="companyStore.selectedCompany?.id_firma" :companyId="companyStore.selectedCompany?.id_firma"
@periodChanged="handlePeriodChange" @periodChanged="handlePeriodChange"
@tabChanged="handleTabChange"
/> />
</div> </div>
</div> </div>
@@ -79,6 +81,11 @@ const handlePeriodChange = (period) => {
console.log('Maturity period changed:', 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 // Lifecycle
onMounted(() => { onMounted(() => {
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)