feat(unified-mobile-desktop-ui): Complete US-509 - Fix Detailed Invoices - Grupuri Expandabile Desktop

Implemented by Ralph autonomous loop.
Iteration: 9

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-12 22:48:38 +00:00
parent bb9e336e2f
commit 1ad2c25933
3 changed files with 351 additions and 68 deletions

View File

@@ -202,8 +202,8 @@
"npm run build passes",
"Verify in browser desktop: grupurile se extind la click"
],
"passes": false,
"notes": ""
"passes": true,
"notes": "Completed in iteration 9"
},
{
"id": "US-510",

View File

@@ -118,3 +118,9 @@ Design Reference: src/modules/reports/views/InvoicesView.vue
[2026-01-12 22:42:56] Working on story: US-508
[2026-01-12 22:42:56] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_8_US-508.log)
[2026-01-12 22:45:25] SUCCESS: Story US-508 passed!
[2026-01-12 22:45:25] Changes committed
[2026-01-12 22:45:25] Progress: 8/19 stories completed
[2026-01-12 22:45:27] === Iteration 9/100 ===
[2026-01-12 22:45:27] Working on story: US-509
[2026-01-12 22:45:27] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_9_US-509.log)
[2026-01-12 22:48:38] SUCCESS: Story US-509 passed!

View File

