Files
roa2web-service-auto/scripts/ralph/prd.json
Claude Agent 7b3541403f feat(data-entry): Bulk Receipt Upload cu Mobile UX Android Nativ
## Funcționalități Principale

### Bulk Upload & Processing
- Drag & drop pentru upload bonuri multiple oriunde pe pagină
- Batch processing cu job queue și worker pool
- Real-time updates via SSE (Server-Sent Events) cu fallback polling
- Duplicate detection via SHA-256 file hash
- Auto-retry pentru job-uri failed
- Cancel individual jobs sau batch complet

### Mobile UX - Android Native Style
- Top bar fixă cu hamburger, titlu centrat, acțiuni (search/filter)
- Bottom navigation cu 4 tab-uri (Bonuri, Upload, Rapoarte, Setări)
- FAB (Floating Action Button) cu hide/show on scroll
- Filter chips orizontal scrollabile
- Selecție multiplă prin long-press (500ms)
- Select All + Bulk Delete cu confirmare
- Layout Android pentru Create/Edit/View bon (Gmail compose style)

### Bug Fixes
- Refresh individual via SSE în loc de refresh total pagină
- Bonurile cu eroare OCR rămân vizibile pentru editare manuală
- Afișare nume fișier original pentru toate bonurile
- Upload stabil pe mobil (fix race condition File API)
- Păstrare ordine bonuri la refresh (nu se reordonează)

### Backend
- SSE endpoint pentru status updates real-time
- Bulk delete endpoint cu partial success
- Auto-cleanup bonuri failed după 7 zile
- Batch model cu tracking complet

### Testing
- E2E tests cu Playwright
- Unit tests pentru bulk upload, auto-create, cleanup

## Commits Squashed: 43 user stories (US-001 → US-043)
## Branch: ralph/bulk-receipt-upload
## Timp dezvoltare: ~3 zile (Ralph autonomous)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:33:17 +00:00

