import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_alarmas.dart'; import '../l10n/display_names.dart'; import '../l10n/formato_fechas.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/alarma_musical.dart'; import '../tema/pluriwave_theme.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_layout.dart'; import '../widgets/pluri_push_scaffold.dart'; /// Vacaciones manager (design ADR-6, `alarm-vacation-ranges` spec, WU9): /// active-range hero (name + days-remaining + per-alarm impact line), the /// "PROGRAMADOS" upcoming list, an "Añadir rango" CTA, and a "Rangos /// pasados" history section. This is the REAL destination WU8's Alarmas- /// root summary row pushes to, replacing WU8's own temporary placeholder /// (`_PantallaVacacionesTemporal` in `pantalla_alarmas.dart`). class PantallaVacaciones extends StatelessWidget { const PantallaVacaciones({super.key}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final estado = context.watch(); final activo = estado.rangoVacacionesActivo(); final proximas = estado.vacacionesProximas(); final pasadas = estado.vacacionesPasadas(); return PluriPushScaffold( title: l10n.vacationRangesTitle, body: ListView( padding: PluriLayout.pageContentPadding, children: [ if (activo != null) _HeroRangoActivo(estado: estado, rango: activo) else PluriGlassSurface(child: Text(l10n.vacationNoActiveRangeHint)), const SizedBox(height: 16), _SeccionProgramados(proximas: proximas), const SizedBox(height: 16), Align( alignment: Alignment.centerLeft, child: OutlinedButton.icon( onPressed: () => _abrirAlta(context), icon: const Icon(Icons.add_rounded), label: Text(l10n.addVacationRangeCta), ), ), const SizedBox(height: 16), _SeccionRangosPasados(pasadas: pasadas), ], ), ); } Future _abrirAlta(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => const _EditorVacacionesSheet(), ); } } /// Active-range hero: name, days-remaining countdown (reusing WU8's own /// `vacationSummaryActiveCountdown` string — same concept, bigger stage), /// a determinate progress bar, and the per-alarm impact line(s) from /// `EstadoAlarmas.impactoDeRango`. class _HeroRangoActivo extends StatelessWidget { const _HeroRangoActivo({required this.estado, required this.rango}); final EstadoAlarmas estado; final RangoVacaciones rango; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final hoy = DateTime.now(); final hoyDia = DateTime(hoy.year, hoy.month, hoy.day); final diasRestantes = rango.finDia.difference(hoyDia).inDays; final impacto = estado.impactoDeRango(rango); return PluriGlassSurface( glowColor: context.pluriTokens.electricMagenta.withValues(alpha: 0.24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( localizedVacationName(l10n, rango.nombre), style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 4), Text(l10n.vacationSummaryActiveCountdown(diasRestantes)), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(999), child: LinearProgressIndicator(value: _progreso(hoyDia)), ), if (impacto.pausadas.isNotEmpty) ...[ const SizedBox(height: 10), Text(l10n.vacationImpactPausedLabel(_horas(impacto.pausadas))), ], if (impacto.noAfectadas.isNotEmpty) ...[ const SizedBox(height: 4), Text( l10n.vacationImpactContinuesLabel(_horas(impacto.noAfectadas)), ), ], ], ), ); } double _progreso(DateTime hoyDia) { final totalDias = rango.finDia.difference(rango.inicioDia).inDays + 1; if (totalDias <= 0) return 0; final transcurridos = hoyDia.difference(rango.inicioDia).inDays + 1; return (transcurridos / totalDias).clamp(0.0, 1.0); } String _horas(List alarmas) => alarmas .map( (a) => '${a.hora.toString().padLeft(2, '0')}:${a.minuto.toString().padLeft(2, '0')}', ) .join(', '); } class _SeccionProgramados extends StatelessWidget { const _SeccionProgramados({required this.proximas}); final List proximas; @override Widget build(BuildContext context) { if (proximas.isEmpty) return const SizedBox.shrink(); final l10n = AppLocalizations.of(context); final type = context.pluriType; return PluriGlassSurface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.vacationUpcomingSectionTitle, style: type.eyebrowLabel), const SizedBox(height: 8), for (final rango in proximas) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.event_rounded), title: Text(localizedVacationName(l10n, rango.nombre)), subtitle: Text(_rangoFechas(l10n, rango)), ), ], ), ); } } class _SeccionRangosPasados extends StatelessWidget { const _SeccionRangosPasados({required this.pasadas}); final List pasadas; @override Widget build(BuildContext context) { if (pasadas.isEmpty) return const SizedBox.shrink(); final l10n = AppLocalizations.of(context); final type = context.pluriType; return PluriGlassSurface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.vacationPastSectionTitle, style: type.eyebrowLabel), const SizedBox(height: 8), for (final rango in pasadas) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.history_rounded), title: Text(localizedVacationName(l10n, rango.nombre)), subtitle: Text(_rangoFechas(l10n, rango)), ), ], ), ); } } String _rangoFechas(AppLocalizations l10n, RangoVacaciones rango) => '${fechaCortaLocalizada(l10n.localeName, rango.inicioDia)} → ' '${fechaCortaLocalizada(l10n.localeName, rango.finDia)}'; /// Add-range form. Moved verbatim from `pantalla_alarmas.dart` (WU8's /// `_PantallaVacacionesTemporal` used it as a placeholder push target; now /// this screen is the one real consumer). Behaviour unchanged. class _EditorVacacionesSheet extends StatefulWidget { const _EditorVacacionesSheet(); @override State<_EditorVacacionesSheet> createState() => _EditorVacacionesSheetState(); } class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> { // Created lazily: AppLocalizations.of(context) cannot be read in // initState (inherited-widget lookup assert in debug builds). TextEditingController? _nombreController; late DateTime _inicio; late DateTime _fin; @override void initState() { super.initState(); final hoy = DateTime.now(); _inicio = DateTime(hoy.year, hoy.month, hoy.day); _fin = _inicio.add(const Duration(days: 2)); } @override void didChangeDependencies() { super.didChangeDependencies(); _nombreController ??= TextEditingController( text: AppLocalizations.of(context).vacationsDefaultName, ); } @override void dispose() { _nombreController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final bottom = MediaQuery.of(context).viewInsets.bottom; return Padding( padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12), child: PluriGlassSurface( borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.all(18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.newVacationRangeTitle, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 12), TextField( controller: _nombreController, decoration: InputDecoration(labelText: l10n.nameLabel), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _PickerButton( icon: Icons.play_arrow_rounded, label: l10n.startLabel, value: fechaCortaLocalizada(l10n.localeName, _inicio), onTap: () => _elegirFecha(esInicio: true), ), ), const SizedBox(width: 10), Expanded( child: _PickerButton( icon: Icons.stop_rounded, label: l10n.endLabel, value: fechaCortaLocalizada(l10n.localeName, _fin), onTap: () => _elegirFecha(esInicio: false), ), ), ], ), const SizedBox(height: 16), FilledButton.icon( onPressed: _guardar, icon: const Icon(Icons.check_rounded), label: Text(l10n.saveRangeAction), ), ], ), ), ); } Future _elegirFecha({required bool esInicio}) async { final actual = esInicio ? _inicio : _fin; final hoy = DateTime.now(); final seleccion = await showDatePicker( context: context, initialDate: actual, firstDate: DateTime(hoy.year, hoy.month, hoy.day), lastDate: hoy.add(const Duration(days: 1460)), ); if (seleccion == null) return; setState(() { if (esInicio) { _inicio = seleccion; if (_fin.isBefore(_inicio)) _fin = _inicio; } else { _fin = seleccion; } }); } Future _guardar() async { final estado = context.read(); final rango = estado.servicio.crearRangoVacaciones( inicio: _inicio, fin: _fin, nombre: _nombreController?.text.trim() ?? '', ); await estado.crearRangoVacaciones(rango); if (mounted) Navigator.pop(context); } } class _PickerButton extends StatelessWidget { const _PickerButton({ required this.icon, required this.label, required this.value, required this.onTap, }); final IconData icon; final String label; final String value; final VoidCallback? onTap; @override Widget build(BuildContext context) { return OutlinedButton.icon( onPressed: onTap, icon: Icon(icon), label: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context).textTheme.labelSmall), Text(value), ], ), ); } }