## 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>
610 lines
26 KiB
JSON
610 lines
26 KiB
JSON
{
|
||
"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"
|
||
}
|
||
]
|
||
}
|