Files
hackathon-innovaflow5.0-cdf…/recolecta_app/lib/features/home/house_screen.dart
shinra32 c91b6e2091 Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>
Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>

implementacion de login, vistas, correcion de errores en vista registro, domicilios
2026-05-22 23:07:24 -06:00

581 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:latlong2/latlong.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: 'token') ?? '';
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),
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: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Funcionalidad próximamente disponible'),
behavior: SnackBarBehavior.floating,
backgroundColor: AppTheme.primary,
),
),
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;
const _MapaColoniaRestringido({required this.colonia});
@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),
);
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(
initialCameraFit: CameraFit.bounds(bounds: bounds),
// ESTO ES LA MAGIA DE LA PRIVACIDAD: Bloquea el mapa a esta caja
cameraConstraint: CameraConstraint.contain(bounds: bounds),
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.drag | InteractiveFlag.pinchZoom,
),
),
children: [
TileLayer(
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,
),
],
),
],
),
);
}
}
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'),
),
),
],
),
);
}
}