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
583 lines
19 KiB
Dart
583 lines
19 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: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';
|
||
import '../../core/widgets/app_widgets.dart';
|
||
|
||
class MyHouseScreen extends StatefulWidget {
|
||
const MyHouseScreen({super.key});
|
||
|
||
@override
|
||
State<MyHouseScreen> createState() => _MyHouseScreenState();
|
||
}
|
||
|
||
class _MyHouseScreenState extends State<MyHouseScreen> {
|
||
bool _isLoading = true;
|
||
UIHouseModel? _casa;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_cargarDomicilio();
|
||
}
|
||
|
||
Future<void> _cargarDomicilio() async {
|
||
try {
|
||
const storage = FlutterSecureStorage();
|
||
final token = await storage.read(key: authTokenStorageKey) ?? '';
|
||
|
||
if (token.isEmpty) {
|
||
setState(() => _isLoading = false);
|
||
return;
|
||
}
|
||
|
||
final dio = Dio(
|
||
BaseOptions(
|
||
baseUrl: const String.fromEnvironment(
|
||
'API_BASE_URL',
|
||
defaultValue: 'http://localhost:8000',
|
||
),
|
||
headers: {'Authorization': 'Bearer $token'},
|
||
),
|
||
);
|
||
|
||
final res = await dio.get('/addresses');
|
||
if (res.data is List && (res.data as List).isNotEmpty) {
|
||
final addr = res.data[0];
|
||
setState(() {
|
||
_casa = UIHouseModel.fromJson(addr);
|
||
_isLoading = false;
|
||
});
|
||
} else {
|
||
setState(() => _isLoading = false);
|
||
}
|
||
} catch (e) {
|
||
setState(() => _isLoading = false);
|
||
debugPrint('Error al cargar domicilio: $e');
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (_isLoading) {
|
||
return const Scaffold(
|
||
backgroundColor: AppTheme.background,
|
||
body: Center(child: CircularProgressIndicator()),
|
||
);
|
||
}
|
||
|
||
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.')),
|
||
);
|
||
}
|
||
|
||
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: ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
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,
|
||
),
|
||
const SizedBox(height: 16),
|
||
const AppSectionTitle(title: 'Radio de alerta'),
|
||
_RadioAlertaCard(
|
||
radioActual: _casa!.radioAlertaMetros,
|
||
onChanged: (v) =>
|
||
setState(() => _casa = _casa!.copyWith(radioAlertaMetros: v)),
|
||
),
|
||
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 {
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _mostrarEditarDireccion(BuildContext context) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: AppTheme.surface,
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(AppTheme.radiusXl),
|
||
),
|
||
),
|
||
builder: (_) => _EditarDireccionSheet(casa: _casa!),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Tarjeta de la casa ────────────────────────────────────────────────────────
|
||
class _CasaCard extends StatelessWidget {
|
||
final UIHouseModel casa;
|
||
const _CasaCard({required this.casa});
|
||
|
||
@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),
|
||
boxShadow: AppTheme.softShadow,
|
||
),
|
||
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: 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',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Mapa de Colonia (Restringido para Privacidad) ──────────────────────────────
|
||
class _MapaColoniaRestringido extends StatelessWidget {
|
||
final String colonia;
|
||
final double? lat;
|
||
final double? lng;
|
||
const _MapaColoniaRestringido({required this.colonia, this.lat, this.lng});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final center = kColoniaCenter(colonia);
|
||
final pin = (lat != null && lng != null) ? LatLng(lat!, lng!) : center;
|
||
|
||
return Container(
|
||
height: 200,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||
border: Border.all(color: AppTheme.border, width: 1),
|
||
),
|
||
clipBehavior: Clip.hardEdge,
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _DetailRow extends StatelessWidget {
|
||
final IconData icon;
|
||
final String text;
|
||
const _DetailRow({required this.icon, required this.text});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(icon, size: 15, color: AppTheme.textSecondary),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
text,
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: AppTheme.textSecondary,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Radio de alerta ───────────────────────────────────────────────────────────
|
||
class _RadioAlertaCard extends StatelessWidget {
|
||
final int radioActual;
|
||
final ValueChanged<int> onChanged;
|
||
const _RadioAlertaCard({required this.radioActual, required this.onChanged});
|
||
|
||
@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.border, width: 0.5),
|
||
boxShadow: AppTheme.softShadow,
|
||
),
|
||
child: Column(
|
||
children: [200, 400, 600].map((dist) {
|
||
final selected = dist == radioActual;
|
||
return GestureDetector(
|
||
onTap: () => onChanged(dist),
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
||
decoration: BoxDecoration(
|
||
color: selected ? AppTheme.primaryLight : AppTheme.background,
|
||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||
border: Border.all(
|
||
color: selected ? AppTheme.primary : AppTheme.border,
|
||
width: selected ? 1.5 : 0.5,
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
selected
|
||
? Icons.radio_button_checked
|
||
: Icons.radio_button_unchecked,
|
||
color: selected ? AppTheme.primary : AppTheme.border,
|
||
size: 18,
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(
|
||
'$dist metros',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: selected
|
||
? AppTheme.primaryDark
|
||
: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
),
|
||
if (selected)
|
||
Text(
|
||
dist == 200
|
||
? '~2-3 min'
|
||
: dist == 400
|
||
? '~4-5 min'
|
||
: '~6-8 min',
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: AppTheme.primary,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Notificaciones ────────────────────────────────────────────────────────────
|
||
class _NotificacionesCard extends StatelessWidget {
|
||
final UIHouseModel casa;
|
||
final ValueChanged<bool> onAlertaCercanaChanged;
|
||
final ValueChanged<bool> onAlertaMediaChanged;
|
||
final ValueChanged<bool> onRecordatorioChanged;
|
||
|
||
const _NotificacionesCard({
|
||
required this.casa,
|
||
required this.onAlertaCercanaChanged,
|
||
required this.onAlertaMediaChanged,
|
||
required this.onRecordatorioChanged,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.surface,
|
||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||
border: Border.all(color: AppTheme.border, width: 0.5),
|
||
boxShadow: AppTheme.softShadow,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
AppLabeledSwitch(
|
||
label: 'Alerta cuando el camión esté cerca',
|
||
value: casa.alertaCercana,
|
||
onChanged: onAlertaCercanaChanged,
|
||
),
|
||
const Divider(height: 1, color: AppTheme.borderLight),
|
||
AppLabeledSwitch(
|
||
label: 'Alerta a distancia media',
|
||
value: casa.alertaMedia,
|
||
onChanged: onAlertaMediaChanged,
|
||
),
|
||
const Divider(height: 1, color: AppTheme.borderLight),
|
||
AppLabeledSwitch(
|
||
label: 'Recordatorio diario del horario',
|
||
value: casa.recordatorioDiario,
|
||
onChanged: onRecordatorioChanged,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Horario ───────────────────────────────────────────────────────────────────
|
||
class _HorarioCard extends StatelessWidget {
|
||
final List<_HorarioDia> _dias = const [
|
||
_HorarioDia(dia: 'Lunes', hora: '8:00 – 10:00 a.m.', activo: true),
|
||
_HorarioDia(dia: 'Martes', hora: '8:00 – 10:00 a.m.', activo: true),
|
||
_HorarioDia(dia: 'Miércoles', hora: 'Sin servicio', activo: false),
|
||
_HorarioDia(dia: 'Jueves', hora: '8:00 – 10:00 a.m.', activo: true),
|
||
_HorarioDia(dia: 'Viernes', hora: '8:00 – 10:00 a.m.', activo: true),
|
||
_HorarioDia(dia: 'Sábado', hora: '9:00 – 11:00 a.m.', activo: true),
|
||
_HorarioDia(dia: 'Domingo', hora: 'Sin servicio', activo: false),
|
||
];
|
||
|
||
@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.border, width: 0.5),
|
||
boxShadow: AppTheme.softShadow,
|
||
),
|
||
child: Column(
|
||
children: _dias.map((d) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
d.dia,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w500,
|
||
color: d.activo
|
||
? AppTheme.textPrimary
|
||
: AppTheme.textSecondary,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
d.hora,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: d.activo ? AppTheme.primary : AppTheme.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _HorarioDia {
|
||
final String dia;
|
||
final String hora;
|
||
final bool activo;
|
||
const _HorarioDia({
|
||
required this.dia,
|
||
required this.hora,
|
||
required this.activo,
|
||
});
|
||
}
|
||
|
||
// ── Sheet editar dirección ────────────────────────────────────────────────────
|
||
class _EditarDireccionSheet extends StatelessWidget {
|
||
final UIHouseModel casa;
|
||
const _EditarDireccionSheet({required this.casa});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: EdgeInsets.only(
|
||
left: 24,
|
||
right: 24,
|
||
top: 24,
|
||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 36,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.border,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
const Text(
|
||
'Editar dirección',
|
||
style: TextStyle(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w700,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
AppFormField(label: 'Calle y número', initialValue: casa.calle),
|
||
const SizedBox(height: 14),
|
||
AppFormField(label: 'Colonia', initialValue: casa.colonia),
|
||
const SizedBox(height: 24),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 50,
|
||
child: ElevatedButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: const Text('Guardar cambios'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|