Files
home-control/lib/screens/calendario_screen.dart
2026-06-14 15:34:53 -04:00

148 lines
6.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
import '../providers/factura_provider.dart';
import '../providers/servicio_provider.dart';
import '../providers/configuracion_provider.dart';
import '../utils/currency_utils.dart';
class CalendarioScreen extends StatefulWidget {
const CalendarioScreen({super.key});
@override
State<CalendarioScreen> createState() => _CalendarioScreenState();
}
class _CalendarioScreenState extends State<CalendarioScreen> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Map<DateTime, List<Map<String, dynamic>>> _eventos = {};
@override
void initState() {
super.initState();
_cargarEventos();
}
void _cargarEventos() {
final facturaProv = context.read<FacturaProvider>();
final servicioProv = context.read<ServicioProvider>();
facturaProv.cargarFacturas().then((_) {
_construirEventos(facturaProv, servicioProv);
});
}
void _construirEventos(FacturaProvider facturaProv, ServicioProvider servicioProv) {
final eventos = <DateTime, List<Map<String, dynamic>>>{};
for (final f in facturaProv.facturas) {
final fecha = DateTime.tryParse(f.fechaVencimiento);
if (fecha == null) continue;
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
final day = DateTime(fecha.year, fecha.month, fecha.day);
eventos.putIfAbsent(day, () => []).add({
'servicio': servicio?.nombre ?? 'Desconocido',
'monto': f.monto,
'estado': f.estado,
'color': Color(servicio?.color ?? 0xFF6200EE),
});
}
setState(() => _eventos = eventos);
}
@override
Widget build(BuildContext context) {
context.watch<FacturaProvider>();
context.watch<ServicioProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final moneda = configProv.config?.moneda ?? 'USD';
return Scaffold(
appBar: AppBar(title: const Text('Calendario de pagos')),
body: Column(
children: [
TableCalendar(
firstDay: DateTime(2024),
lastDay: DateTime(2035),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
eventLoader: (day) {
final d = DateTime(day.year, day.month, day.day);
return _eventos[d] ?? [];
},
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), shape: BoxShape.circle),
selectedDecoration: BoxDecoration(color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle),
markerDecoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, shape: BoxShape.circle),
),
headerStyle: const HeaderStyle(formatButtonVisible: false, titleCentered: true),
onDaySelected: (selected, focused) {
setState(() {
_selectedDay = selected;
_focusedDay = focused;
});
},
onPageChanged: (focused) => _focusedDay = focused,
calendarBuilders: CalendarBuilders(
markerBuilder: (context, date, events) {
if (events.isEmpty) return null;
final pendientes = events.where((e) => (e as Map)['estado'] == 'Pendiente').length;
final pagadas = events.where((e) => (e as Map)['estado'] == 'Pagada').length;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (pendientes > 0)
Container(margin: const EdgeInsets.only(right: 2), width: 6, height: 6, decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle)),
if (pagadas > 0)
Container(width: 6, height: 6, decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle)),
],
);
},
),
),
const Divider(),
Expanded(
child: _selectedDay == null || (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)]?.isEmpty ?? true)
? Center(child: Text('Sin facturas en esta fecha', style: Theme.of(context).textTheme.bodyLarge))
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)] ?? []).length,
itemBuilder: (_, i) {
final e = _eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)]![i];
final esPagada = e['estado'] == 'Pagada';
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 250 + i * 80),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 16 * (1 - value)),
child: child,
),
);
},
child: Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(backgroundColor: e['color'] as Color, child: Text((e['servicio'] as String).substring(0, 2).toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 12))),
title: Text(e['servicio'] as String, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(CurrencyUtils.formatPrice((e['monto'] as num).toDouble(), moneda)),
trailing: Chip(
label: Text(e['estado'] as String, style: TextStyle(fontSize: 12, color: esPagada ? Colors.green : Colors.orange)),
backgroundColor: esPagada ? Colors.green.withValues(alpha: 0.12) : Colors.orange.withValues(alpha: 0.12),
side: BorderSide.none,
padding: const EdgeInsets.symmetric(horizontal: 4),
),
),
),
);
},
),
),
],
),
);
}
}