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
This commit is contained in:
385
views_v2/eta_screen.dart
Normal file
385
views_v2/eta_screen.dart
Normal file
@@ -0,0 +1,385 @@
|
||||
// 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user