Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>

modificacion de vistas panel admin, login, animaciones y implementacion de mascota
This commit is contained in:
shinra32
2026-05-23 03:58:03 -06:00
parent 45ffba69b2
commit 68d04f3917
33 changed files with 5188 additions and 643 deletions

View File

@@ -1,3 +1,14 @@
// lib/features/eta/eta_screen.dart
// Vista principal del ciudadano: ETA con mapa de domicilio y progreso de ruta.
// Fusiona eta_screen.dart (doc-1) + eta_screen_v2.dart (doc-2).
// Orden visual:
// 1. Hero card (estado + ventana horaria)
// 2. Domicilio registrado
// 3. ProgressSteps ← nuevo: justo debajo del mapa/dirección
// 4. PreventionBanner
// 5. FCM badge
// 6. Horario semanal
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -5,43 +16,13 @@ import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_widgets.dart';
import '../../core/network/api_client.dart';
import '../notifications/notification_service.dart';
import '../../shared/widgets/prevention_banner.dart';
import '../../shared/widgets/progress_steps.dart';
// ── Provider de ETA ───────────────────────────────────────────────────────────
final etaProvider = FutureProvider.autoDispose<_EtaResult>((ref) async {
final dio = ref.read(apiClientProvider);
final addressesResp = await dio.get<dynamic>('/addresses');
final raw = addressesResp.data;
List<dynamic> items = const [];
if (raw is List) {
items = raw;
} else if (raw is Map && raw['data'] is List) {
items = raw['data'] as List;
} else if (raw is Map && raw['addresses'] is List) {
items = raw['addresses'] as List;
}
if (items.isEmpty) {
return const _EtaResult.noAddress();
}
final addressId = items.first['id'] as String;
final etaResp = await dio.get<dynamic>(
'/eta',
queryParameters: {'address_id': addressId},
);
final data = etaResp.data as Map<String, dynamic>;
return _EtaResult(
mensaje: data['mensaje'] as String? ?? '',
status: data['status'] as String? ?? '',
direccion: items.first['calle'] as String? ?? '',
colonia: items.first['colonia'] as String? ?? '',
hasAddress: true,
);
});
// ─────────────────────────────────────────────────────────────────────────────
// Modelo de resultado ETA
// ─────────────────────────────────────────────────────────────────────────────
class _EtaResult {
final String mensaje;
final String status;
@@ -58,211 +39,446 @@ class _EtaResult {
});
const _EtaResult.noAddress()
: mensaje = '',
status = '',
direccion = '',
colonia = '',
hasAddress = false;
: mensaje = '',
status = '',
direccion = '',
colonia = '',
hasAddress = false;
// ── Utilidades derivadas ───────────────────────────────────────────────────
bool get isCompleted => status == 'completada';
bool get isNearby =>
mensaje.contains('15 minutos') || mensaje.contains('Está atendiendo');
double get progreso {
if (mensaje.contains('15 minutos') || mensaje.contains('Está atendiendo')) {
return 0.85;
}
if (mensaje.contains('finalizado')) return 1.0;
if (isNearby) return 0.85;
if (isCompleted) return 1.0;
return 0.35;
}
/// Índice para el widget ProgressSteps (0 = inicio, 1 = en ruta, 2 = cerca,
/// 3 = atendiendo, 4 = completado). Ajusta los valores según tu enum real.
int get stepIndex {
if (isCompleted) return 4;
if (isNearby) return 3;
if (status == 'en_ruta') return 2;
return 1;
}
String get etiquetaEstado {
if (status == 'completada') return 'Finalizado';
if (isCompleted) return 'Finalizado';
if (status == 'en_ruta') return 'En ruta';
return 'Pendiente';
}
}
// ── Pantalla ETA ──────────────────────────────────────────────────────────────
class EtaScreen extends ConsumerWidget {
// ─────────────────────────────────────────────────────────────────────────────
// Provider de ETA
// ─────────────────────────────────────────────────────────────────────────────
class _EtaNotifier extends AsyncNotifier<_EtaResult> {
@override
Future<_EtaResult> build() => _fetch();
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(_fetch);
}
Future<_EtaResult> _fetch() async {
final dio = ref.read(apiClientProvider);
final addressesResp = await dio.get<dynamic>('/addresses');
final raw = addressesResp.data;
List<dynamic> items = const [];
if (raw is List) {
items = raw;
} else if (raw is Map && raw['data'] is List) {
items = raw['data'] as List;
} else if (raw is Map && raw['addresses'] is List) {
items = raw['addresses'] as List;
}
if (items.isEmpty) return const _EtaResult.noAddress();
final addressId = items.first['id'] as String;
final etaResp = await dio.get<dynamic>(
'/eta',
queryParameters: {'address_id': addressId},
);
final data = etaResp.data as Map<String, dynamic>;
return _EtaResult(
mensaje: data['mensaje'] as String? ?? '',
status: data['status'] as String? ?? '',
direccion: items.first['calle'] as String? ?? '',
colonia: items.first['colonia'] as String? ?? '',
hasAddress: true,
);
}
}
final etaProvider = AsyncNotifierProvider.autoDispose<_EtaNotifier, _EtaResult>(
_EtaNotifier.new,
);
// Expone el routeId activo (se puebla desde el provider de sesión/domicilio)
class ActiveRouteIdNotifier extends Notifier<String?> {
@override
String? build() => null;
}
final activeRouteIdProvider = NotifierProvider<ActiveRouteIdNotifier, String?>(
ActiveRouteIdNotifier.new,
);
// ─────────────────────────────────────────────────────────────────────────────
// Pantalla principal
// ─────────────────────────────────────────────────────────────────────────────
class EtaScreen extends ConsumerStatefulWidget {
const EtaScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<EtaScreen> createState() => _EtaScreenState();
}
class _EtaScreenState extends ConsumerState<EtaScreen>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Refresca al recibir push FCM (RUTA_PROXIMITY, ROUTE_START, etc.)
NotificationService.onFcmMessage.addListener(_onPush);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
NotificationService.onFcmMessage.removeListener(_onPush);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ref.read(etaProvider.notifier).refresh();
}
}
void _onPush() => ref.read(etaProvider.notifier).refresh();
@override
Widget build(BuildContext context) {
final etaAsync = ref.watch(etaProvider);
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(
title: const Text('Estado del camión'),
title: const Text('Mi recolección'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
icon: const Icon(Icons.refresh_rounded),
tooltip: 'Actualizar',
onPressed: () => ref.invalidate(etaProvider),
onPressed: () => ref.read(etaProvider.notifier).refresh(),
),
],
),
body: etaAsync.when(
loading: () => const _EtaLoading(),
error: (error, _) => _EtaError(
error: error.toString(),
onRetry: () => ref.invalidate(etaProvider),
error: (e, _) => _EtaError(
error: e.toString(),
onRetry: () => ref.read(etaProvider.notifier).refresh(),
),
data: (result) => result.hasAddress
? _EtaContent(result: result)
: _NoAddressState(
onAdd: () => context.go('/addresses/new'),
),
: _NoAddressState(onAdd: () => context.go('/addresses/new')),
),
);
}
}
// ── Contenido ETA ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Contenido principal
// ─────────────────────────────────────────────────────────────────────────────
class _EtaContent extends StatelessWidget {
final _EtaResult result;
const _EtaContent({required this.result});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// ── Tarjeta de estado principal ────────────────────────────────
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: AppTheme.primaryMid),
boxShadow: AppTheme.softShadow,
return RefreshIndicator(
onRefresh: () => ProviderScope.containerOf(
context,
).read(etaProvider.notifier).refresh(),
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// ── 1. Hero card ────────────────────────────────────────────────
_EtaHeroCard(result: result),
const SizedBox(height: 16),
// ── 2. Domicilio registrado ─────────────────────────────────────
AppInfoRow(
icon: Icons.home_outlined,
label: 'Col. ${result.colonia}',
value: result.direccion.isEmpty ? 'Mi domicilio' : result.direccion,
trailing: AppStatusBadge.green('Activo'),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 12),
// ── 3. Pasos de progreso (justo debajo del domicilio) ───────────
ProgressSteps(stepIndex: result.stepIndex),
const SizedBox(height: 12),
// ── 4. Banner de prevención ─────────────────────────────────────
const PreventionBanner(),
const SizedBox(height: 12),
// ── 5. Badge de suscripción FCM ─────────────────────────────────
const _FcmStatusBadge(),
const SizedBox(height: 16),
// ── 6. Horario semanal ──────────────────────────────────────────
AppSectionTitle(title: 'Horario del camión'),
_HorarioCard(),
const SizedBox(height: 24),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Hero card: estado + ventana horaria + barra de progreso
// ─────────────────────────────────────────────────────────────────────────────
class _EtaHeroCard extends StatelessWidget {
final _EtaResult result;
const _EtaHeroCard({required this.result});
Color _bgColor(BuildContext context) {
final cs = Theme.of(context).colorScheme;
if (result.isCompleted) return cs.surfaceContainerHighest;
if (result.isNearby) return const Color(0xFFFFF8E1); // amber-50
return const Color(0xFFE1F5EE); // teal-50
}
Color _accentColor(BuildContext context) {
if (result.isCompleted) return Theme.of(context).colorScheme.outline;
if (result.isNearby) return const Color(0xFFBA7517); // amber-400
return const Color(0xFF1D9E75); // teal-400
}
@override
Widget build(BuildContext context) {
final accent = _accentColor(context);
final textTheme = Theme.of(context).textTheme;
return AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _bgColor(context),
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: accent.withOpacity(0.3)),
boxShadow: AppTheme.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Cabecera: icono + etiqueta + punto vivo
Row(
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Camión recolector',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
),
),
const SizedBox(height: 2),
AppStatusBadge.green(result.etiquetaEstado),
],
),
),
_LiveDot(active: result.status == 'en_ruta'),
],
),
const SizedBox(height: 20),
// Mensaje ETA
Text(
result.mensaje,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
height: 1.3,
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: accent,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 16),
// Barra de progreso
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: result.progreso,
backgroundColor:
AppTheme.primaryMid.withValues(alpha: 0.35),
valueColor:
const AlwaysStoppedAnimation<Color>(AppTheme.primary),
minHeight: 8,
),
),
const SizedBox(height: 6),
const Row(
children: [
Text('Inicio de ruta',
style: TextStyle(
fontSize: 10, color: AppTheme.primaryDark)),
Spacer(),
Text('Tu casa',
style: TextStyle(
fontSize: 10, color: AppTheme.primaryDark)),
],
),
],
),
),
const SizedBox(height: 16),
// ── Domicilio registrado ───────────────────────────────────────
AppInfoRow(
icon: Icons.home_outlined,
label: 'Col. ${result.colonia}',
value: result.direccion.isEmpty ? 'Mi domicilio' : result.direccion,
trailing: AppStatusBadge.green('Activo'),
),
const SizedBox(height: 16),
// ── Aviso de privacidad ────────────────────────────────────────
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.blueLight,
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.shield_outlined, color: AppTheme.blue, size: 18),
SizedBox(width: 10),
const SizedBox(width: 12),
Expanded(
child: Text(
'Tu ubicación exacta y la del camión no se comparten. Solo ves el estado de tu ruta.',
style: TextStyle(
fontSize: 12, color: AppTheme.blue, height: 1.5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Camión recolector',
style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
color: accent,
),
),
const SizedBox(height: 2),
_StatusPill(result: result, accent: accent),
],
),
),
_LiveDot(active: result.status == 'en_ruta'),
],
),
const SizedBox(height: 16),
// Ventana horaria o mensaje de estado
Text(
result.mensaje.isNotEmpty
? result.mensaje
: _windowLabel(result.status),
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: accent,
height: 1.2,
),
),
const SizedBox(height: 16),
// Barra de progreso
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: result.progreso,
backgroundColor: accent.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(accent),
minHeight: 8,
),
),
const SizedBox(height: 6),
Row(
children: [
Text(
'Inicio de ruta',
style: TextStyle(fontSize: 10, color: accent.withOpacity(0.7)),
),
const Spacer(),
Text(
'Tu casa',
style: TextStyle(fontSize: 10, color: accent.withOpacity(0.7)),
),
],
),
],
),
);
}
String _windowLabel(String s) {
switch (s) {
case 'completada':
return 'Servicio finalizado';
case 'diferida':
return 'Servicio diferido';
case 'reasignada':
return 'Ruta reasignada';
default:
return 'En camino';
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Pill de estado con punto pulsante
// ─────────────────────────────────────────────────────────────────────────────
class _StatusPill extends StatelessWidget {
final _EtaResult result;
final Color accent;
const _StatusPill({required this.result, required this.accent});
@override
Widget build(BuildContext context) {
final label = result.isNearby
? 'Cerca de tu domicilio'
: result.isCompleted
? 'Servicio completado'
: 'En camino a tu sector';
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!result.isCompleted) _PulsingDot(color: accent),
if (!result.isCompleted) const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: accent.withOpacity(0.15),
borderRadius: BorderRadius.circular(100),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: accent,
),
),
),
const SizedBox(height: 16),
// ── Horario estimado de la semana ──────────────────────────────
AppSectionTitle(title: 'Horario del camión'),
_HorarioCard(),
],
);
}
}
// ── Punto animado "en vivo" ───────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Punto pulsante (animación de opacidad)
// ─────────────────────────────────────────────────────────────────────────────
class _PulsingDot extends StatefulWidget {
final Color color;
const _PulsingDot({required this.color});
@override
State<_PulsingDot> createState() => _PulsingDotState();
}
class _PulsingDotState extends State<_PulsingDot>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _anim;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat(reverse: true);
_anim = Tween<double>(begin: 1.0, end: 0.3).animate(_ctrl);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _anim,
builder: (_, __) => Opacity(
opacity: _anim.value,
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: widget.color,
shape: BoxShape.circle,
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Punto vivo "EN VIVO" (escala + opacidad)
// ─────────────────────────────────────────────────────────────────────────────
class _LiveDot extends StatefulWidget {
final bool active;
const _LiveDot({required this.active});
@@ -292,34 +508,87 @@ class _LiveDotState extends State<_LiveDot>
@override
Widget build(BuildContext context) {
if (!widget.active) {
return const SizedBox.shrink();
}
if (!widget.active) return const SizedBox.shrink();
return AnimatedBuilder(
animation: _anim,
builder: (_, child) => Container(
builder: (_, __) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.primary
.withValues(alpha: 0.5 + _anim.value * 0.5),
color: AppTheme.primary.withValues(alpha: 0.5 + _anim.value * 0.5),
),
),
);
}
}
// ── Horario ───────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Badge de suscripción FCM
// ─────────────────────────────────────────────────────────────────────────────
class _FcmStatusBadge extends ConsumerWidget {
const _FcmStatusBadge();
@override
Widget build(BuildContext context, WidgetRef ref) {
final routeId = ref.watch(activeRouteIdProvider);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF1D9E75),
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
Expanded(
child: Text.rich(
TextSpan(
children: [
const TextSpan(
text: 'Notificaciones activas ',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextSpan(
text: routeId != null
? 'para topic_$routeId'
: '— suscribiendo...',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
style: const TextStyle(fontSize: 12),
),
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Horario semanal
// ─────────────────────────────────────────────────────────────────────────────
class _HorarioCard extends StatelessWidget {
final List<_HorarioDia> _dias = const [
_HorarioDia(dia: 'Lunes', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Martes', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Miércoles',hora: 'Sin servicio', activo: false),
_HorarioDia(dia: 'Jueves', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Viernes', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Sábado', hora: '9:00 11:00 a.m.', activo: true),
_HorarioDia(dia: 'Domingo', hora: 'Sin servicio', activo: false),
static const _dias = [
_HorarioDia(dia: 'Lunes', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Martes', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Miércoles', hora: 'Sin servicio', activo: false),
_HorarioDia(dia: 'Jueves', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Viernes', hora: '8:00 10:00 a.m.', activo: true),
_HorarioDia(dia: 'Sábado', hora: '9:00 11:00 a.m.', activo: true),
_HorarioDia(dia: 'Domingo', hora: 'Sin servicio', activo: false),
];
@override
@@ -369,11 +638,16 @@ class _HorarioDia {
final String dia;
final String hora;
final bool activo;
const _HorarioDia(
{required this.dia, required this.hora, required this.activo});
const _HorarioDia({
required this.dia,
required this.hora,
required this.activo,
});
}
// ── Sin domicilio ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Sin domicilio registrado
// ─────────────────────────────────────────────────────────────────────────────
class _NoAddressState extends StatelessWidget {
final VoidCallback onAdd;
const _NoAddressState({required this.onAdd});
@@ -393,23 +667,30 @@ class _NoAddressState extends StatelessWidget {
color: AppTheme.primaryLight,
shape: BoxShape.circle,
),
child: const Icon(Icons.home_outlined,
color: AppTheme.primary, size: 40),
child: const Icon(
Icons.home_outlined,
color: AppTheme.primary,
size: 40,
),
),
const SizedBox(height: 20),
const Text(
'Sin domicilio registrado',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary),
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
const Text(
'Registra tu domicilio para\nrecibir el ETA de tu ruta.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13, color: AppTheme.textSecondary, height: 1.5),
fontSize: 13,
color: AppTheme.textSecondary,
height: 1.5,
),
),
const SizedBox(height: 24),
SizedBox(
@@ -426,7 +707,9 @@ class _NoAddressState extends StatelessWidget {
}
}
// ── Cargando ──────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Cargando
// ─────────────────────────────────────────────────────────────────────────────
class _EtaLoading extends StatelessWidget {
const _EtaLoading();
@@ -434,19 +717,23 @@ class _EtaLoading extends StatelessWidget {
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: AppTheme.primary),
SizedBox(height: 16),
Text('Consultando estado del camión…',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14)),
Text(
'Consultando estado del servicio...',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14),
),
],
),
);
}
}
// ── Error ─────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Error
// ─────────────────────────────────────────────────────────────────────────────
class _EtaError extends StatelessWidget {
final String error;
final VoidCallback onRetry;
@@ -460,25 +747,36 @@ class _EtaError extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off_outlined,
color: AppTheme.textSecondary, size: 48),
const Icon(
Icons.wifi_off_rounded,
color: AppTheme.textSecondary,
size: 48,
),
const SizedBox(height: 16),
const Text('No se pudo obtener el estado',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary)),
const Text(
'No se pudo obtener el estado',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(error,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
Text(
error,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 20),
SizedBox(
width: 160,
child: ElevatedButton(
child: FilledButton.icon(
onPressed: onRetry,
child: const Text('Reintentar'),
icon: const Icon(Icons.refresh_rounded),
label: const Text('Reintentar'),
),
),
],