import 'package:sysfarma_mobile/src/core/network/api_client.dart'; import 'package:sysfarma_mobile/src/core/network/inventory_options_cache.dart'; import 'package:sysfarma_mobile/src/core/theme/app_tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class StocktakeListScreen extends ConsumerStatefulWidget { const StocktakeListScreen({super.key}); @override ConsumerState createState() => _StocktakeListScreenState(); } class _StocktakeListScreenState extends ConsumerState { late Future>> _countsFuture; @override void initState() { super.initState(); _countsFuture = _loadCounts(); } Future>> _loadCounts() async { final response = await ref.read(apiClientProvider).physicalCounts(status: 'all'); return response.data ?? const >[]; } void _refresh() { setState(() => _countsFuture = _loadCounts()); // also refresh cached options in background ref.read(inventoryOptionsCacheProvider.notifier).refresh(); } Future _createCount() async { final optionsState = ref.read(inventoryOptionsCacheProvider); final warehouses = ref.read(inventoryOptionsCacheProvider.notifier).warehouses; if (optionsState is AsyncLoading) { _showMessage('Cargando almacenes…', isError: false); return; } if (warehouses.isEmpty) { _showMessage('No hay almacenes disponibles.', isError: true); return; } final selected = warehouses.length == 1 ? warehouses.first : await showModalBottomSheet>( context: context, builder: (context) => _WarehousePicker(warehouses: warehouses), ); if (selected == null || !mounted) return; final warehouseId = int.tryParse('${selected['id']}'); if (warehouseId == null) return; final response = await ref .read(apiClientProvider) .createPhysicalCount(warehouseId: warehouseId); if (!mounted) return; if (!response.success) { _showMessage(response.message ?? 'No se pudo crear el conteo.', isError: true); return; } final id = int.tryParse('${response.data?['id']}'); if (id != null) { context.push('/stocktake/$id'); } else { _refresh(); } } void _showMessage(String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? AppTokens.danger : AppTokens.success, ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Conteos fisicos'), actions: [ IconButton( onPressed: _refresh, icon: const Icon(Icons.refresh_rounded), tooltip: 'Actualizar', ), ], ), floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 84), child: FloatingActionButton.extended( onPressed: _createCount, icon: const Icon(Icons.add_rounded), label: const Text('Nuevo conteo'), ), ), body: RefreshIndicator( onRefresh: () async => _refresh(), child: FutureBuilder>>( future: _countsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } final counts = snapshot.data ?? const []; if (counts.isEmpty) { return ListView( padding: AppTokens.pagePadding, children: const [ _EmptyState( icon: Icons.fact_check_outlined, title: 'Sin conteos', subtitle: 'Crea un conteo para empezar a escanear productos.', ), SizedBox(height: 96), ], ); } // Group by status for visual separation final pending = counts .where((r) => r['status'] == 'draft' || r['status'] == 'submitted') .toList(); final closed = counts .where((r) => r['status'] == 'approved' || r['status'] == 'rejected' || r['status'] == 'cancelled') .toList(); return ListView( padding: AppTokens.pagePadding, children: [ if (pending.isNotEmpty) ...[ _SectionHeader( label: 'En progreso', count: pending.length, color: AppTokens.warning, ), const SizedBox(height: 6), ...pending.map((row) => _CountCard(row: row)), const SizedBox(height: 16), ], if (closed.isNotEmpty) ...[ _SectionHeader( label: 'Cerrados', count: closed.length, color: AppTokens.secondary, ), const SizedBox(height: 6), ...closed.map((row) => _CountCard(row: row)), ], const SizedBox(height: 96), ], ); }, ), ), ); } } // ── Warehouse picker bottom sheet ───────────────────────────────────────────── class _WarehousePicker extends StatelessWidget { const _WarehousePicker({required this.warehouses}); final List> warehouses; @override Widget build(BuildContext context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Text('Selecciona almacen', style: Theme.of(context).textTheme.titleMedium), ), const Divider(height: 1), ...warehouses.map((row) => ListTile( leading: const Icon(Icons.warehouse_outlined), title: Text(row['description']?.toString() ?? 'Almacen'), onTap: () => Navigator.of(context).pop(row), )), const SizedBox(height: 8), ], ), ); } } // ── Section header ───────────────────────────────────────────────────────────── class _SectionHeader extends StatelessWidget { const _SectionHeader({ required this.label, required this.count, required this.color, }); final String label; final int count; final Color color; @override Widget build(BuildContext context) { return Row( children: [ Container( width: 3, height: 14, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text(label, style: Theme.of(context) .textTheme .labelLarge ?.copyWith(color: color, fontWeight: FontWeight.w700)), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Text('$count', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: color)), ), ], ); } } // ── Count card ───────────────────────────────────────────────────────────────── class _CountCard extends StatelessWidget { const _CountCard({required this.row}); final Map row; @override Widget build(BuildContext context) { final id = int.tryParse('${row['id']}') ?? 0; final status = row['status']?.toString() ?? ''; final isFinal = status == 'submitted' || status == 'approved' || status == 'rejected' || status == 'cancelled'; final statusColor = _statusColor(status); final statusLabel = _statusLabel(status); final statusIcon = _statusIcon(status); return Card( margin: const EdgeInsets.only(bottom: 10), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade200), ), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => context.push(isFinal ? '/stocktake/$id/report' : '/stocktake/$id'), child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(10), ), child: Icon(statusIcon, color: statusColor, size: 22), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('Conteo #$id', style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 14)), const Spacer(), _StatusChip( label: statusLabel, color: statusColor), ], ), const SizedBox(height: 3), Text( row['warehouse_name']?.toString() ?? '-', style: TextStyle( fontSize: 12, color: Colors.grey.shade600), ), const SizedBox(height: 2), Row( children: [ Icon(Icons.inventory_2_outlined, size: 12, color: Colors.grey.shade400), const SizedBox(width: 3), Text('${row['lines_count'] ?? 0} items', style: TextStyle( fontSize: 11, color: Colors.grey.shade500)), const SizedBox(width: 10), Icon(Icons.schedule_outlined, size: 12, color: Colors.grey.shade400), const SizedBox(width: 3), Expanded( child: Text( row['created_at']?.toString() ?? '', style: TextStyle( fontSize: 11, color: Colors.grey.shade500), overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ), const SizedBox(width: 6), Icon(Icons.chevron_right_rounded, color: Colors.grey.shade400, size: 20), ], ), ), ), ); } static String _statusLabel(String status) { return { 'draft': 'Borrador', 'submitted': 'En revision', 'approved': 'Aprobado', 'rejected': 'Rechazado', 'cancelled': 'Cancelado', }[status] ?? status; } static Color _statusColor(String status) { return { 'draft': AppTokens.secondary, 'submitted': AppTokens.warning, 'approved': AppTokens.success, 'rejected': AppTokens.danger, 'cancelled': AppTokens.secondary, }[status] ?? AppTokens.secondary; } static IconData _statusIcon(String status) { return { 'draft': Icons.edit_note_rounded, 'submitted': Icons.hourglass_top_rounded, 'approved': Icons.check_circle_rounded, 'rejected': Icons.cancel_rounded, 'cancelled': Icons.block_rounded, }[status] ?? Icons.fact_check_rounded; } } // ── Status chip ──────────────────────────────────────────────────────────────── class _StatusChip extends StatelessWidget { const _StatusChip({required this.label, required this.color}); final String label; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withValues(alpha: 0.3)), ), child: Text(label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: color)), ); } } // ── Empty state ──────────────────────────────────────────────────────────────── class _EmptyState extends StatelessWidget { const _EmptyState({ required this.icon, required this.title, required this.subtitle, }); final IconData icon; final String title; final String subtitle; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 48), child: Column( children: [ Icon(icon, size: 52, color: Colors.grey.shade300), const SizedBox(height: 14), Text(title, style: Theme.of(context) .textTheme .titleMedium ?.copyWith(color: Colors.grey.shade600)), const SizedBox(height: 6), Text(subtitle, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: Colors.grey.shade500)), ], ), ); } }