feat(unified-mobile-material-design): Complete US-103 - Refactor ReceiptsListView să folosească componente comune

Implemented by Ralph autonomous loop.
Iteration: 5

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-12 09:55:34 +00:00
parent 58ff6efebc
commit fb84cbc4f0
3 changed files with 108 additions and 334 deletions

View File

@@ -104,8 +104,8 @@
"Verify in browser că lista bonuri funcționează identic pe mobil", "Verify in browser că lista bonuri funcționează identic pe mobil",
"npm run build passes" "npm run build passes"
], ],
"passes": false, "passes": true,
"notes": "" "notes": "Completed in iteration 5"
}, },
{ {
"id": "US-104", "id": "US-104",

View File

@@ -31,3 +31,9 @@ Mon Jan 12 09:44:54 AM UTC 2026
[2026-01-12 09:49:17] Working on story: US-102 [2026-01-12 09:49:17] Working on story: US-102
[2026-01-12 09:49:17] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_4_US-102.log) [2026-01-12 09:49:17] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_4_US-102.log)
[2026-01-12 09:50:55] SUCCESS: Story US-102 passed! [2026-01-12 09:50:55] SUCCESS: Story US-102 passed!
[2026-01-12 09:50:55] Changes committed
[2026-01-12 09:50:55] Progress: 4/20 stories completed
[2026-01-12 09:50:57] === Iteration 5/100 ===
[2026-01-12 09:50:57] Working on story: US-103
[2026-01-12 09:50:57] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_5_US-103.log)
[2026-01-12 09:55:34] SUCCESS: Story US-103 passed!

View File

