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>
This commit is contained in:
Claude Agent
2026-01-12 08:33:17 +00:00
parent b4a226409c
commit 7b3541403f
53 changed files with 15810 additions and 196 deletions

View File

@@ -58,6 +58,7 @@ logger = logging.getLogger(__name__)
# Global variables for background tasks
telegram_bot_task = None
ocr_job_worker_running = False
cleanup_task_running = False
# ============================================================================
@@ -160,6 +161,33 @@ async def init_ocr_job_worker():
ocr_job_worker_running = False
async def init_cleanup_task():
"""Initialize the cleanup background task for expired failed receipts (US-008).
Runs cleanup at startup and then every 24 hours:
- Finds receipts with processing_status='failed' older than 7 days
- Deletes the receipts and their attachment files from storage
"""
global cleanup_task_running
logger.info("[CLEANUP] Initializing cleanup background task...")
try:
from backend.modules.data_entry.services.cleanup_service import start_cleanup_task
from backend.modules.data_entry.db.database import get_session
success = await start_cleanup_task(get_session)
cleanup_task_running = success
if success:
logger.info("[CLEANUP] ✅ Cleanup task started (runs daily)")
else:
logger.warning("[CLEANUP] ⚠️ Cleanup task failed to start")
except Exception as e:
logger.warning(f"[CLEANUP] ⚠️ Cleanup task init failed: {e}")
cleanup_task_running = False
async def run_telegram_bot():
"""Run Telegram bot as background task."""
logger.info("[TELEGRAM] Starting bot...")
@@ -270,7 +298,10 @@ async def startup_event():
# Step 3: Initialize OCR job worker (with persistent PaddleOCR)
await init_ocr_job_worker()
# Step 4: Start Telegram bot as background task
# Step 4: Initialize cleanup task for expired failed receipts (US-008)
await init_cleanup_task()
# Step 5: Start Telegram bot as background task
if settings.telegram_bot_token:
telegram_bot_task = asyncio.create_task(run_telegram_bot())
logger.info("[STARTUP] ✅ Telegram bot task created")
@@ -290,13 +321,24 @@ async def startup_event():
@app.on_event("shutdown")
async def shutdown_event():
"""Application shutdown - Cleanup resources."""
global telegram_bot_task, ocr_job_worker_running
global telegram_bot_task, ocr_job_worker_running, cleanup_task_running
logger.info("=" * 80)
logger.info("[SHUTDOWN] Stopping ROA2WEB Unified Backend...")
logger.info("=" * 80)
try:
# Stop cleanup task (US-008)
if cleanup_task_running:
logger.info("[SHUTDOWN] Stopping cleanup task...")
try:
from backend.modules.data_entry.services.cleanup_service import stop_cleanup_task
await stop_cleanup_task()
cleanup_task_running = False
logger.info("[SHUTDOWN] Cleanup task stopped")
except Exception as e:
logger.error(f"[SHUTDOWN] Cleanup task error: {e}")
# Stop OCR job worker
if ocr_job_worker_running:
logger.info("[SHUTDOWN] Stopping OCR job worker...")