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 las vistas principales para el usuario ciudadano, primer avance para el panel admin
This commit is contained in:
shinra32
2026-05-23 03:13:46 -06:00
parent 0279ad05f4
commit 45ffba69b2
33 changed files with 2810 additions and 296 deletions

View File

@@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../core/constants/auth_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../core/models/ui_models.dart';
import 'colonias_data.dart';
@@ -30,8 +31,8 @@ class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
Future<void> _loadData() async {
try {
const storage = FlutterSecureStorage();
final token = await storage.read(key: 'token') ?? '';
final token = await storage.read(key: authTokenStorageKey) ?? '';
if (token.isEmpty) {
if (mounted) setState(() => _isLoading = false);
return;
@@ -53,7 +54,8 @@ class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
if (colRes.data is List) {
for (var c in colRes.data) {
final nombre = c['nombre'] ?? c['colonia'] ?? '';
final horario = c['horario_estimado'] ?? c['schedule'] ?? 'Horario no definido';
final horario =
c['horario_estimado'] ?? c['schedule'] ?? 'Horario no definido';
if (nombre.isNotEmpty) {
_horarios[nombre] = horario;
}
@@ -67,14 +69,19 @@ class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
final res = await dio.get('/addresses');
List<UIHouseModel> loadedCasas = [];
if (res.data is List) {
loadedCasas = (res.data as List).map((e) => UIHouseModel.fromJson(e)).toList();
loadedCasas = (res.data as List)
.map((e) => UIHouseModel.fromJson(e))
.toList();
}
// 3. Obtener ETA (Tiempo Estimado) para cada domicilio
Map<String, String> loadedEtas = {};
for (var casa in loadedCasas) {
try {
final etaRes = await dio.get('/eta', queryParameters: {'address_id': casa.id});
final etaRes = await dio.get(
'/eta',
queryParameters: {'address_id': casa.id},
);
loadedEtas[casa.id] = etaRes.data['mensaje'] ?? 'Estado desconocido';
} catch (e) {
loadedEtas[casa.id] = 'Calculando...';
@@ -110,29 +117,30 @@ class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
setState(() => _isLoading = true);
_loadData();
},
)
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _casas.isEmpty
? const Center(
child: Text(
'No tienes domicilios registrados.',
style: TextStyle(color: AppTheme.textSecondary),
),
)
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _casas.length,
separatorBuilder: (_, __) => const SizedBox(height: 24),
itemBuilder: (context, index) {
final casa = _casas[index];
final eta = _etas[casa.id] ?? 'Actualizando...';
final horario = _horarios[casa.colonia] ?? 'Horario asignado a la ruta';
return _HouseEtaCard(casa: casa, etaMsg: eta, horario: horario);
},
),
? const Center(
child: Text(
'No tienes domicilios registrados.',
style: TextStyle(color: AppTheme.textSecondary),
),
)
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _casas.length,
separatorBuilder: (_, __) => const SizedBox(height: 24),
itemBuilder: (context, index) {
final casa = _casas[index];
final eta = _etas[casa.id] ?? 'Actualizando...';
final horario =
_horarios[casa.colonia] ?? 'Horario asignado a la ruta';
return _HouseEtaCard(casa: casa, etaMsg: eta, horario: horario);
},
),
);
}
}
@@ -151,13 +159,11 @@ class _HouseEtaCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final center = kColoniasCoordinates[casa.colonia] ?? const LatLng(20.5222, -100.8123);
// Restricción del mapa a la colonia (Privacidad por Diseño)
final bounds = LatLngBounds(
LatLng(center.latitude - 0.01, center.longitude - 0.01),
LatLng(center.latitude + 0.01, center.longitude + 0.01),
);
// Si el usuario registró coordenadas, las usamos; si no, el centro de la colonia
final coloniaCenter = kColoniaCenter(casa.colonia);
final pin = (casa.lat != null && casa.lng != null)
? LatLng(casa.lat!, casa.lng!)
: coloniaCenter;
return Container(
decoration: BoxDecoration(
@@ -170,15 +176,15 @@ class _HouseEtaCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Mapa Restringido ──
// ── Mapa Restringido a la colonia ──
SizedBox(
height: 180,
child: FlutterMap(
options: MapOptions(
initialCameraFit: CameraFit.bounds(bounds: bounds),
cameraConstraint: CameraConstraint.contain(bounds: bounds),
initialCenter: pin,
initialZoom: 16.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.drag | InteractiveFlag.pinchZoom,
flags: InteractiveFlag.none,
),
),
children: [
@@ -186,22 +192,24 @@ class _HouseEtaCard extends StatelessWidget {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.onlineshack.recolecta',
),
CircleLayer(
circles: [
CircleMarker(
point: center,
color: AppTheme.primary.withValues(alpha: 0.15),
borderColor: AppTheme.primary,
borderStrokeWidth: 2,
radius: 400,
useRadiusInMeter: true,
MarkerLayer(
markers: [
Marker(
point: pin,
width: 36,
height: 36,
child: const Icon(
Icons.home_rounded,
color: AppTheme.primary,
size: 36,
),
),
],
),
],
),
),
// ── Recuadro de Información ──
Padding(
padding: const EdgeInsets.all(16),
@@ -223,11 +231,19 @@ class _HouseEtaCard extends StatelessWidget {
],
),
const SizedBox(height: 14),
_InfoRow(icon: Icons.location_on_outlined, title: 'Dirección', value: casa.direccionCompleta),
_InfoRow(
icon: Icons.location_on_outlined,
title: 'Dirección',
value: casa.direccionCompleta,
),
const SizedBox(height: 12),
_InfoRow(icon: Icons.schedule_outlined, title: 'Horario Habitual', value: horario),
_InfoRow(
icon: Icons.schedule_outlined,
title: 'Horario Habitual',
value: horario,
),
const SizedBox(height: 18),
// ── Alerta de ETA en Tiempo Real ──
Container(
padding: const EdgeInsets.all(12),
@@ -239,7 +255,10 @@ class _HouseEtaCard extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.local_shipping_outlined, color: AppTheme.primaryDark),
const Icon(
Icons.local_shipping_outlined,
color: AppTheme.primaryDark,
),
const SizedBox(width: 10),
Expanded(
child: Column(
@@ -282,8 +301,12 @@ class _InfoRow extends StatelessWidget {
final IconData icon;
final String title;
final String value;
const _InfoRow({required this.icon, required this.title, required this.value});
const _InfoRow({
required this.icon,
required this.title,
required this.value,
});
@override
Widget build(BuildContext context) {
@@ -298,12 +321,20 @@ class _InfoRow extends StatelessWidget {
children: [
Text(
title,
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary, fontWeight: FontWeight.w500),
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary, height: 1.3),
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
height: 1.3,
),
),
],
),

View File

@@ -1,8 +1,5 @@
import 'package:latlong2/latlong.dart';
// Coordenadas de referencia para el centro de cada colonia en Celaya, Gto.
// Para el MVP, estas coordenadas son fijas y coinciden con el JSON de `colonias-rutas`.
// En una versión futura, podrían venir de una API de geocodificación o de la BD.
const Map<String, LatLng> kColoniasCoordinates = {
'Zona Centro': LatLng(20.52254, -100.81153),
'Las Arboledas': LatLng(20.51422, -100.82793),
@@ -12,3 +9,12 @@ const Map<String, LatLng> kColoniasCoordinates = {
'Las Insurgentes': LatLng(20.52427, -100.79548),
'Trojes': LatLng(20.50899, -100.77167),
};
/// Lookup case-insensitive y sin espacios extras.
LatLng kColoniaCenter(String colonia) {
final key = colonia.trim().toLowerCase();
for (final e in kColoniasCoordinates.entries) {
if (e.key.toLowerCase() == key) return e.value;
}
return const LatLng(20.52254, -100.81153); // fallback: Zona Centro
}

View File

@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart';
import '../../core/constants/auth_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../core/models/ui_models.dart';
import 'colonias_data.dart';
@@ -28,7 +30,7 @@ class _MyHouseScreenState extends State<MyHouseScreen> {
Future<void> _cargarDomicilio() async {
try {
const storage = FlutterSecureStorage();
final token = await storage.read(key: 'token') ?? '';
final token = await storage.read(key: authTokenStorageKey) ?? '';
if (token.isEmpty) {
setState(() => _isLoading = false);
@@ -96,7 +98,11 @@ class _MyHouseScreenState extends State<MyHouseScreen> {
_CasaCard(casa: _casa!),
const SizedBox(height: 16),
const AppSectionTitle(title: 'Mapa del Sector (Restringido)'),
_MapaColoniaRestringido(colonia: _casa!.colonia),
_MapaColoniaRestringido(
colonia: _casa!.colonia,
lat: _casa!.lat,
lng: _casa!.lng,
),
const SizedBox(height: 16),
const AppSectionTitle(title: 'Radio de alerta'),
_RadioAlertaCard(
@@ -120,13 +126,13 @@ class _MyHouseScreenState extends State<MyHouseScreen> {
_HorarioCard(),
const SizedBox(height: 16),
GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Funcionalidad próximamente disponible'),
behavior: SnackBarBehavior.floating,
backgroundColor: AppTheme.primary,
),
),
onTap: () 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(
@@ -251,19 +257,14 @@ class _CasaCard extends StatelessWidget {
// ── Mapa de Colonia (Restringido para Privacidad) ──────────────────────────────
class _MapaColoniaRestringido extends StatelessWidget {
final String colonia;
const _MapaColoniaRestringido({required this.colonia});
final double? lat;
final double? lng;
const _MapaColoniaRestringido({required this.colonia, this.lat, this.lng});
@override
Widget build(BuildContext context) {
// Usa las coordenadas del archivo centralizado de datos de colonias.
final center =
kColoniasCoordinates[colonia] ?? const LatLng(20.5222, -100.8123);
// Creamos una "caja" o límite geográfico de aprox 1km a la redonda
final bounds = LatLngBounds(
LatLng(center.latitude - 0.01, center.longitude - 0.01),
LatLng(center.latitude + 0.01, center.longitude + 0.01),
);
final center = kColoniaCenter(colonia);
final pin = (lat != null && lng != null) ? LatLng(lat!, lng!) : center;
return Container(
height: 200,
@@ -274,11 +275,10 @@ class _MapaColoniaRestringido extends StatelessWidget {
clipBehavior: Clip.hardEdge,
child: FlutterMap(
options: MapOptions(
initialCameraFit: CameraFit.bounds(bounds: bounds),
// ESTO ES LA MAGIA DE LA PRIVACIDAD: Bloquea el mapa a esta caja
cameraConstraint: CameraConstraint.contain(bounds: bounds),
initialCenter: pin,
initialZoom: 16.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.drag | InteractiveFlag.pinchZoom,
flags: InteractiveFlag.none,
),
),
children: [
@@ -286,15 +286,17 @@ class _MapaColoniaRestringido extends StatelessWidget {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.onlineshack.recolecta',
),
CircleLayer(
circles: [
CircleMarker(
point: center,
color: AppTheme.primary.withValues(alpha: 0.2),
borderColor: AppTheme.primary,
borderStrokeWidth: 2,
radius: 350, // 350 metros a la redonda remarcados
useRadiusInMeter: true,
MarkerLayer(
markers: [
Marker(
point: pin,
width: 36,
height: 36,
child: const Icon(
Icons.home_rounded,
color: AppTheme.primary,
size: 36,
),
),
],
),