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
346 lines
11 KiB
Dart
346 lines
11 KiB
Dart
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: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';
|
|
|
|
class CitizenHomeScreen extends StatefulWidget {
|
|
const CitizenHomeScreen({super.key});
|
|
|
|
@override
|
|
State<CitizenHomeScreen> createState() => _CitizenHomeScreenState();
|
|
}
|
|
|
|
class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
|
|
bool _isLoading = true;
|
|
List<UIHouseModel> _casas = [];
|
|
Map<String, String> _etas = {};
|
|
Map<String, String> _horarios = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadData();
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
try {
|
|
const storage = FlutterSecureStorage();
|
|
final token = await storage.read(key: authTokenStorageKey) ?? '';
|
|
|
|
if (token.isEmpty) {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
return;
|
|
}
|
|
|
|
final dio = Dio(
|
|
BaseOptions(
|
|
baseUrl: const String.fromEnvironment(
|
|
'API_BASE_URL',
|
|
defaultValue: 'http://localhost:8000',
|
|
),
|
|
headers: {'Authorization': 'Bearer $token'},
|
|
),
|
|
);
|
|
|
|
// 1. Obtener horarios de las colonias
|
|
try {
|
|
final colRes = await dio.get('/colonias');
|
|
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';
|
|
if (nombre.isNotEmpty) {
|
|
_horarios[nombre] = horario;
|
|
}
|
|
}
|
|
}
|
|
} catch (_) {
|
|
debugPrint('Aviso: No se pudieron cargar los horarios.');
|
|
}
|
|
|
|
// 2. Obtener los domicilios del ciudadano
|
|
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();
|
|
}
|
|
|
|
// 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},
|
|
);
|
|
loadedEtas[casa.id] = etaRes.data['mensaje'] ?? 'Estado desconocido';
|
|
} catch (e) {
|
|
loadedEtas[casa.id] = 'Calculando...';
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_casas = loadedCasas;
|
|
_etas = loadedEtas;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error en CitizenHomeScreen: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppTheme.background,
|
|
appBar: AppBar(
|
|
title: const Text('Estado del Servicio'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
tooltip: 'Actualizar tiempos',
|
|
onPressed: () {
|
|
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);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Widget para la Tarjeta de Mapa y ETA ─────────────────────────────────────
|
|
class _HouseEtaCard extends StatelessWidget {
|
|
final UIHouseModel casa;
|
|
final String etaMsg;
|
|
final String horario;
|
|
|
|
const _HouseEtaCard({
|
|
required this.casa,
|
|
required this.etaMsg,
|
|
required this.horario,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 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(
|
|
color: AppTheme.surface,
|
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
|
border: Border.all(color: AppTheme.border),
|
|
boxShadow: AppTheme.cardShadow,
|
|
),
|
|
clipBehavior: Clip.hardEdge,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// ── Mapa Restringido a la colonia ──
|
|
SizedBox(
|
|
height: 180,
|
|
child: FlutterMap(
|
|
options: MapOptions(
|
|
initialCenter: pin,
|
|
initialZoom: 16.0,
|
|
interactionOptions: const InteractionOptions(
|
|
flags: InteractiveFlag.none,
|
|
),
|
|
),
|
|
children: [
|
|
TileLayer(
|
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
userAgentPackageName: 'com.onlineshack.recolecta',
|
|
),
|
|
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),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.home, color: AppTheme.primary, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
casa.alias.isNotEmpty ? casa.alias : 'Mi Domicilio',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
_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,
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// ── Alerta de ETA en Tiempo Real ──
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryLight,
|
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
border: Border.all(color: AppTheme.primaryMid),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Icon(
|
|
Icons.local_shipping_outlined,
|
|
color: AppTheme.primaryDark,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Estado del Camión',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.primaryDark,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
etaMsg,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppTheme.primaryDark,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Fila auxiliar de info ────────────────────────────────────────────────────
|
|
class _InfoRow extends StatelessWidget {
|
|
final IconData icon;
|
|
final String title;
|
|
final String value;
|
|
|
|
const _InfoRow({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.value,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 18, color: AppTheme.textSecondary),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|