678 lines
22 KiB
Dart
678 lines
22 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,
|
||
body: Column(
|
||
children: [
|
||
_buildPageHeader(context, showEdit: false),
|
||
const Expanded(
|
||
child: Center(
|
||
child: Text(
|
||
'No tienes un domicilio registrado.',
|
||
style: TextStyle(fontSize: 15, color: AppTheme.textSecondary),
|
||
),
|
||
),
|
||
),
|
||
_buildAddressButton(context),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Scaffold(
|
||
backgroundColor: AppTheme.background,
|
||
body: CustomScrollView(
|
||
slivers: [
|
||
SliverToBoxAdapter(child: _buildPageHeader(context, showEdit: true)),
|
||
SliverPadding(
|
||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||
sliver: SliverList(
|
||
delegate: SliverChildListDelegate([
|
||
const AppSectionTitle(title: 'Domicilio registrado'),
|
||
_CasaCard(casa: _casa!),
|
||
const SizedBox(height: 20),
|
||
const AppSectionTitle(title: 'Mapa del Sector (Restringido)'),
|
||
_MapaColoniaRestringido(
|
||
colonia: _casa!.colonia,
|
||
lat: _casa!.lat,
|
||
lng: _casa!.lng,
|
||
),
|
||
const SizedBox(height: 20),
|
||
const AppSectionTitle(title: 'Radio de alerta'),
|
||
_RadioAlertaCard(
|
||
radioActual: _casa!.radioAlertaMetros,
|
||
onChanged: (v) => setState(
|
||
() => _casa = _casa!.copyWith(radioAlertaMetros: v),
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
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: 20),
|
||
const AppSectionTitle(title: 'Horario del camión'),
|
||
_HorarioCard(),
|
||
const SizedBox(height: 24),
|
||
]),
|
||
),
|
||
),
|
||
SliverToBoxAdapter(child: _buildAddressButton(context)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPageHeader(BuildContext context, {required bool showEdit}) {
|
||
return Container(
|
||
padding: EdgeInsets.fromLTRB(
|
||
20,
|
||
MediaQuery.of(context).padding.top + 12,
|
||
20,
|
||
24,
|
||
),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [Color(0xFF4A0E26), Color(0xFF9B1B4A)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.only(
|
||
bottomLeft: Radius.circular(28),
|
||
bottomRight: Radius.circular(28),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: const Icon(
|
||
Icons.home_outlined,
|
||
color: Colors.white,
|
||
size: 24,
|
||
),
|
||
),
|
||
const SizedBox(width: 14),
|
||
const Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Mi Casa',
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.w700,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
SizedBox(height: 2),
|
||
Text(
|
||
'Domicilio registrado',
|
||
style: TextStyle(fontSize: 13, color: Colors.white70),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (showEdit)
|
||
GestureDetector(
|
||
onTap: () => _mostrarEditarDireccion(context),
|
||
child: Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: const Icon(
|
||
Icons.edit_outlined,
|
||
color: Colors.white,
|
||
size: 18,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAddressButton(BuildContext context) {
|
||
return SafeArea(
|
||
top: false,
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 96),
|
||
child: SizedBox(
|
||
width: double.infinity,
|
||
child: ElevatedButton.icon(
|
||
onPressed: () async {
|
||
final added = await context.push<bool>('/add-address');
|
||
if (added == true && mounted) {
|
||
setState(() => _isLoading = true);
|
||
_cargarDomicilio();
|
||
}
|
||
},
|
||
icon: const Icon(Icons.add_home_outlined, size: 20),
|
||
label: const Text('Agregar otra dirección'),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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(
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.surface,
|
||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||
border: Border.all(color: AppTheme.border, width: 0.5),
|
||
boxShadow: AppTheme.softShadow,
|
||
),
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(AppTheme.radiusLg - 0.5),
|
||
child: IntrinsicHeight(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Container(width: 3, color: AppTheme.primary),
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
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 =
|
||
kColoniasCoordinates[colonia] ?? const LatLng(20.5222, -100.8123);
|
||
final pin = (lat != null && lng != null) ? LatLng(lat!, lng!) : center;
|
||
|
||
return Container(
|
||
height: 220,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||
border: Border.all(color: AppTheme.border, width: 1),
|
||
boxShadow: AppTheme.softShadow,
|
||
),
|
||
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'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|