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",
"Verify in browser: Tabs switch between Clienți and Furnizori correctly"
],
"passes": false,
"notes": "Folosește PrimeVue TabView, modifică src/modules/reports/views/MaturityAnalysisView.vue"
"passes": true,
"notes": "Completed in iteration 2"
},
{
"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] 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] 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>
<!-- US-602: TabView for Clienți/Furnizori -->
<div class="maturity-card">
<div class="card-header">
<h3>Analiză Comparativă Scadențe</h3>
<h3>Analiză Scadențe</h3>
<select
v-model="selectedPeriod"
@change="handlePeriodChange"
@@ -28,105 +29,123 @@
<button @click="loadData" class="retry-btn">Încearcă din nou</button>
</div>
<div v-else class="maturity-comparison">
<!-- Clients Side -->
<div class="clients-side">
<h4 class="side-title clients-title">
Clienți - De încasat
<span class="total-amount">{{ formatCurrency(clientsTotal) }}</span>
</h4>
<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="client-name">{{ client.name }}</span>
<span class="due-info">
<span v-if="client.daysOverdue > 0" class="overdue-days">
Restant {{ client.daysOverdue }} zile
<!-- 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>
</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>
<span class="amount-value">{{
formatCurrency(client.amount)
}}</span>
<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 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 v-if="clientsData.length === 0" class="empty-state">
<p>Nu există facturi de încasat pentru această perioadă</p>
<div class="tab-summary">
<span class="summary-label">Total de încasat:</span>
<span class="summary-value clients-value">{{ formatCurrency(clientsTotal) }}</span>
</div>
</div>
</div>
</TabPanel>
<!-- Divider -->
<div class="comparison-divider"></div>
<!-- Suppliers Side -->
<div class="suppliers-side">
<h4 class="side-title suppliers-title">
Furnizori - De plătit
<span class="total-amount">{{ formatCurrency(suppliersTotal) }}</span>
</h4>
<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="supplier-name">{{ supplier.name }}</span>
<span class="due-info">
<span v-if="supplier.daysOverdue > 0" class="overdue-days">
Restant {{ supplier.daysOverdue }} zile
<!-- 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>
</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>
<span class="amount-value">{{
formatCurrency(supplier.amount)
}}</span>
<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 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 v-if="suppliersData.length === 0" class="empty-state">
<p>Nu există facturi de plătit pentru această perioadă</p>
<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>
</div>
</TabPanel>
</TabView>
<!-- Balance Indicator -->
<div v-if="!isLoading && !error" class="balance-indicator">
@@ -177,28 +196,54 @@
<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,
},
});
// Emits
const emit = defineEmits(["periodChanged"]);
const emit = defineEmits(["periodChanged", "tabChanged"]);
// 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: [],
@@ -433,46 +478,89 @@ onMounted(() => {
background: var(--color-primary-dark);
}
/* Comparison Layout */
.maturity-comparison {
display: grid;
grid-template-columns: 1fr 1px 1fr;
gap: var(--space-lg, 1rem);
padding: var(--space-lg, 1rem);
min-height: 300px;
/* US-602: TabView Styles */
.maturity-tabs {
border: none;
}
.comparison-divider {
background: var(--color-border);
margin: var(--space-md, 1rem) 0;
}
/* Side Headers */
.side-title {
/* 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);
min-height: 280px;
display: flex;
flex-direction: column;
}
/* Tab summary */
.tab-summary {
display: flex;
justify-content: space-between;
margin: 0 0 var(--space-md, 1rem) 0;
font-size: var(--text-base, 1rem);
font-weight: var(--font-semibold, 600);
padding-bottom: var(--space-sm, 0.5rem);
border-bottom: 1px solid var(--color-border);
align-items: center;
padding: var(--space-md);
margin-top: auto;
background: var(--surface-hover);
border-radius: var(--radius-md);
}
.clients-title {
color: var(--color-text);
.summary-label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-color-secondary);
}
.suppliers-title {
color: var(--color-text);
.summary-value {
font-size: var(--text-lg);
font-weight: var(--font-bold);
}
.total-amount {
font-size: var(--text-sm, 0.875rem);
font-weight: var(--font-bold, 700);
padding: 0.25rem 0.5rem;
background: var(--color-bg-secondary, #f8f9fa);
border-radius: var(--radius-sm, 4px);
.summary-value.clients-value {
color: var(--color-primary);
}
.summary-value.suppliers-value {
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 */
@@ -528,12 +616,7 @@ onMounted(() => {
margin-bottom: var(--space-xs, 0.25rem);
}
.client-name,
.supplier-name {
font-weight: var(--font-medium, 500);
color: var(--color-text);
font-size: var(--text-sm, 0.875rem);
}
/* entity-name defined in TabView styles above */
.due-info {
font-size: var(--text-xs, 0.75rem);
@@ -736,35 +819,43 @@ onMounted(() => {
}
/* 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) {
.card-header {
flex-direction: column;
gap: var(--space-sm, 0.5rem);
gap: var(--space-sm);
align-items: stretch;
}
.card-header h3 {
text-align: center;
font-size: var(--text-base, 1rem);
font-size: var(--text-base);
}
.period-selector {
width: 100%;
}
.maturity-comparison {
padding: var(--space-md, 0.75rem);
/* US-602: 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;
gap: var(--space-xs);
text-align: center;
}
.balance-content {
@@ -779,35 +870,28 @@ onMounted(() => {
.card-footer {
flex-direction: column;
gap: var(--space-sm, 0.5rem);
gap: var(--space-sm);
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) {
.maturity-card {
margin: 0 -var(--space-sm, 0.5rem);
margin: 0 calc(-1 * var(--space-sm));
}
.card-header,
.maturity-comparison,
.balance-indicator,
.card-footer {
padding: var(--space-md, 0.75rem);
padding: var(--space-md);
}
.maturity-list {
max-height: 200px;
}
.tab-title {
font-size: var(--text-sm);
}
}
</style>

View File

@@ -31,11 +31,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">
<MaturityAnalysisCard
ref="maturityCardRef"
:companyId="companyStore.selectedCompany?.id_firma"
@periodChanged="handlePeriodChange"
@tabChanged="handleTabChange"
/>
</div>
</div>
@@ -79,6 +81,11 @@ 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)