simulacion de estados y flujo de notificacion, modificacion de estilos en todas las vistas
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -11,6 +12,7 @@ import '../../core/network/api_client.dart';
|
||||
import '../notifications/notification_service.dart';
|
||||
import '../../shared/widgets/prevention_banner.dart';
|
||||
import '../../shared/widgets/progress_steps.dart';
|
||||
import '../separation_guide/ai_pet_chat_screen.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Modelo de resultado ETA
|
||||
@@ -75,11 +77,19 @@ class _EtaResult {
|
||||
// Provider de ETA
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class _EtaNotifier extends AsyncNotifier<_EtaResult> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
Future<_EtaResult> build() => _fetch();
|
||||
Future<_EtaResult> build() {
|
||||
// Consulta silenciosa cada 10 segundos para ver el avance en tiempo real
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 10), (_) => refresh());
|
||||
ref.onDispose(() => _timer?.cancel());
|
||||
|
||||
return _fetch();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(_fetch);
|
||||
}
|
||||
|
||||
@@ -127,6 +137,20 @@ final etaProvider = AsyncNotifierProvider<_EtaNotifier, _EtaResult>(
|
||||
class ActiveRouteIdNotifier extends Notifier<String?> {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void set(String? value) {
|
||||
debugPrint('📡 [FCM] Evaluando suscripción a la ruta: $value');
|
||||
if (state != value) {
|
||||
final oldRoute = state;
|
||||
state = value;
|
||||
if (oldRoute != null) NotificationService.unsubscribeFromRoute(oldRoute);
|
||||
if (value != null) {
|
||||
NotificationService.subscribeToRoute(
|
||||
value,
|
||||
).catchError((e) => debugPrint('❌ Error FCM: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final activeRouteIdProvider = NotifierProvider<ActiveRouteIdNotifier, String?>(
|
||||
@@ -149,7 +173,7 @@ class _CitizenHomeScreenState extends ConsumerState<CitizenHomeScreen>
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// Refresca al recibir push FCM (RUTA_PROXIMITY, ROUTE_START, etc.)
|
||||
// Refresca al recibir push FCM (RUTA_PROXIMITY, ROUTE_START, etc.)F
|
||||
NotificationService.onFcmMessage.addListener(_onPush);
|
||||
}
|
||||
|
||||
@@ -242,11 +266,15 @@ class _EtaContent extends StatelessWidget {
|
||||
const PreventionBanner(),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── 5. Badge de suscripción FCM ─────────────────────────────────
|
||||
// ── 5. Banner del Chat IA (Eco) ─────────────────────────────────
|
||||
const _EcoChatBanner(),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── 6. Badge de suscripción FCM ─────────────────────────────────
|
||||
const _FcmStatusBadge(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── 6. Horario semanal ──────────────────────────────────────────
|
||||
// ── 7. Horario semanal ──────────────────────────────────────────
|
||||
AppSectionTitle(title: 'Horario del camión'),
|
||||
_HorarioCard(),
|
||||
const SizedBox(height: 24),
|
||||
@@ -256,6 +284,71 @@ class _EtaContent extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Banner de Eco (Chat IA)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class _EcoChatBanner extends StatelessWidget {
|
||||
const _EcoChatBanner();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AiPetChatScreen()),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryDark,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||
boxShadow: AppTheme.softShadow,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white24,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'¿Dudas sobre reciclaje?',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Pregúntale a Eco, tu asistente inteligente',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mapa de ubicación del domicilio (no interactivo)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -322,13 +415,13 @@ class _EtaHeroCard extends StatelessWidget {
|
||||
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
|
||||
return const Color(0xFFE8D5DB); // rosa claro institucional
|
||||
}
|
||||
|
||||
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
|
||||
if (result.isNearby) return const Color(0xFFC8A36A); // beige dorado
|
||||
return const Color(0xFF9B1B4A); // vino principal
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -606,7 +699,7 @@ class _FcmStatusBadge extends ConsumerWidget {
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF1D9E75),
|
||||
color: Color(0xFF1E7A46),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,13 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/app_widgets.dart';
|
||||
import '../../shared/widgets/eco_floating_button.dart';
|
||||
import '../notifications/notification_service.dart';
|
||||
import '../alerts/alerts_provider.dart';
|
||||
|
||||
class CitizenShell extends StatelessWidget {
|
||||
class CitizenShell extends ConsumerStatefulWidget {
|
||||
const CitizenShell({super.key, required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<CitizenShell> createState() => _CitizenShellState();
|
||||
}
|
||||
|
||||
class _CitizenShellState extends ConsumerState<CitizenShell> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
NotificationService.onFcmMessage.addListener(_onGlobalPush);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
NotificationService.onFcmMessage.removeListener(_onGlobalPush);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onGlobalPush() {
|
||||
final msg = NotificationService.onFcmMessage.lastMessage;
|
||||
if (msg?.notification != null) {
|
||||
final title = msg!.notification!.title ?? 'Nueva Alerta';
|
||||
final body = msg.notification!.body ?? '';
|
||||
|
||||
// Guardamos la alerta en el historial
|
||||
ref.read(alertsProvider.notifier).addAlert(title, body);
|
||||
|
||||
// Mostramos un globo de notificación amigable dentro de la app
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: AppTheme.primaryDark,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 5),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.notifications_active,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
body,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'VER',
|
||||
textColor: AppTheme.primaryLight,
|
||||
onPressed: () => context.go('/alerts'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _currentIndex(BuildContext context) {
|
||||
final location = GoRouterState.of(context).matchedLocation;
|
||||
if (location.startsWith('/alerts')) return 1;
|
||||
@@ -32,7 +114,7 @@ class CitizenShell extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
body: widget.child,
|
||||
floatingActionButton: const EcoFloatingButton(),
|
||||
bottomNavigationBar: AppBottomNav(
|
||||
currentIndex: _currentIndex(context),
|
||||
|
||||
@@ -75,95 +75,170 @@ class _MyHouseScreenState extends State<MyHouseScreen> {
|
||||
if (_casa == null) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
appBar: AppBar(title: const Text('Mi casa')),
|
||||
body: const Center(child: Text('No tienes un domicilio registrado.')),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildPageHeader(context, showEdit: false),
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'No tienes un domicilio registrado.',
|
||||
style: TextStyle(fontSize: 15, color: AppTheme.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildAddressButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Mi casa'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
onPressed: () => _mostrarEditarDireccion(context),
|
||||
tooltip: 'Editar dirección',
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: _buildPageHeader(context, showEdit: true)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
const AppSectionTitle(title: 'Domicilio registrado'),
|
||||
_CasaCard(casa: _casa!),
|
||||
const SizedBox(height: 20),
|
||||
const AppSectionTitle(title: 'Mapa del Sector (Restringido)'),
|
||||
_MapaColoniaRestringido(
|
||||
colonia: _casa!.colonia,
|
||||
lat: _casa!.lat,
|
||||
lng: _casa!.lng,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const AppSectionTitle(title: 'Radio de alerta'),
|
||||
_RadioAlertaCard(
|
||||
radioActual: _casa!.radioAlertaMetros,
|
||||
onChanged: (v) => setState(
|
||||
() => _casa = _casa!.copyWith(radioAlertaMetros: v),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const AppSectionTitle(title: 'Notificaciones'),
|
||||
_NotificacionesCard(
|
||||
casa: _casa!,
|
||||
onAlertaCercanaChanged: (v) =>
|
||||
setState(() => _casa = _casa!.copyWith(alertaCercana: v)),
|
||||
onAlertaMediaChanged: (v) =>
|
||||
setState(() => _casa = _casa!.copyWith(alertaMedia: v)),
|
||||
onRecordatorioChanged: (v) => setState(
|
||||
() => _casa = _casa!.copyWith(recordatorioDiario: v),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const AppSectionTitle(title: 'Horario del camión'),
|
||||
_HorarioCard(),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: _buildAddressButton(context)),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageHeader(BuildContext context, {required bool showEdit}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
MediaQuery.of(context).padding.top + 12,
|
||||
20,
|
||||
24,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFF4A0E26), Color(0xFF9B1B4A)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(28),
|
||||
bottomRight: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_CasaCard(casa: _casa!),
|
||||
const SizedBox(height: 16),
|
||||
const AppSectionTitle(title: 'Mapa del Sector (Restringido)'),
|
||||
_MapaColoniaRestringido(
|
||||
colonia: _casa!.colonia,
|
||||
lat: _casa!.lat,
|
||||
lng: _casa!.lng,
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.home_outlined,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const AppSectionTitle(title: 'Radio de alerta'),
|
||||
_RadioAlertaCard(
|
||||
radioActual: _casa!.radioAlertaMetros,
|
||||
onChanged: (v) =>
|
||||
setState(() => _casa = _casa!.copyWith(radioAlertaMetros: v)),
|
||||
const SizedBox(width: 14),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Mi Casa',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Domicilio registrado',
|
||||
style: TextStyle(fontSize: 13, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const AppSectionTitle(title: 'Notificaciones'),
|
||||
_NotificacionesCard(
|
||||
casa: _casa!,
|
||||
onAlertaCercanaChanged: (v) =>
|
||||
setState(() => _casa = _casa!.copyWith(alertaCercana: v)),
|
||||
onAlertaMediaChanged: (v) =>
|
||||
setState(() => _casa = _casa!.copyWith(alertaMedia: v)),
|
||||
onRecordatorioChanged: (v) =>
|
||||
setState(() => _casa = _casa!.copyWith(recordatorioDiario: v)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const AppSectionTitle(title: 'Horario del camión'),
|
||||
_HorarioCard(),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (showEdit)
|
||||
GestureDetector(
|
||||
onTap: () => _mostrarEditarDireccion(context),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.edit_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddressButton(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 96),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final added = await context.push<bool>('/add-address');
|
||||
if (added == true && mounted) {
|
||||
setState(() => _isLoading = true);
|
||||
_cargarDomicilio();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||
border: Border.all(color: AppTheme.primaryMid),
|
||||
boxShadow: AppTheme.softShadow,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_home_outlined,
|
||||
color: AppTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Agregar otra dirección',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add_home_outlined, size: 20),
|
||||
label: const Text('Agregar otra dirección'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -191,64 +266,82 @@ class _CasaCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||
border: Border.all(color: AppTheme.primaryMid, width: 0.8),
|
||||
border: Border.all(color: AppTheme.border, width: 0.5),
|
||||
boxShadow: AppTheme.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg - 0.5),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.home_outlined,
|
||||
color: AppTheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(width: 3, color: AppTheme.primary),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
casa.alias,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.home_outlined,
|
||||
color: AppTheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
casa.alias,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
AppStatusBadge.green(
|
||||
casa.activa ? 'Activa' : 'Inactiva',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
AppStatusBadge.green(casa.activa ? 'Activa' : 'Inactiva'),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
const Divider(color: AppTheme.borderLight),
|
||||
const SizedBox(height: 10),
|
||||
_DetailRow(
|
||||
icon: Icons.location_on_outlined,
|
||||
text: casa.direccionCompleta,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DetailRow(
|
||||
icon: Icons.radar_outlined,
|
||||
text:
|
||||
'Alerta a ${casa.radioAlertaMetros} m de distancia',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Divider(color: AppTheme.borderLight),
|
||||
const SizedBox(height: 10),
|
||||
_DetailRow(
|
||||
icon: Icons.location_on_outlined,
|
||||
text: casa.direccionCompleta,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_DetailRow(
|
||||
icon: Icons.radar_outlined,
|
||||
text: 'Alerta a ${casa.radioAlertaMetros} m de distancia',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -263,14 +356,16 @@ class _MapaColoniaRestringido extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final center = kColoniaCenter(colonia);
|
||||
final center =
|
||||
kColoniasCoordinates[colonia] ?? const LatLng(20.5222, -100.8123);
|
||||
final pin = (lat != null && lng != null) ? LatLng(lat!, lng!) : center;
|
||||
|
||||
return Container(
|
||||
height: 200,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||
border: Border.all(color: AppTheme.border, width: 1),
|
||||
boxShadow: AppTheme.softShadow,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: FlutterMap(
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/app_widgets.dart';
|
||||
import '../notifications/notification_service.dart';
|
||||
import '../alerts/alerts_provider.dart';
|
||||
import 'citizen_home_screen.dart';
|
||||
import '../alerts/alerts_screen.dart';
|
||||
import 'house_screen.dart';
|
||||
import '../profile/profile_screen.dart';
|
||||
|
||||
class MainShell extends StatefulWidget {
|
||||
class MainShell extends ConsumerStatefulWidget {
|
||||
const MainShell({super.key});
|
||||
|
||||
@override
|
||||
State<MainShell> createState() => _MainShellState();
|
||||
ConsumerState<MainShell> createState() => _MainShellState();
|
||||
}
|
||||
|
||||
class _MainShellState extends State<MainShell> {
|
||||
class _MainShellState extends ConsumerState<MainShell> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
static const List<Widget> _screens = [
|
||||
@@ -22,6 +27,77 @@ class _MainShellState extends State<MainShell> {
|
||||
ProfileScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
NotificationService.onFcmMessage.addListener(_onGlobalPush);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
NotificationService.onFcmMessage.removeListener(_onGlobalPush);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onGlobalPush() {
|
||||
final msg = NotificationService.onFcmMessage.lastMessage;
|
||||
if (msg?.notification != null) {
|
||||
final title = msg!.notification!.title ?? 'Nueva Alerta';
|
||||
final body = msg.notification!.body ?? '';
|
||||
|
||||
ref.read(alertsProvider.notifier).addAlert(title, body);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: AppTheme.primaryDark,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 5),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.notifications_active,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
body,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'VER',
|
||||
textColor: AppTheme.primaryLight,
|
||||
onPressed: () => setState(() => _currentIndex = 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
Reference in New Issue
Block a user