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

implementacion de login, vistas, correcion de errores en vista registro, domicilios
2026-05-22 23:07:24 -06:00

385 lines
12 KiB
Dart

// lib/features/eta/eta_screen.dart
// Vista principal del ciudadano: ETA sin mapa ni coordenadas.
// Se refresca en initState y al recibir push FCM (vía NotificationService).
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'eta_model.dart';
import 'eta_provider.dart';
import '../notifications/notification_service.dart';
import '../../shared/widgets/prevention_banner.dart';
import '../../shared/widgets/progress_steps.dart';
class EtaScreen extends ConsumerStatefulWidget {
const EtaScreen({super.key});
@override
ConsumerState<EtaScreen> createState() => _EtaScreenState();
}
class _EtaScreenState extends ConsumerState<EtaScreen>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Escuchar pushes en foreground: refrescar ETA al recibir cualquier
// evento FCM de RUTA_PROXIMITY o ROUTE_START.
NotificationService.onFcmMessage.addListener(_onPush);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
NotificationService.onFcmMessage.removeListener(_onPush);
super.dispose();
}
/// Refresca cuando la app vuelve al foreground.
@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: Theme.of(context).colorScheme.surface,
appBar: AppBar(
title: const Text('Mi recolección'),
actions: [
IconButton(
icon: const Icon(Icons.refresh_rounded),
tooltip: 'Actualizar',
onPressed: () => ref.read(etaProvider.notifier).refresh(),
),
],
),
body: etaAsync.when(
loading: () => const _EtaLoading(),
error: (e, _) => _EtaError(message: e.toString()),
data: (eta) => _EtaContent(eta: eta),
),
);
}
}
// ──────────────────────────────────────────
// Contenido principal cuando hay datos
// ──────────────────────────────────────────
class _EtaContent extends StatelessWidget {
final EtaResponse eta;
const _EtaContent({required this.eta});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () =>
ProviderScope.containerOf(context).read(etaProvider.notifier).refresh(),
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
_EtaHeroCard(eta: eta),
const SizedBox(height: 12),
const PreventionBanner(),
const SizedBox(height: 12),
ProgressSteps(stepIndex: eta.stepIndex),
const SizedBox(height: 12),
const _FcmStatusBadge(),
const SizedBox(height: 24),
],
),
);
}
}
// ──────────────────────────────────────────
// Hero card con ventana horaria y mensaje
// ──────────────────────────────────────────
class _EtaHeroCard extends StatelessWidget {
final EtaResponse eta;
const _EtaHeroCard({required this.eta});
Color _bgColor(BuildContext context) {
final cs = Theme.of(context).colorScheme;
if (eta.isCompleted) return cs.surfaceContainerHighest;
if (eta.isNearby) return const Color(0xFFFFF8E1); // amber-50 equivalente
return const Color(0xFFE1F5EE); // teal-50
}
Color _accentColor(BuildContext context) {
if (eta.isCompleted) return Theme.of(context).colorScheme.outline;
if (eta.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(18),
decoration: BoxDecoration(
color: _bgColor(context),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: accent.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status pill
_StatusPill(eta: eta, accent: accent),
const SizedBox(height: 10),
// Ventana horaria o estado
Text(
eta.ventanaHoraria ?? _windowLabel(eta.status),
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: accent,
height: 1.1,
),
),
const SizedBox(height: 6),
Text(
eta.mensaje,
style: textTheme.bodyMedium?.copyWith(
color: accent.withOpacity(0.85),
height: 1.45,
),
),
],
),
);
}
String _windowLabel(RouteStatus s) {
switch (s) {
case RouteStatus.completada:
return 'Servicio finalizado';
case RouteStatus.diferida:
return 'Servicio diferido';
case RouteStatus.reasignada:
return 'Ruta reasignada';
default:
return 'En camino';
}
}
}
class _StatusPill extends StatelessWidget {
final EtaResponse eta;
final Color accent;
const _StatusPill({required this.eta, required this.accent});
@override
Widget build(BuildContext context) {
final label = eta.isNearby
? 'Cerca de tu domicilio'
: eta.isCompleted
? 'Servicio completado'
: 'En camino a tu sector';
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!eta.isCompleted)
_PulsingDot(color: accent),
if (!eta.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,
),
),
),
],
);
}
}
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 AnimationController _ctrl;
late 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,
),
),
),
);
}
}
// ──────────────────────────────────────────
// 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),
),
),
],
),
);
}
}
// Expone el routeId activo (se puebla desde el provider de sesión/domicilio)
final activeRouteIdProvider = StateProvider<String?>((ref) => null);
// ──────────────────────────────────────────
// Estados de carga y error
// ──────────────────────────────────────────
class _EtaLoading extends StatelessWidget {
const _EtaLoading();
@override
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator.adaptive(),
SizedBox(height: 12),
Text('Consultando estado del servicio...'),
],
),
);
}
}
class _EtaError extends StatelessWidget {
final String message;
const _EtaError({required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.wifi_off_rounded, size: 48, color: Colors.grey),
const SizedBox(height: 12),
const Text(
'No se pudo obtener el estado',
style: TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 6),
Text(
message,
style: const TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: () =>
ProviderScope.containerOf(context)
.read(etaProvider.notifier)
.refresh(),
icon: const Icon(Icons.refresh_rounded),
label: const Text('Reintentar'),
),
],
),
),
);
}
}