610 lines
26 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"projectName": "mobile-ux-improvements",
"branchName": "ralph/bulk-receipt-upload",
"description": "Corectarea comportamentului de refresh pentru bulk upload, implementarea selecției multiple pe mobil cu interfață Android nativă, afișarea numelui fișierului pentru toate bonurile, și rezolvarea bug-urilor de UX raportate.",
"cssRules": {
"documentation": [
"docs/ONBOARDING_CSS.md",
"docs/DESIGN_TOKENS.md",
"docs/CSS_PATTERNS.md"
],
"goldenRules": [
"Folosește DOAR design tokens - NICIODATĂ valori hardcodate",
"Verifică CSS_PATTERNS.md înainte de a scrie CSS nou",
"Testează în AMBELE teme (light + dark mode)",
"NICIODATĂ :deep() în componente (PrimeVue → vendor/)",
"NICIODATĂ duplicate CSS (write once, use everywhere)"
],
"mobileLayoutTokens": {
"topBarHeight": "56px",
"bottomNavHeight": "56px",
"fabSize": "56px",
"fabBottomOffset": "72px",
"touchTargetMin": "48px"
},
"selectionModeColors": {
"selected": {
"background": "var(--blue-50)",
"border": "var(--blue-500)"
},
"selectedDark": {
"background": "var(--blue-900)",
"border": "var(--blue-400)"
}
}
},
"userStories": [
{
"id": "US-001",
"title": "Backend - Stocare Batch și Processing Status",
"description": "Ca developer, vreau să extind schema Receipt pentru a stoca informații de batch, pentru că am nevoie de persistență pentru tracking.",
"priority": 1,
"acceptanceCriteria": [
"Câmpuri noi în tabelul receipts: batch_id, processing_status, processing_error, file_hash, processing_started_at, processing_completed_at",
"Index pe batch_id, file_hash, processing_status",
"Migration reversibilă cu Alembic"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-002",
"title": "Backend - Endpoint List cu Batch Info",
"description": "Ca developer, vreau să extind endpoint-ul GET /receipts pentru a include info de batch.",
"priority": 2,
"acceptanceCriteria": [
"Response include câmpurile de batch și processing pentru fiecare receipt",
"Filtrare pe processing_status și batch_id funcționează",
"Response include processing_stats"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-003",
"title": "Backend - Reject Automat pentru Duplicate (File Hash)",
"description": "Ca sistem, vreau să detectez și să reject fișierele duplicate la upload.",
"priority": 3,
"acceptanceCriteria": [
"SHA-256 hash pentru duplicate detection",
"Response include existing_receipt_id pentru duplicates"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-004",
"title": "Frontend - Drag Anywhere pentru Upload",
"description": "Ca utilizator, vreau să pot trage fișiere oriunde pe pagina de bonuri.",
"priority": 4,
"acceptanceCriteria": [
"DragDropOverlay.vue componentă",
"Global listeners cleanup în onUnmounted"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-005",
"title": "Frontend - Row Grouping per Batch în DataTable",
"description": "Ca utilizator, vreau să văd bonurile din același batch grupate vizual.",
"priority": 5,
"acceptanceCriteria": [
"BatchGroupHeader.vue componentă",
"Grupuri sortate după processing_started_at descending"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-006",
"title": "Frontend - Coloană Status Procesare în Tabel",
"description": "Ca utilizator, vreau să văd statusul fiecărui bon din batch într-o coloană dedicată.",
"priority": 6,
"acceptanceCriteria": [
"ProcessingStatusCell.vue componentă",
"Status updates în real-time via polling"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-007",
"title": "Frontend - Mesaj Eroare Vizibil în Listă",
"description": "Ca utilizator, vreau să văd mesajul de eroare direct în listă.",
"priority": 7,
"acceptanceCriteria": [
"Truncated error message cu tooltip pentru full text"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-008",
"title": "Frontend - Quick Filter Chips pentru Statusuri Procesare",
"description": "Ca utilizator, vreau filtre rapide pentru bonurile cu erori sau în procesare.",
"priority": 8,
"acceptanceCriteria": [
"Chips 'În procesare (N)' și 'Cu erori (N)' în status bar"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-009",
"title": "Frontend - Lock Row în Procesare (Read-Only)",
"description": "Ca utilizator, vreau ca bonurile în procesare să fie read-only.",
"priority": 9,
"acceptanceCriteria": [
"Butoane și checkbox disabled pentru pending/processing",
"Tooltip pe butoane dezactivate"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-010",
"title": "Frontend - Retry Individual și Retry All Failed",
"description": "Ca utilizator, vreau să pot re-procesa bonurile cu erori.",
"priority": 10,
"acceptanceCriteria": [
"Buton Reîncercă per rând failed",
"Buton Reîncercă toate erorile în BatchGroupHeader"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-011",
"title": "Frontend - Auto-Resume Polling la Refresh/Revenire",
"description": "Ca utilizator, vreau ca procesarea să continue când revin.",
"priority": 11,
"acceptanceCriteria": [
"localStorage pentru active batch IDs",
"Auto-resume polling la onMounted"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-012",
"title": "Backend - Auto-Cleanup Erori După 7 Zile",
"description": "Ca sistem, vreau să șterg automat bonurile cu erori după 7 zile.",
"priority": 12,
"acceptanceCriteria": [
"Background job pentru cleanup",
"Șterge receipts și attachments"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-013",
"title": "Cleanup - Eliminare Pagină Separată Bulk Upload",
"description": "Ca developer, vreau să elimin pagina separată de bulk upload.",
"priority": 13,
"acceptanceCriteria": [
"Route redirect pentru backwards compatibility",
"Șterge componente nefolosite"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-014",
"title": "Backend - Endpoint Cancel Job Individual",
"description": "Ca sistem, vreau un endpoint API pentru anularea unui job specific de procesare.",
"priority": 14,
"acceptanceCriteria": [
"POST /api/data-entry/bulk/cancel/{job_id} endpoint creat",
"Job-uri cu status completed/failed returnează 400 Bad Request",
"Job-uri pending/processing sunt marcate cancelled",
"Response include: {success, job_id, cancelled_at, message}"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-015",
"title": "Backend - Endpoint Cancel Batch Complet",
"description": "Ca sistem, vreau un endpoint API pentru anularea tuturor job-urilor dintr-un batch.",
"priority": 15,
"acceptanceCriteria": [
"POST /api/data-entry/bulk/cancel-batch/{batch_id} endpoint creat",
"Response include: {success, batch_id, cancelled_count, skipped_count, message}"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-016",
"title": "Frontend - Store Actions pentru Cancel",
"description": "Ca dezvoltator, vreau acțiuni în batchProgressStore pentru cancel.",
"priority": 16,
"acceptanceCriteria": [
"batchProgressStore.cancelJob(jobId) implementat",
"batchProgressStore.cancelBatch(batchId) implementat"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-017",
"title": "Frontend - Afișare Jobs Pending în Tabel",
"description": "Ca utilizator, vreau să văd fișierele încărcate imediat în tabel.",
"priority": 17,
"acceptanceCriteria": [
"După upload success, rândurile pentru jobs apar instant în tabel",
"Tabelul poate randa atât Receipt-uri cât și BatchJob-uri"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-018",
"title": "Frontend - Tranziție Job → Receipt când OCR Termină",
"description": "Ca sistem, vreau ca rândul de job să se transforme în receipt când OCR termină.",
"priority": 18,
"acceptanceCriteria": [
"Când polling detectează job completed cu receipt_id, rândul se actualizează",
"Tranziția e smooth - rândul NU dispare și reapare"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-019",
"title": "Frontend - Animație Status Change",
"description": "Ca utilizator, vreau o indicație vizuală când un fișier își schimbă statusul.",
"priority": 19,
"acceptanceCriteria": [
"Badge-ul de status se schimbă cu CSS transition opacity 300ms",
"Highlight verde/roșu subtil pentru completed/failed"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-020",
"title": "Frontend - Buton Cancel Individual pe Row",
"description": "Ca utilizator, vreau un buton Cancel pe fiecare fișier pending/processing.",
"priority": 20,
"acceptanceCriteria": [
"Fișierele pending/processing au icon Cancel (×)",
"După cancel success, rândul dispare cu fade-out 300ms"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-021",
"title": "Frontend - Buton Cancel All în BatchGroupHeader",
"description": "Ca utilizator, vreau un buton pentru a anula toate fișierele dintr-un batch.",
"priority": 21,
"acceptanceCriteria": [
"BatchGroupHeader are buton 'Anulează tot' pentru batch-uri cu pending/processing jobs",
"Job-urile completed/failed rămân vizibile"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-022",
"title": "Frontend - Checkbox Disabled pentru Jobs în Procesare",
"description": "Ca utilizator, vreau ca checkbox-urile să fie disabled pentru fișiere în procesare.",
"priority": 22,
"acceptanceCriteria": [
"Checkbox-ul este disabled pentru rânduri de tip job",
"Select All NU include job-urile în procesare"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-023",
"title": "Frontend - Restore Jobs la Refresh/Revenire",
"description": "Ca utilizator, vreau să văd job-urile pending când revin pe pagină.",
"priority": 23,
"acceptanceCriteria": [
"La onMounted, verifică localStorage pentru active batch IDs",
"Job-urile pending/processing apar în tabel"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-024",
"title": "Backend - Endpoint Bulk Delete",
"description": "Ca frontend, vreau să pot trimite o listă de ID-uri pentru ștergere.",
"priority": 24,
"acceptanceCriteria": [
"DELETE /api/data-entry/receipts/bulk acceptă body: { ids: [...] }",
"Returnează partial success response"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-025",
"title": "Frontend - Buton Șterge în Bulk Actions Bar",
"description": "Ca utilizator, vreau să văd un buton 'Șterge' când am selecții.",
"priority": 25,
"acceptanceCriteria": [
"Butonul 'Șterge' apare în bulk actions bar când selectedReceipts.length > 0",
"Butonul are icon pi-trash și severity danger"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-026",
"title": "Frontend - Dialog Confirmare Ștergere Bulk",
"description": "Ca utilizator, vreau o confirmare înainte de ștergere.",
"priority": 26,
"acceptanceCriteria": [
"La click pe 'Șterge', apare dialog cu mesaj confirmare",
"Dialog-ul folosește PrimeVue ConfirmDialog"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-027",
"title": "Frontend - Bulk Delete cu Partial Success Toast",
"description": "Ca utilizator, vreau să văd rezultatul ștergerii.",
"priority": 27,
"acceptanceCriteria": [
"Toast arată rezultatul: 'X bonuri șterse'",
"Bonurile șterse dispar instant din listă",
"Selecția se golește după ștergere"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-028",
"title": "Frontend - Navigare la Pagina Anterioară când Lista Devine Goală",
"description": "Ca utilizator, vreau să fiu redirecționat când șterg toate bonurile de pe pagină.",
"priority": 28,
"acceptanceCriteria": [
"După bulk delete, dacă lista devine goală și currentPage > 1, navigare la pagina anterioară",
"Dacă eram pe pagina 1, afișează empty state"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-029",
"title": "Frontend - Metodă updateReceiptInPlace în Store",
"description": "Ca frontend, vreau să actualizez un singur rând fără să re-renderez toată lista.",
"priority": 29,
"acceptanceCriteria": [
"Metoda updateReceiptInPlace(receiptId, updates) în receiptsStore",
"Object.assign pentru updates, nu înlocuire array"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-030",
"title": "Backend - SSE Endpoint pentru Status Updates",
"description": "Ca frontend, vreau să primesc notificări real-time despre schimbări de status.",
"priority": 30,
"acceptanceCriteria": [
"GET /api/data-entry/receipts/sse/status returnează SSE stream",
"Format eveniment: {receipt_id, status, processing_status}"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-031",
"title": "Frontend - SSE Client Service",
"description": "Ca frontend, vreau să mă conectez la SSE și să actualizez rândurile individual.",
"priority": 31,
"acceptanceCriteria": [
"sseService.js cu connect(), disconnect(), onStatusChange()",
"Folosește native EventSource API"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-032",
"title": "Frontend - Înlocuire Polling cu SSE",
"description": "Ca frontend, vreau să folosesc SSE în loc de polling.",
"priority": 32,
"acceptanceCriteria": [
"SSE în loc de setInterval pentru auto-refresh",
"La primire eveniment SSE, apelează updateReceiptInPlace()"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-033",
"title": "Frontend - Graceful Degradation la SSE Failure",
"description": "Ca utilizator, vreau ca aplicația să funcționeze și fără SSE.",
"priority": 33,
"acceptanceCriteria": [
"Dacă SSE fail, activează fallback la polling clasic",
"Retry SSE periodic (la 30s)"
],
"passes": true,
"notes": "Completed previously"
},
{
"id": "US-034",
"title": "Fix - Refresh Individual vs Refresh Total",
"description": "Ca utilizator, vreau ca bonurile să se actualizeze individual fără să se reîncarce toată lista, pentru că vreau să văd progresul în timp real fără să pierd poziția.",
"priority": 34,
"acceptanceCriteria": [
"SSE handler NU apelează store.fetchReceipts() când un receipt nu este în pagina curentă",
"Verifică dacă receipt-ul aparține unui batch activ și îl adaugă local dacă nu există",
"Ordinea bonurilor din batch rămâne stabilă (nu se reordonează)",
"npm run typecheck passes",
"Verify in browser: uploadează 5 bonuri, nu se reîncarcă pagina între procesări"
],
"technicalNotes": "Modificare în handleSSEStatusChange din ReceiptsListView.vue:2397. Când receipt nu e găsit: verifică dacă batch_id e în batchProgressStore, apoi fetch individual receipt și inserează local în poziția corectă.",
"passes": true,
"notes": "Completed in iteration 1"
},
{
"id": "US-035",
"title": "Fix - Bonuri cu Eroare Rămân în Listă",
"description": "Ca utilizator, vreau ca bonurile cu eroare de procesare să rămână vizibile în listă, pentru că vreau să le pot edita manual sau să le șterg.",
"priority": 35,
"acceptanceCriteria": [
"Bonurile cu processing_status='failed' NU sunt eliminate din view după refresh",
"Eroarea de extragere dată/sumă afișează bonul cu status 'Eroare' și buton 'Editează'",
"Link rapid 'Editează manual' duce la formularul de editare cu datele disponibile pre-populate",
"Bonurile failed au highlight vizual (roșu subtil) pentru identificare ușoară",
"npm run typecheck passes",
"Verify in browser: bon cu eroare OCR rămâne în listă și poate fi editat"
],
"technicalNotes": "Bonurile cu eroare ar trebui să aibă status='draft' și processing_status='failed'. Nu se șterg la refresh, doar se actualizează in-place.",
"passes": true,
"notes": "Completed in iteration 2"
},
{
"id": "US-036",
"title": "Afișare Nume Fișier pentru Toate Bonurile",
"description": "Ca utilizator, vreau să văd numele fișierului original pentru bonurile procesate, pentru că vreau să identific care bon corespunde cărui fișier uploadat.",
"priority": 36,
"acceptanceCriteria": [
"Coloana 'Fișier' afișează original_filename și pentru receipt-uri completate, nu doar pentru job-uri",
"Pe mobil, numele fișierului apare sub partener (font mic, gri)",
"Tooltip pe desktop arată numele complet dacă e trunchiat",
"Dacă receipt nu are original_filename, afișează '-'",
"npm run typecheck passes",
"Verify in browser: bon procesat afișează numele fișierului original"
],
"technicalNotes": "Backend trebuie să populeze original_filename pe receipt din job-ul OCR. Verifică că auto-save service copiază filename din job la receipt.",
"passes": true,
"notes": "Completed in iteration 3"
},
{
"id": "US-037",
"title": "Fix - Upload Nu Mai Face Refresh Automat",
"description": "Ca utilizator, vreau ca selectarea fișierelor să nu reîncarce pagina, pentru că pierd fișierele selectate înainte să apăs 'Procesează'.",
"priority": 37,
"acceptanceCriteria": [
"Selectarea fișierelor via input NU declanșează refresh/reload pagină",
"Fișierele selectate rămân în listă până la submit explicit",
"Comportament identic pe desktop și mobil (Chrome, Safari)",
"Nu există race condition între clonarea fișierelor și resetarea input-ului",
"npm run typecheck passes",
"Verify on mobile: selectează 5 fișiere, toate rămân în listă"
],
"technicalNotes": "Bug probabil în onBulkFileInputChange sau în interacțiunea cu store. Verifică că nu există watchers care declanșează refresh la schimbări de state. Test specific pe Chrome Android - vezi gotcha din CLAUDE.md despre File API.",
"passes": true,
"notes": "Completed in iteration 4"
},
{
"id": "US-038",
"title": "Mobile - Selecție Multiplă prin Long-Press",
"description": "Ca utilizator mobil, vreau să selectez bonuri prin apăsare lungă, pentru că vreau să pot șterge sau acționa asupra mai multor bonuri simultan.",
"priority": 38,
"acceptanceCriteria": [
"Long-press (500ms) pe un card activează modul de selecție",
"Card-ul selectat primește checkmark și background diferit (var(--blue-50))",
"După activare, tap simplu pe alte carduri le adaugă/elimină din selecție",
"Tap în afara cardurilor dezactivează modul selecție",
"Header contextual arată numărul de elemente selectate",
"npm run typecheck passes",
"Verify on mobile: long-press selectează, tap adaugă la selecție"
],
"technicalNotes": "Implementare cu setTimeout(500ms) pe touchstart, clear pe touchend/touchmove. CSS: .receipt-card.selected { background: var(--blue-50); border-color: var(--blue-500); }",
"passes": true,
"notes": "Completed in iteration 5"
},
{
"id": "US-039",
"title": "Mobile - Select All și Buton Ștergere",
"description": "Ca utilizator mobil, vreau să am butoane 'Selectează tot' și 'Șterge' în modul selecție, pentru că vreau să pot șterge rapid mai multe bonuri.",
"priority": 39,
"acceptanceCriteria": [
"În modul selecție, apare top bar contextual cu: număr selectate, buton 'Selectează tot', buton X pentru ieșire",
"Apare bottom bar cu buton 'Șterge' (roșu, icon coș)",
"'Selectează tot' selectează toate bonurile din pagina curentă",
"Buton 'Șterge' afișează confirmare înainte de ștergere",
"După ștergere, modul selecție se dezactivează",
"npm run typecheck passes",
"Verify on mobile: select all + delete funcționează"
],
"technicalNotes": "Top bar: position: sticky; top: 0. Bottom bar: position: fixed; bottom: 0. Animație slide-in pentru bars.",
"passes": true,
"notes": "Completed in iteration 6"
},
{
"id": "US-040",
"title": "Mobile - Layout Android Nativ pentru Lista Bonuri",
"description": "Ca utilizator mobil, vreau o interfață similară cu aplicațiile Android native, pentru că vreau experiență familiară și intuitivă.",
"priority": 40,
"acceptanceCriteria": [
"Top Bar fixă cu: hamburger/back (stânga), titlu centrat, search+filter+more (dreapta)",
"Filter Chips sub top bar (orizontal scrollabil): Toate, Ciorne, În așteptare, Validate, Respinse",
"Bottom Navigation fixă cu 4 tab-uri: Bonuri (activ), Upload, Rapoarte, Setări",
"FAB (56x56px) în colț dreapta jos, 16px de la margine, 72px de la bottom",
"FAB se ascunde când scroll în jos, apare când scroll în sus",
"Lista ocupă spațiul dintre top bar și bottom nav",
"npm run typecheck passes",
"Verify on mobile: layout similar cu Gmail/WhatsApp"
],
"technicalNotes": "CSS: .mobile-top-bar { position: fixed; top: 0; height: 56px; z-index: 1000 }. .mobile-bottom-nav { position: fixed; bottom: 0; height: 56px }. .mobile-fab { position: fixed; bottom: 72px; right: 16px; width: 56px; border-radius: 16px }",
"passes": true,
"notes": "Completed in iteration 7"
},
{
"id": "US-041",
"title": "Mobile - Layout Android pentru Editare/Creare Bon",
"description": "Ca utilizator mobil, vreau interfață de editare bon similară cu compose email în Gmail, pentru că vreau butoane de acțiune accesibile și layout familiar.",
"priority": 41,
"acceptanceCriteria": [
"Top Bar cu: X/back (stânga), titlu centrat, attach+save icons (dreapta)",
"Content Area scrollabilă cu form fields 100% width",
"Bottom Action Bar fixă cu: 'Salvează Ciornă' (secondary), 'Trimite pentru Validare' (primary)",
"Keyboard-aware: bottom bar se mută deasupra tastaturii",
"npm run typecheck passes",
"Verify on mobile: layout similar cu Gmail compose"
],
"technicalNotes": "CSS: .mobile-receipt-form { padding-bottom: 80px }. .mobile-form-bottom-bar { position: fixed; bottom: 0; display: flex; gap: var(--space-sm) }. Butoane cu flex: 1 pentru width egal.",
"passes": true,
"notes": "Completed in iteration 8"
},
{
"id": "US-042",
"title": "Mobile - Layout Android pentru Vizualizare Bon",
"description": "Ca utilizator mobil, vreau interfață de vizualizare bon similară cu view email în Gmail, pentru că vreau acțiuni rapide și navigare ușoară.",
"priority": 42,
"acceptanceCriteria": [
"Top Bar cu: back arrow (stânga), edit+delete+more icons (dreapta)",
"Content Area cu detalii bon (read-only)",
"Bottom Action Bar cu butoane contextuale: DRAFT→Editează/Trimite, PENDING→Validează/Respinge, APPROVED→Anulează",
"npm run typecheck passes",
"Verify on mobile: acțiuni accesibile din bottom bar"
],
"technicalNotes": "Refolosește stilurile din US-041. Butoanele din bottom bar se schimbă dinamic bazat pe receipt.status.",
"passes": true,
"notes": "Completed in iteration 9"
},
{
"id": "US-043",
"title": "Păstrare Ordine Bonuri la Refresh",
"description": "Ca utilizator, vreau ca bonurile să rămână în ordinea în care au fost uploadate, pentru că vreau să urmăresc progresul fiecărui bon uploadat.",
"priority": 43,
"acceptanceCriteria": [
"Bonurile din același batch păstrează ordinea de upload (nu se reordonează)",
"Un bon finalizat rămâne în aceeași poziție vizuală, nu sare la sfârșit",
"SSE updates modifică status-ul in-place, fără a muta rândul",
"Refresh manual poate reordona, dar SSE updates nu",
"npm run typecheck passes",
"Verify in browser: bon procesat nu își schimbă poziția în listă"
],
"technicalNotes": "Lista trebuie sortată by created_at DESC sau by batch order, nu by last_modified. updateReceiptInPlace din store NU trebuie să reordoneze array-ul.",
"passes": true,
"notes": "Completed in iteration 10"
}
]
}