@@ -134,80 +134,142 @@
<div v-else class="data-section">
<!-- Desktop Table -->
<div v-if="!isMobile" class="table-wrapper">
<!-- Treasury DataTable (no expansion needed) -->
<DataTable
:value="selectedType === 'treasury' ? paginatedData : paginatedGroups"
v-if="selectedType === 'treasury'"
:value="paginatedData"
:loading="isLoading"
stripedRows
class="p-datatable-sm"
>
<!-- Treasury columns -->
<template v-if="selectedType === 'treasury'">
<Column field="cont" header="Cont" sortable></Column>
<Column field="nume_cont" header="Nume Cont" sortable></Column>
<Column field="sold" header="Sold" sortable>
<template #body="slotProps">
<span class="font-mono">{{ formatCurrency(slotProps.data.sold) }}</span>
</template>
</Column>
<Column field="valuta" header="Valută" sortable></Column>
<Column field="tip" header="Tip" sortable></Column>
</template>
<Column field="cont" header="Cont" sortable></Column>
<Column field="nume_cont" header="Nume Cont" sortable></Column>
<Column field="sold" header="Sold" sortable>
<template #body="slotProps">
<span class="font-mono">{{ formatCurrency(slotProps.data.sold) }}</span>
</template>
</Column>
<Column field="valuta" header="Valută" sortable></Column>
<Column field="tip" header="Tip" sortable></Column>
</DataTable>
<!-- Clients/Suppliers columns -->
<template v-else>
<Column :field="selectedType === 'clients' ? 'client' : 'furnizor'" :header="selectedType === 'clients' ? 'Client' : 'Furnizor'" sortable>
<template #body="slotProps">
<div class="name-cell">
<strong>{{ slotProps.data.name }}</strong>
<span v-if="slotProps.data.facturi?.length > 1" class="count-badge">
({{ slotProps.data.facturi.length }})
<!-- Clients/Suppliers Expandable Groups -->
<div v-else class="expandable-groups-table">
<!-- Table Header -->
<div class="groups-table-header">
<div class="header-cell expand-col"></div>
<div class="header-cell name-col">{{ selectedType === 'clients' ? 'Client' : 'Furnizor' }}</div>
<div class="header-cell">Nr. Document</div>
<div class="header-cell">Data Document</div>
<div class="header-cell">Data Scadență</div>
<div class="header-cell text-right">Facturat</div>
<div class="header-cell text-right">{{ selectedType === 'clients' ? 'Încasat' : 'Achitat' }}</div>
<div class="header-cell text-right">Sold</div>
</div>
<!-- Table Body - Groups -->
<div class="groups-table-body">
<template v-for="group in paginatedGroups" :key="group.name">
<!-- Group Header Row (clickable for expand) -->
<div
class="group-row"
:class="{
'expandable': group.facturi.length > 1,
'expanded': isGroupExpanded(group.name)
}"
@click="group.facturi.length > 1 && toggleGroup(group.name)"
>
<div class="row-cell expand-col">
<i
v-if="group.facturi.length > 1"
class="expand-icon pi pi-chevron-right"
:class="{ 'rotated': isGroupExpanded(group.name) }"
></i>
</div>
<div class="row-cell name-col">
<strong>{{ group.name }}</strong>
<span v-if="group.facturi.length > 1" class="count-badge">
({{ group.facturi.length }} facturi)
</span>
</div>
</template>
</Column>
<Column field="numar_document" header="Nr. Document">
<template #body="slotProps">
{{ slotProps.data.facturi?.length === 1 ? slotProps.data.facturi[0].numar_document : '-' }}
</template>
</Column>
<Column field="data_document" header="Data Document">
<template #body="slotProps">
{{ slotProps.data.facturi?.length === 1 ? formatDate(slotProps.data.facturi[0].data_document) : '-' }}
</template>
</Column>
<Column field="data_scadenta" header="Data Scadență">
<template #body="slotProps">
{{ slotProps.data.facturi?.length === 1 ? formatDate(slotProps.data.facturi[0].data_scadenta) : '-' }}
</template>
</Column>
<Column header="Facturat">
<template #body="slotProps">
<span class="font-mono">
{{ slotProps.data.facturi?.length === 1 ? formatCurrency(slotProps.data.facturi[0].facturat) : '-' }}
</span>
</template>
</Column>
<Column :header="selectedType === 'clients' ? 'Încasat' : 'Achitat'">
<template #body="slotProps">
<span class="font-mono">
{{ slotProps.data.facturi?.length === 1
? formatCurrency(slotProps.data.facturi[0][selectedType === 'clients' ? 'incasat' : 'achitat'])
: '-' }}
</span>
</template>
</Column>
<Column field="totalSold" header="Sold" sortable>
<template #body="slotProps">
<span
class="font-mono font-bold"
:class="{ 'sold-restant': slotProps.data.hasRestant }"
<div class="row-cell">
{{ group.facturi.length === 1 ? group.facturi[0].numar_document : '-' }}
</div>
<div class="row-cell">
{{ group.facturi.length === 1 ? formatDate(group.facturi[0].data_document) : '-' }}
</div>
<div class="row-cell">
{{ group.facturi.length === 1 ? formatDate(group.facturi[0].data_scadenta) : '-' }}
</div>
<div class="row-cell text-right">
<span class="font-mono">
{{ group.facturi.length === 1 ? formatCurrency(group.facturi[0].facturat) : '-' }}
</span>
</div>
<div class="row-cell text-right">
<span class="font-mono">
{{ group.facturi.length === 1
? formatCurrency(group.facturi[0][selectedType === 'clients' ? 'incasat' : 'achitat'])
: '-' }}
</span>
</div>
<div class="row-cell text-right">
<span
class="font-mono font-bold"
:class="{ 'sold-restant': group.hasRestant }"
>
{{ formatCurrency(group.totalSold) }}
</span>
</div>
</div>
<!-- Expanded Invoice Rows -->
<Transition name="expand">
<div
v-if="group.facturi.length > 1 && isGroupExpanded(group.name)"
class="expanded-invoices"
>
{{ formatCurrency(slotProps.data.totalSold) }}
</span>
</template>
</Column>
</template>
</DataTable>
<div
v-for="(factura, idx) in group.facturi"
:key="`${group.name}-invoice-${idx}`"
class="invoice-row"
>
<div class="row-cell expand-col"></div>
<div class="row-cell name-col sub-invoice-indicator">
<span class="invoice-connector"></span>
<span class="invoice-doc-label">Factura</span>
</div>
<div class="row-cell">{{ factura.numar_document }}</div>
<div class="row-cell">{{ formatDate(factura.data_document) }}</div>
<div class="row-cell">{{ formatDate(factura.data_scadenta) }}</div>
<div class="row-cell text-right">
<span class="font-mono">{{ formatCurrency(factura.facturat) }}</span>
</div>
<div class="row-cell text-right">
<span class="font-mono">
{{ formatCurrency(factura[selectedType === 'clients' ? 'incasat' : 'achitat']) }}
</span>
</div>
<div class="row-cell text-right">
<span
class="font-mono"
:class="{ 'sold-restant': factura.status === 'Restant' }"
>
{{ formatCurrency(factura.sold) }}
</span>
</div>
</div>
</div>
</Transition>
</template>
<!-- Empty state -->
<div v-if="paginatedGroups.length === 0" class="empty-table-state">
<i class="pi pi-inbox"></i>
<p>Nu există facturi pentru criteriile selectate</p>
</div>
</div>
</div>
</div>
<!-- Mobile Cards -->
@@ -408,7 +470,7 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch, Transition } from 'vue'
import { useRouter } from 'vue-router'
import Button from 'primevue/button'
import Dropdown from 'primevue/dropdown'
@@ -1030,6 +1092,221 @@ onUnmounted(() => {
overflow: hidden;
}
/* ============================================
US-509: Expandable Groups Table (Desktop)
============================================ */
.expandable-groups-table {
width: 100%;
}
/* Table Header */
.groups-table-header {
display: grid;
grid-template-columns: 40px 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--space-xs);
padding: var(--space-md);
background: var(--surface-hover);
border-bottom: 1px solid var(--surface-border);
font-weight: var(--font-semibold);
font-size: var(--text-sm);
color: var(--text-color);
}
.header-cell {
padding: var(--space-xs) var(--space-sm);
}
.header-cell.expand-col {
padding: 0;
}
.header-cell.text-right {
text-align: right;
}
/* Table Body */
.groups-table-body {
display: flex;
flex-direction: column;
}
/* Group Row (Parent) */
.group-row {
display: grid;
grid-template-columns: 40px 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--surface-border);
font-size: var(--text-sm);
color: var(--text-color);
transition: background var(--transition-fast);
}
.group-row:last-child {
border-bottom: none;
}
.group-row.expandable {
cursor: pointer;
}
.group-row.expandable:hover {
background: var(--surface-hover);
}
.group-row.expanded {
background: var(--primary-50);
border-bottom-color: var(--primary-100);
}
/* Row cells */
.row-cell {
display: flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
min-height: 40px;
}
.row-cell.expand-col {
justify-content: center;
padding: 0;
}
.row-cell.name-col {
gap: var(--space-sm);
}
.row-cell.text-right {
justify-content: flex-end;
}
/* Expand Icon with rotation animation */
.expand-icon {
font-size: var(--text-sm);
color: var(--text-color-secondary);
transition: transform var(--transition-normal);
}
.expand-icon.rotated {
transform: rotate(90deg);
}
/* Expanded Invoices Container */
.expanded-invoices {
background: var(--surface-ground);
border-bottom: 1px solid var(--surface-border);
overflow: hidden;
}
/* Individual Invoice Row */
.invoice-row {
display: grid;
grid-template-columns: 40px 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-md);
font-size: var(--text-sm);
color: var(--text-color);
border-bottom: 1px solid var(--surface-border);
}
.invoice-row:last-child {
border-bottom: none;
}
.invoice-row:hover {
background: var(--surface-hover);
}
/* Sub-invoice indicator (connector line + label) */
.sub-invoice-indicator {
position: relative;
padding-left: var(--space-lg) !important;
}
.invoice-connector {
position: absolute;
left: var(--space-sm);
top: 50%;
width: var(--space-md);
height: 1px;
background: var(--surface-border);
}
.invoice-connector::before {
content: '';
position: absolute;
left: 0;
top: -8px;
width: 1px;
height: 16px;
background: var(--surface-border);
}
.invoice-doc-label {
font-size: var(--text-xs);
color: var(--text-color-secondary);
font-style: italic;
}
/* Expand/Collapse Animation */
.expand-enter-active,
.expand-leave-active {
transition: all var(--transition-normal);
transform-origin: top;
}
.expand-enter-from,
.expand-leave-to {
opacity: 0;
max-height: 0;
transform: scaleY(0);
}
.expand-enter-to,
.expand-leave-from {
opacity: 1;
max-height: 1000px;
transform: scaleY(1);
}
/* Empty table state */
.empty-table-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
color: var(--text-color-secondary);
}
.empty-table-state .pi {
font-size: 48px;
margin-bottom: var(--space-md);
opacity: 0.5;
}
/* Dark mode support for expandable groups */
[data-theme="dark"] .group-row.expanded {
background: var(--primary-900);
border-bottom-color: var(--primary-800);
}
[data-theme="dark"] .expanded-invoices {
background: var(--surface-100);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .group-row.expanded {
background: var(--primary-900);
border-bottom-color: var(--primary-800);
}
:root:not([data-theme]) .expanded-invoices {
background: var(--surface-100);
}
}
.font-mono {
font-family: var(--font-mono);
}