Files
hackathon-innovaflow5.0-cdf…/recolecta_app/lib/features/home/house_screen.dart
shinra32 45ffba69b2 Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>
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
2026-05-23 03:13:46 -06:00

583 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'),
),
),
],
),
);
}
}