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 '../tema/pluriwave_tokens.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 start/end date pair (item 21 / audit 9b.4, replacing the former /// 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); final type = context.pluriType; 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), // Item 21 / audit 9b.4 (t4:454): the "active now" caption is a // teal eyebrow, not default body text. Text( l10n.vacationSummaryActiveCountdown(diasRestantes), style: type.eyebrowLabel.copyWith(color: PluriWaveTokens.brand), ), const SizedBox(height: 12), // Item 21 / audit 9b.4 (t4:451-462): the screen's signature // element is a start/end date pair joined by a gradient rule — // the prototype never draws a determinate progress bar here. _ParFechasVacaciones( inicio: rango.inicioDia, fin: rango.finDia, destacado: true, reglaKey: const ValueKey('vacaciones-regla-activo'), ), if (impacto.pausadas.isNotEmpty) ...[ const SizedBox(height: 12), Text(l10n.vacationImpactPausedLabel(_horas(impacto.pausadas))), ], if (impacto.noAfectadas.isNotEmpty) ...[ const SizedBox(height: 4), Text( l10n.vacationImpactContinuesLabel(_horas(impacto.noAfectadas)), ), ], ], ), ); } 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), // Item 21 / audit 9b.5 (t4:469-479, gap:10 at t4:468): a date-pair // card per row, not a ListTile — the same connector widget the // active hero uses, just not `destacado`. PluriPanelColumn( gap: 10, children: [ for (final rango in proximas) _TarjetaRangoVacaciones( rango: rango, // Reuses the existing "Next range in {days} days" string // (already shipped, already translated) as the header // eyebrow — the prototype's own header text ("EMPIEZA EN // 1 DIA - 15 DIAS") pairs a start countdown with a // duration count; only the countdown half has a // corresponding ARB string today, so that's the honest // subset delivered here. encabezado: l10n.vacationSummaryUpcomingCountdown( rango.inicioDia .difference(DateTime.now().dateOnly()) .inDays, ), ), ], ), ], ), ); } } 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), // No prototype-specified header text exists for an already-ended // range, so `encabezado` is omitted rather than invented. PluriPanelColumn( gap: 10, children: [ for (final rango in pasadas) _TarjetaRangoVacaciones(rango: rango), ], ), ], ), ); } } extension _SoloFecha on DateTime { DateTime dateOnly() => DateTime(year, month, day); } /// Item 21 / audit 9b.5 (t4:469-479): the shared card for both "programados" /// and "pasados" rows — radius 20, opaque `listSurface`, the same date-pair /// connector the active hero uses (flat, not gradient), and a label row /// (icon + range name) matching the prototype's "Verano"/"Puente" caption /// (t4:478). class _TarjetaRangoVacaciones extends StatelessWidget { const _TarjetaRangoVacaciones({required this.rango, this.encabezado}); final RangoVacaciones rango; final String? encabezado; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final t = context.pluriTokens; final type = context.pluriType; return DecoratedBox( decoration: BoxDecoration( color: t.listSurface, borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white.withValues(alpha: 0.08)), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (encabezado != null) ...[ Text( encabezado!, style: type.eyebrowLabel.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.5), ), ), const SizedBox(height: 11), ], _ParFechasVacaciones( inicio: rango.inicioDia, fin: rango.finDia, destacado: false, reglaKey: ValueKey('vacaciones-regla-${rango.id}'), ), const SizedBox(height: 12), Row( children: [ Icon( Icons.label_outline_rounded, size: 17, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.6), ), const SizedBox(width: 8), Expanded( child: Text( localizedVacationName(l10n, rango.nombre), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.6), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ), ); } } /// Item 21 / audit 9b.4 + 9b.5 (t4:459-461, 473-475): a start/end date pair /// joined by a connector rule — the vacation screen's one recurring motif, /// shared by the active hero (`destacado: true`, brand-teal gradient rule) /// and every scheduled/past row (`destacado: false`, flat translucent rule). class _ParFechasVacaciones extends StatelessWidget { const _ParFechasVacaciones({ required this.inicio, required this.fin, required this.destacado, required this.reglaKey, }); final DateTime inicio; final DateTime fin; final bool destacado; final Key reglaKey; @override Widget build(BuildContext context) { final locale = AppLocalizations.of(context).localeName; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ _BloqueFecha(fecha: inicio, locale: locale, alinearDerecha: false), const SizedBox(width: 12), Expanded( child: DecoratedBox( key: reglaKey, decoration: BoxDecoration( borderRadius: BorderRadius.circular(1), gradient: destacado ? LinearGradient( colors: [ PluriWaveTokens.brand, PluriWaveTokens.brand.withValues(alpha: 0.3), ], ) : null, color: destacado ? null : Colors.white.withValues(alpha: 0.14), ), child: const SizedBox(height: 2), ), ), const SizedBox(width: 12), _BloqueFecha(fecha: fin, locale: locale, alinearDerecha: true), ], ); } } class _BloqueFecha extends StatelessWidget { const _BloqueFecha({ required this.fecha, required this.locale, required this.alinearDerecha, }); final DateTime fecha; final String locale; final bool alinearDerecha; @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: alinearDerecha ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ // t4:459: 26px/w800/ls-1/lh1. Text( diaMesLocalizado(locale, fecha), style: const TextStyle( fontSize: 26, fontWeight: FontWeight.w800, letterSpacing: -1, height: 1, ), ), const SizedBox(height: 2), // t4:459: 11px/w700/rgba(242,247,250,.5). Text( nombreDiaSemanaLocalizado(locale, fecha), style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ); } } /// 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), ], ), ); } }