@@ -45,67 +45,18 @@
</template> </template>
</Dialog> </Dialog>
<!-- US-040: Mobile Android-Native Top Bar --> <!-- US-103: Mobile Android-Native Top Bar (using shared component) -->
<header v-if="isMobile" class="mobile-top-bar" :class="{ 'selection-active': mobileSelectionMode }"> <MobileTopBar
<div class="top-bar-left"> v-if="isMobile"
<Button :title="mobileSelectionMode ? `${selectedReceipts.length} selectate` : 'Bonuri'"
v-if="mobileSelectionMode" :show-back="mobileSelectionMode"
icon="pi pi-times" :show-menu="!mobileSelectionMode"
text :selection-active="mobileSelectionMode"
rounded :actions="mobileTopBarActions"
class="top-bar-btn" @back-click="exitMobileSelectionMode"
@click="exitMobileSelectionMode" @menu-click="toggleMobileMenu"
/> @action-click="handleTopBarAction"
<Button />
v-else
icon="pi pi-bars"
text
rounded
class="top-bar-btn"
@click="toggleMobileMenu"
/>
</div>
<h1 class="top-bar-title">
{{ mobileSelectionMode ? `${selectedReceipts.length} selectate` : 'Bonuri' }}
</h1>
<div class="top-bar-right">
<template v-if="mobileSelectionMode">
<Button
icon="pi pi-check-square"
text
rounded
class="top-bar-btn"
@click="selectAllMobile"
v-tooltip.bottom="'Selectează tot'"
/>
</template>
<template v-else>
<Button
icon="pi pi-search"
text
rounded
class="top-bar-btn"
@click="showFilters = !showFilters"
:class="{ 'active': showFilters }"
/>
<Button
icon="pi pi-filter"
text
rounded
class="top-bar-btn"
:class="{ 'active': hasActiveFilters }"
@click="showFilters = !showFilters"
/>
<Button
icon="pi pi-ellipsis-v"
text
rounded
class="top-bar-btn"
@click="toggleMoreMenu"
/>
</template>
</div>
</header>
<!-- US-040: Mobile Filter Chips (horizontal scrollable) --> <!-- US-040: Mobile Filter Chips (horizontal scrollable) -->
<div v-if="isMobile && stats && !mobileSelectionMode" class="mobile-filter-chips-container"> <div v-if="isMobile && stats && !mobileSelectionMode" class="mobile-filter-chips-container">
@@ -603,18 +554,11 @@
/> />
</div> </div>
<!-- US-039: Mobile Selection Bottom Bar --> <!-- US-103: Mobile Selection Bottom Bar (using shared component) -->
<Transition name="slide-up"> <MobileSelectionFooter
<div v-if="mobileSelectionMode && selectedReceipts.length > 0" class="mobile-selection-bottom-bar"> :visible="mobileSelectionMode && selectedReceipts.length > 0"
<Button :actions="mobileSelectionActions"
label="Șterge" />
icon="pi pi-trash"
severity="danger"
class="delete-btn-mobile"
@click="confirmBulkDelete"
/>
</div>
</Transition>
</div> </div>
<!-- US-040: Mobile FAB (Floating Action Button) --> <!-- US-040: Mobile FAB (Floating Action Button) -->
@@ -629,25 +573,12 @@
</button> </button>
</Transition> </Transition>
<!-- US-040: Mobile Bottom Navigation --> <!-- US-103: Mobile Bottom Navigation (using shared component) -->
<nav v-if="isMobile && !mobileSelectionMode" class="mobile-bottom-nav"> <MobileBottomNav
<router-link to="/data-entry" class="bottom-nav-item active"> v-if="isMobile && !mobileSelectionMode"
<i class="pi pi-receipt"></i> :items="mobileBottomNavItems"
<span>Bonuri</span> @item-click="handleBottomNavClick"
</router-link> />
<button class="bottom-nav-item" @click="openBulkFileInput">
<i class="pi pi-cloud-upload"></i>
<span>Upload</span>
</button>
<router-link to="/reports/dashboard" class="bottom-nav-item">
<i class="pi pi-chart-bar"></i>
<span>Rapoarte</span>
</router-link>
<router-link to="/data-entry/ocr-metrics" class="bottom-nav-item">
<i class="pi pi-cog"></i>
<span>Setări</span>
</router-link>
</nav>
<!-- Desktop: Compact Data Table with Batch Grouping (US-002) --> <!-- Desktop: Compact Data Table with Batch Grouping (US-002) -->
<div v-else class="data-table-container"> <div v-else class="data-table-container">
@@ -1048,6 +979,10 @@ import Dropdown from 'primevue/dropdown'
import Checkbox from 'primevue/checkbox' import Checkbox from 'primevue/checkbox'
import Sidebar from 'primevue/sidebar' // US-040: Mobile hamburger menu import Sidebar from 'primevue/sidebar' // US-040: Mobile hamburger menu
import DragDropOverlay from '@data-entry/components/bulk/DragDropOverlay.vue' import DragDropOverlay from '@data-entry/components/bulk/DragDropOverlay.vue'
// US-103: Mobile Material Design common components
import MobileTopBar from '@shared/components/mobile/MobileTopBar.vue'
import MobileBottomNav from '@shared/components/mobile/MobileBottomNav.vue'
import MobileSelectionFooter from '@shared/components/mobile/MobileSelectionFooter.vue'
import BatchGroupHeader from '@data-entry/components/bulk/BatchGroupHeader.vue' import BatchGroupHeader from '@data-entry/components/bulk/BatchGroupHeader.vue'
import ProcessingStatusCell from '@data-entry/components/bulk/ProcessingStatusCell.vue' import ProcessingStatusCell from '@data-entry/components/bulk/ProcessingStatusCell.vue'
import Paginator from 'primevue/paginator' import Paginator from 'primevue/paginator'
@@ -1155,6 +1090,72 @@ const moreMenuItems = computed(() => [
} }
]) ])
// US-103: Top bar actions for MobileTopBar component
const mobileTopBarActions = computed(() => {
if (mobileSelectionMode.value) {
// Selection mode - show select all action
return [
{ id: 'select-all', icon: 'pi pi-check-square', label: 'Selectează tot', tooltip: 'Selectează tot' }
]
}
// Normal mode - show search, filter, more menu
return [
{ id: 'search', icon: 'pi pi-search', active: showFilters.value, tooltip: 'Căutare' },
{ id: 'filter', icon: 'pi pi-filter', active: hasActiveFilters.value, tooltip: 'Filtre' },
{ id: 'more', icon: 'pi pi-ellipsis-v', tooltip: 'Mai multe' }
]
})
// US-103: Handle top bar action clicks
const handleTopBarAction = (action) => {
switch (action.id) {
case 'select-all':
selectAllMobile()
break
case 'search':
case 'filter':
showFilters.value = !showFilters.value
break
case 'more':
// The more menu needs to be toggled with the event, but we don't have the event here
// So we need to use a different approach - we'll keep using toggleMoreMenu directly
// by storing a ref to trigger it
if (moreMenuRef.value) {
// Create a synthetic event at the more button position
const btn = document.querySelector('.mobile-top-bar .top-bar-btn:last-child')
if (btn) {
moreMenuRef.value.toggle({ currentTarget: btn })
}
}
break
}
}
// US-103: Bottom nav items for MobileBottomNav component
const mobileBottomNavItems = computed(() => [
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri', active: true },
{ icon: 'pi pi-cloud-upload', label: 'Upload' }, // No 'to' - handled via item-click
{ to: '/reports/dashboard', icon: 'pi pi-chart-bar', label: 'Rapoarte' },
{ to: '/data-entry/ocr-metrics', icon: 'pi pi-cog', label: 'Setări' }
])
// US-103: Handle bottom nav clicks for items without routes
const handleBottomNavClick = (item) => {
if (item.label === 'Upload') {
openBulkFileInput()
}
}
// US-103: Selection footer actions for MobileSelectionFooter component
const mobileSelectionActions = computed(() => [
{
label: 'Șterge',
icon: 'pi pi-trash',
severity: 'danger',
handler: () => confirmBulkDelete()
}
])
// US-040: Handle scroll to show/hide FAB // US-040: Handle scroll to show/hide FAB
const handleScroll = () => { const handleScroll = () => {
if (!isMobile.value) return if (!isMobile.value) return
@@ -3322,68 +3323,7 @@ const cleanupCompletedBatches = (storedBatchIds) => {
} }
} }
/* US-039: Mobile Selection Bottom Bar */ /* US-103: Mobile Selection Bottom Bar styles now in MobileSelectionFooter.vue */
.mobile-selection-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--surface-card);
border-top: 1px solid var(--surface-border);
padding: var(--space-md);
display: flex;
justify-content: center;
align-items: center;
z-index: var(--z-fixed);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.mobile-selection-bottom-bar .delete-btn-mobile {
width: 100%;
max-width: 400px;
height: 48px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
}
/* Slide-up animation for bottom bar */
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform var(--transition-normal), opacity var(--transition-normal);
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
opacity: 0;
}
.slide-up-enter-to,
.slide-up-leave-from {
transform: translateY(0);
opacity: 1;
}
/* Dark mode for bottom bar */
[data-theme="dark"] .mobile-selection-bottom-bar {
background: var(--surface-card);
border-top-color: var(--surface-border);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
}
/* Auto dark mode (system preference) for bottom bar */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .mobile-selection-bottom-bar {
background: var(--surface-card);
border-top-color: var(--surface-border);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
}
}
/* Add padding to receipt cards when bottom bar is visible to prevent overlap */
.receipt-cards:has(.mobile-selection-bottom-bar) {
padding-bottom: 80px;
}
.card-row-1 { .card-row-1 {
display: flex; display: flex;
@@ -4287,58 +4227,7 @@ const cleanupCompletedBatches = (storedBatchIds) => {
padding: var(--space-sm); padding: var(--space-sm);
} }
/* Mobile Top Bar - Android native style */ /* US-103: Mobile Top Bar styles now in MobileTopBar.vue */
.mobile-top-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: var(--surface-card);
border-bottom: 1px solid var(--surface-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-xs);
z-index: 1000;
box-shadow: var(--shadow-sm);
}
.mobile-top-bar.selection-active {
background: var(--blue-50);
border-bottom-color: var(--blue-200);
}
.top-bar-left,
.top-bar-right {
display: flex;
align-items: center;
gap: var(--space-xs);
}
.top-bar-btn {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
color: var(--text-color);
}
.top-bar-btn.active {
color: var(--color-primary);
background: var(--blue-50);
}
.top-bar-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-color);
margin: 0;
flex: 1;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Mobile Filter Chips Container */ /* Mobile Filter Chips Container */
.mobile-filter-chips-container { .mobile-filter-chips-container {
@@ -4445,58 +4334,7 @@ const cleanupCompletedBatches = (storedBatchIds) => {
font-size: var(--text-xs); font-size: var(--text-xs);
} }
/* Mobile Bottom Navigation - Android native style */ /* US-103: Mobile Bottom Navigation styles now in MobileBottomNav.vue */
.mobile-bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
background: var(--surface-card);
border-top: 1px solid var(--surface-border);
display: flex;
align-items: stretch;
justify-content: space-around;
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
.bottom-nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-xs);
color: var(--text-color-secondary);
text-decoration: none;
font-size: var(--text-xs);
font-weight: var(--font-medium);
background: none;
border: none;
cursor: pointer;
transition: color var(--transition-fast);
padding: var(--space-xs);
min-width: 48px;
}
.bottom-nav-item i {
font-size: var(--text-xl);
}
.bottom-nav-item:active {
background: var(--surface-hover);
}
.bottom-nav-item.active,
.bottom-nav-item.router-link-active {
color: var(--color-primary);
}
.bottom-nav-item.active i,
.bottom-nav-item.router-link-active i {
color: var(--color-primary);
}
/* Mobile FAB - Floating Action Button */ /* Mobile FAB - Floating Action Button */
.mobile-fab { .mobile-fab {
@@ -4612,25 +4450,7 @@ const cleanupCompletedBatches = (storedBatchIds) => {
padding-bottom: 80px; /* Space for FAB and bottom nav */ padding-bottom: 80px; /* Space for FAB and bottom nav */
} }
/* Dark mode support for US-040 components */ /* US-103: Dark mode for MobileTopBar now in component */
[data-theme="dark"] .mobile-top-bar {
background: var(--surface-card);
border-bottom-color: var(--surface-border);
}
[data-theme="dark"] .mobile-top-bar.selection-active {
background: var(--blue-900);
border-bottom-color: var(--blue-700);
}
[data-theme="dark"] .top-bar-btn {
color: var(--text-color);
}
[data-theme="dark"] .top-bar-btn.active {
color: var(--blue-400);
background: var(--blue-900);
}
[data-theme="dark"] .mobile-filter-chips-container { [data-theme="dark"] .mobile-filter-chips-container {
background: var(--surface-card); background: var(--surface-card);
@@ -4661,24 +4481,7 @@ const cleanupCompletedBatches = (storedBatchIds) => {
border-color: var(--red-700); border-color: var(--red-700);
} }
[data-theme="dark"] .mobile-bottom-nav { /* US-103: Dark mode for MobileBottomNav now in component */
background: var(--surface-card);
border-top-color: var(--surface-border);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
}
[data-theme="dark"] .bottom-nav-item {
color: var(--text-color-secondary);
}
[data-theme="dark"] .bottom-nav-item:active {
background: var(--surface-hover);
}
[data-theme="dark"] .bottom-nav-item.active,
[data-theme="dark"] .bottom-nav-item.router-link-active {
color: var(--blue-400);
}
[data-theme="dark"] .mobile-fab { [data-theme="dark"] .mobile-fab {
background: var(--blue-600); background: var(--blue-600);
@@ -4692,26 +4495,8 @@ const cleanupCompletedBatches = (storedBatchIds) => {
} }
/* System preference dark mode support */ /* System preference dark mode support */
/* US-103: Auto dark mode for MobileTopBar now in component */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root:not([data-theme]) .mobile-top-bar {
background: var(--surface-card);
border-bottom-color: var(--surface-border);
}
:root:not([data-theme]) .mobile-top-bar.selection-active {
background: var(--blue-900);
border-bottom-color: var(--blue-700);
}
:root:not([data-theme]) .top-bar-btn {
color: var(--text-color);
}
:root:not([data-theme]) .top-bar-btn.active {
color: var(--blue-400);
background: var(--blue-900);
}
:root:not([data-theme]) .mobile-filter-chips-container { :root:not([data-theme]) .mobile-filter-chips-container {
background: var(--surface-card); background: var(--surface-card);
border-bottom-color: var(--surface-border); border-bottom-color: var(--surface-border);
@@ -4741,24 +4526,7 @@ const cleanupCompletedBatches = (storedBatchIds) => {
border-color: var(--red-700); border-color: var(--red-700);
} }
:root:not([data-theme]) .mobile-bottom-nav { /* US-103: Auto dark mode for MobileBottomNav now in component */
background: var(--surface-card);
border-top-color: var(--surface-border);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
}
:root:not([data-theme]) .bottom-nav-item {
color: var(--text-color-secondary);
}
:root:not([data-theme]) .bottom-nav-item:active {
background: var(--surface-hover);
}
:root:not([data-theme]) .bottom-nav-item.active,
:root:not([data-theme]) .bottom-nav-item.router-link-active {
color: var(--blue-400);
}
:root:not([data-theme]) .mobile-fab { :root:not([data-theme]) .mobile-fab {
background: var(--blue-600); background: var(--blue-600);