import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_alarmas.dart'; import '../estado/estado_radio.dart'; import '../modelos/alarma_musical.dart'; import '../modelos/emisora.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; import '../widgets/pluri_premium_widgets.dart'; class PantallaAlarmas extends StatelessWidget { const PantallaAlarmas({super.key}); @override Widget build(BuildContext context) { final estado = context.watch(); return RefreshIndicator( onRefresh: estado.refrescarProgramacion, child: ListView( padding: const EdgeInsets.fromLTRB(0, 0, 0, 124), children: [ PluriScreenHeader( title: 'Despertar musical', subtitle: 'Alarmas con radio, sonido seguro, vacaciones inteligentes y próxima ejecución siempre visible.', glyph: PluriIconGlyph.alarm, primaryActionLabel: 'Crear alarma', onPrimaryAction: () => _abrirEditor(context), trailing: PluriStatusPill( icon: Icons.alarm_on_rounded, label: '${estado.alarmas.length} alarmas', ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ _PanelProximaAlarma(estado: estado), const SizedBox(height: 12), if (estado.alarmas.isEmpty) const _EmptyAlarmas() else for (final alarma in estado.alarmas) ...[ _TarjetaAlarma(alarma: alarma), const SizedBox(height: 12), ], _PanelVacaciones(estado: estado), const SizedBox(height: 12), _AccesoDiagnostico(estado: estado), ], ), ), ], ), ); } Future _abrirEditor(BuildContext context, {AlarmaMusical? alarma}) async { await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => _EditorAlarmaSheet(alarma: alarma), ); } } class _PanelProximaAlarma extends StatelessWidget { const _PanelProximaAlarma({required this.estado}); final EstadoAlarmas estado; @override Widget build(BuildContext context) { final proxima = estado.proximaAlarma; return PluriGlassSurface( glowColor: const Color(0xFFFFB86B).withValues(alpha: 0.28), child: Row( children: [ const _AssetIcon('assets/icons/alarmas/alarm_music.png', size: 72), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( proxima == null ? 'Sin alarmas activas' : 'Próxima alarma', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), const SizedBox(height: 4), Text( proxima == null ? 'Creá una alarma y PluriWave calculará la siguiente ejecución automáticamente.' : '${proxima.nombre} · ${_fechaHora(proxima.proximaEjecucion!)}', ), ], ), ), ], ), ); } } class _TarjetaAlarma extends StatelessWidget { const _TarjetaAlarma({required this.alarma}); final AlarmaMusical alarma; @override Widget build(BuildContext context) { final estado = context.watch(); final excepcion = estado.ultimaExcepcionPara(alarma.id); final mensajeVacaciones = _mensajeVacaciones(estado.vacaciones); return PluriGlassSurface( glowColor: const Color(0xFF22D3EE).withValues(alpha: 0.22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _AssetIcon('assets/icons/alarmas/alarm_music.png', size: 64), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _hora(alarma), style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w900, letterSpacing: -1, ), ), Text(alarma.nombre), ], ), ), Switch.adaptive( value: alarma.activa, onChanged: (value) => estado.cambiarActiva(alarma, value), ), ], ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ _InfoChip(icon: Icons.repeat_rounded, label: _programacion(alarma)), _InfoChip(icon: Icons.snooze_rounded, label: '${alarma.snoozeMinutos} min'), _InfoChip( icon: Icons.beach_access_rounded, label: alarma.sonarEnVacaciones ? 'Suena en vacaciones' : 'Pausa en vacaciones', ), _InfoChip( icon: Icons.volume_up_rounded, label: '${(alarma.volumen * 100).round()}%', ), ], ), const SizedBox(height: 12), if (alarma.proximaEjecucion != null) _NoticeLine( icon: Icons.event_available_rounded, text: 'Siguiente ejecución: ${_fechaHora(alarma.proximaEjecucion!)}', ) else const _NoticeLine( icon: Icons.pause_circle_outline_rounded, text: 'No tiene próxima ejecución activa.', ), if (excepcion != null) ...[ const SizedBox(height: 6), _NoticeLine( icon: Icons.skip_next_rounded, text: 'Una ejecución fue omitida: ${_fechaHora(excepcion.ejecucion)}.', ), ], if (mensajeVacaciones != null) ...[ const SizedBox(height: 6), _NoticeLine( icon: Icons.beach_access_rounded, text: mensajeVacaciones, ), ], const SizedBox(height: 12), Row( children: [ TextButton.icon( icon: const Icon(Icons.edit_rounded), label: const Text('Editar'), onPressed: () => _abrirEditor(context, alarma: alarma), ), TextButton.icon( icon: const Icon(Icons.skip_next_rounded), label: const Text('Omitir siguiente'), onPressed: alarma.proximaEjecucion == null ? null : () async { await estado.saltarProxima(alarma.id); if (context.mounted) { final alarmas = context.read().alarmas; AlarmaMusical? actualizada; for (final item in alarmas) { if (item.id == alarma.id) { actualizada = item; break; } } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( actualizada?.proximaEjecucion == null ? 'Alarma omitida. No queda próxima ejecución.' : 'Alarma omitida. Volverá el ${_fechaHora(actualizada!.proximaEjecucion!)}.', ), ), ); } }, ), const Spacer(), IconButton( tooltip: 'Eliminar', icon: const Icon(Icons.delete_outline_rounded), onPressed: () => estado.eliminarAlarma(alarma.id), ), ], ), ], ), ); } String? _mensajeVacaciones(List vacaciones) { if (alarma.sonarEnVacaciones) return null; final ahora = DateTime.now(); RangoVacaciones? actual; for (final rango in vacaciones) { if (rango.contiene(ahora)) { actual = rango; break; } } if (actual != null) { if (alarma.proximaEjecucion == null) { return 'Está pausada por vacaciones (${actual.nombre}) y sin próxima ejecución.'; } return 'Está pausada por vacaciones (${actual.nombre}) y vuelve el ${_fechaHora(alarma.proximaEjecucion!)}.'; } if (alarma.proximaEjecucion != null) { return 'Con vacaciones activas, volverá a sonar el ${_fechaHora(alarma.proximaEjecucion!)}.'; } return null; } void _abrirEditor(BuildContext context, {required AlarmaMusical alarma}) { showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => _EditorAlarmaSheet(alarma: alarma), ); } } class _EditorAlarmaSheet extends StatefulWidget { const _EditorAlarmaSheet({this.alarma}); final AlarmaMusical? alarma; @override State<_EditorAlarmaSheet> createState() => _EditorAlarmaSheetState(); } class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> { late final TextEditingController _nombreController; late TimeOfDay _hora; late DateTime _fecha; late TipoProgramacionAlarma _tipo; late Set _diasSemana; late int _snooze; late double _volumen; late bool _sonarEnVacaciones; late SonidoInternoAlarma _sonidoInterno; Emisora? _emisora; bool _favoritosSolicitados = false; @override void initState() { super.initState(); final alarma = widget.alarma; final ahora = DateTime.now().add(const Duration(minutes: 5)); _nombreController = TextEditingController( text: alarma?.nombre ?? 'Despertador musical', ); _hora = TimeOfDay(hour: alarma?.hora ?? ahora.hour, minute: alarma?.minuto ?? ahora.minute); _fecha = alarma?.fechaUnica ?? ahora; _tipo = alarma?.tipoProgramacion ?? TipoProgramacionAlarma.unica; _diasSemana = {...alarma?.diasSemana ?? const []}; _snooze = alarma?.snoozeMinutos ?? 5; _volumen = alarma?.volumen ?? 0.85; _sonarEnVacaciones = alarma?.sonarEnVacaciones ?? true; _sonidoInterno = alarma?.sonidoInterno ?? SonidoInternoAlarma.amanecer; _emisora = alarma?.emisora ?? context.read().emisoraPreferida; } @override void dispose() { _nombreController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final radio = context.watch(); final bottom = MediaQuery.of(context).viewInsets.bottom; if (!_favoritosSolicitados) { _favoritosSolicitados = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) context.read().cargarFavoritos(); }); } if (_emisora == null && widget.alarma == null && radio.emisoraPreferida != null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _emisora == null) { setState(() => _emisora = radio.emisoraPreferida); } }); } final favoritas = _favoritasConSeleccion(radio.listaFavoritos); return Padding( padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12), child: PluriGlassSurface( borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.all(18), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _AssetIcon('assets/icons/alarmas/alarm_music.png', size: 58), const SizedBox(width: 12), Expanded( child: Text( widget.alarma == null ? 'Nueva alarma' : 'Editar alarma', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, ), ), ), IconButton( icon: const Icon(Icons.close_rounded), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 14), TextField( controller: _nombreController, decoration: const InputDecoration(labelText: 'Nombre'), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _PickerButton( icon: Icons.schedule_rounded, label: 'Hora', value: _hora.format(context), onTap: _elegirHora, ), ), const SizedBox(width: 10), Expanded( child: _PickerButton( icon: Icons.event_rounded, label: 'Fecha', value: _fechaCorta(_fecha), onTap: _tipo == TipoProgramacionAlarma.unica ? _elegirFecha : null, ), ), ], ), const SizedBox(height: 12), SegmentedButton( segments: const [ ButtonSegment(value: TipoProgramacionAlarma.unica, label: Text('Una vez')), ButtonSegment(value: TipoProgramacionAlarma.diaria, label: Text('Diaria')), ButtonSegment(value: TipoProgramacionAlarma.diasSemana, label: Text('Días')), ], selected: {_tipo}, onSelectionChanged: (value) => setState(() => _tipo = value.first), ), if (_tipo == TipoProgramacionAlarma.diasSemana) ...[ const SizedBox(height: 10), Wrap( spacing: 6, children: [ for (var i = DateTime.monday; i <= DateTime.sunday; i++) FilterChip( label: Text(_diaCorto(i)), selected: _diasSemana.contains(i), onSelected: (selected) => setState(() { selected ? _diasSemana.add(i) : _diasSemana.remove(i); }), ), ], ), ], const SizedBox(height: 14), _SectionLabel(icon: 'assets/icons/alarmas/snooze_wave.png', text: 'Postponer'), SegmentedButton( segments: const [ ButtonSegment(value: 3, label: Text('3 min')), ButtonSegment(value: 5, label: Text('5 min')), ButtonSegment(value: 10, label: Text('10 min')), ], selected: {_snooze}, onSelectionChanged: (value) => setState(() => _snooze = value.first), ), const SizedBox(height: 14), _SectionLabel(icon: 'assets/icons/alarmas/fallback_sound.png', text: 'Sonido y volumen'), Slider( value: _volumen, min: 0.25, max: 1, divisions: 15, label: '${(_volumen * 100).round()}%', onChanged: (value) => setState(() => _volumen = value), ), DropdownButtonFormField( initialValue: _sonidoInterno, decoration: const InputDecoration(labelText: 'Sonido seguro interno'), items: const [ DropdownMenuItem(value: SonidoInternoAlarma.amanecer, child: Text('Amanecer cálido')), DropdownMenuItem(value: SonidoInternoAlarma.campanaSuave, child: Text('Campana suave')), DropdownMenuItem(value: SonidoInternoAlarma.pulsoDigital, child: Text('Pulso digital')), ], onChanged: (value) => setState(() => _sonidoInterno = value ?? _sonidoInterno), ), const SizedBox(height: 8), DropdownButtonFormField( key: ValueKey(_emisora?.uuid ?? 'sin-emisora'), initialValue: _emisora?.uuid, decoration: const InputDecoration( labelText: 'Emisora favorita', prefixIcon: Icon(Icons.radio_rounded), ), items: [ const DropdownMenuItem( value: '', child: Text('Sin emisora: usar sonido interno'), ), for (final emisora in favoritas) DropdownMenuItem( value: emisora.uuid, child: Text( emisora.nombre, overflow: TextOverflow.ellipsis, ), ), ], onChanged: (uuid) => setState(() { if (uuid == null || uuid.isEmpty) { _emisora = null; return; } _emisora = favoritas.firstWhere((e) => e.uuid == uuid); }), ), if (favoritas.isEmpty) ...[ const SizedBox(height: 6), const Text( 'Guardá emisoras en Favoritos para usarlas como alarma musical.', ), ], if (radio.emisoraActual != null) ...[ const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: FilledButton.tonalIcon( onPressed: () => setState(() => _emisora = radio.emisoraActual), icon: const Icon(Icons.add_task_rounded), label: const Text('Usar emisora actual'), ), ), ], const SizedBox(height: 8), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, value: _sonarEnVacaciones, onChanged: (value) => setState(() => _sonarEnVacaciones = value), secondary: const _AssetIcon('assets/icons/alarmas/vacation_wave.png', size: 42), title: const Text('Sonar durante vacaciones'), subtitle: const Text('Si lo apagás, la próxima ejecución saltará al primer día válido.'), ), const SizedBox(height: 16), FilledButton.icon( onPressed: _guardar, icon: const Icon(Icons.check_rounded), label: const Text('Guardar alarma'), ), ], ), ), ), ); } Future _elegirHora() async { final nueva = await showTimePicker(context: context, initialTime: _hora); if (nueva != null) setState(() => _hora = nueva); } Future _elegirFecha() async { final ahora = DateTime.now(); final nueva = await showDatePicker( context: context, initialDate: _fecha.isBefore(ahora) ? ahora : _fecha, firstDate: DateTime(ahora.year, ahora.month, ahora.day), lastDate: ahora.add(const Duration(days: 730)), ); if (nueva != null) setState(() => _fecha = nueva); } Future _guardar() async { if (_tipo == TipoProgramacionAlarma.diasSemana && _diasSemana.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Elegí al menos un día de la semana.')), ); return; } final estado = context.read(); final existente = widget.alarma; final alarma = (existente ?? estado.servicio.crearAlarma( nombre: _nombreController.text.trim(), hora: _hora.hour, minuto: _hora.minute, tipoProgramacion: _tipo, diasSemana: _diasSemana.toList()..sort(), )) .copyWith( nombre: _nombreController.text.trim().isEmpty ? 'Despertador musical' : _nombreController.text.trim(), hora: _hora.hour, minuto: _hora.minute, tipoProgramacion: _tipo, diasSemana: _tipo == TipoProgramacionAlarma.diasSemana ? (_diasSemana.toList()..sort()) : const [], fechaUnica: _tipo == TipoProgramacionAlarma.unica ? _fecha : null, limpiarFechaUnica: _tipo != TipoProgramacionAlarma.unica, emisora: _emisora, sonarEnVacaciones: _sonarEnVacaciones, snoozeMinutos: _snooze, volumen: _volumen, sonidoInterno: _sonidoInterno, activa: true, ); await estado.guardarAlarma(alarma); if (mounted) Navigator.pop(context); } List _favoritasConSeleccion(List favoritas) { final mapa = {}; for (final emisora in favoritas) { mapa[emisora.uuid] = emisora; } final seleccionada = _emisora; if (seleccionada != null) { mapa[seleccionada.uuid] = seleccionada; } return mapa.values.toList(); } } class _AccesoDiagnostico extends StatelessWidget { const _AccesoDiagnostico({required this.estado}); final EstadoAlarmas estado; @override Widget build(BuildContext context) { final diag = estado.diagnostico; return TextButton.icon( icon: const _AssetIcon('assets/icons/alarmas/android_reliability.png', size: 28), label: Text( diag == null ? 'Revisar fiabilidad Android' : 'Fiabilidad: exactas ${diag.puedeProgramarExactas ? 'OK' : 'pendiente'} · notificaciones ${diag.notificacionesPermitidas ? 'OK' : 'pendiente'}', ), onPressed: estado.cargarDiagnostico, ); } } class _PanelVacaciones extends StatelessWidget { const _PanelVacaciones({required this.estado}); final EstadoAlarmas estado; @override Widget build(BuildContext context) { final vacaciones = [...estado.vacaciones] ..sort((a, b) => a.inicioDia.compareTo(b.inicioDia)); return PluriGlassSurface( glowColor: const Color(0xFF60A5FA).withValues(alpha: 0.22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _AssetIcon('assets/icons/alarmas/vacation_wave.png', size: 48), const SizedBox(width: 10), Expanded( child: Text( 'Rangos de vacaciones', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), ), FilledButton.tonalIcon( onPressed: () => _abrirAlta(context), icon: const Icon(Icons.add_rounded), label: const Text('Agregar'), ), ], ), const SizedBox(height: 8), Text( 'Si una alarma tiene "Pausa en vacaciones", se salta automáticamente estos rangos.', ), const SizedBox(height: 10), if (vacaciones.isEmpty) const Text('Sin rangos cargados.') else for (final rango in vacaciones) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.event_busy_rounded), title: Text(rango.nombre), subtitle: Text( '${_fechaCorta(rango.inicioDia)} → ${_fechaCorta(rango.finDia)}', ), trailing: IconButton( tooltip: 'Eliminar rango', onPressed: () => estado.eliminarRangoVacaciones(rango.id), icon: const Icon(Icons.delete_outline_rounded), ), ), ], ), ); } Future _abrirAlta(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => const _EditorVacacionesSheet(), ); } } class _EditorVacacionesSheet extends StatefulWidget { const _EditorVacacionesSheet(); @override State<_EditorVacacionesSheet> createState() => _EditorVacacionesSheetState(); } class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> { late final 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)); _nombreController = TextEditingController(text: 'Vacaciones'); } @override void dispose() { _nombreController.dispose(); super.dispose(); } @override Widget build(BuildContext 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( 'Nuevo rango de vacaciones', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, ), ), const SizedBox(height: 12), TextField( controller: _nombreController, decoration: const InputDecoration(labelText: 'Nombre'), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _PickerButton( icon: Icons.play_arrow_rounded, label: 'Inicio', value: _fechaCorta(_inicio), onTap: () => _elegirFecha(esInicio: true), ), ), const SizedBox(width: 10), Expanded( child: _PickerButton( icon: Icons.stop_rounded, label: 'Fin', value: _fechaCorta(_fin), onTap: () => _elegirFecha(esInicio: false), ), ), ], ), const SizedBox(height: 16), FilledButton.icon( onPressed: _guardar, icon: const Icon(Icons.check_rounded), label: const Text('Guardar rango'), ), ], ), ), ); } 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 _AssetIcon extends StatelessWidget { const _AssetIcon(this.asset, {this.size = 44}); final String asset; final double size; @override Widget build(BuildContext context) { return Image.asset( asset, width: size, height: size, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Icon(Icons.music_note_rounded, size: size * 0.65), ); } } 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), ], ), ); } } class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.icon, required this.text}); final String icon; final String text; @override Widget build(BuildContext context) { return Row( children: [ _AssetIcon(icon, size: 34), const SizedBox(width: 8), Text(text, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800)), ], ); } } class _InfoChip extends StatelessWidget { const _InfoChip({required this.icon, required this.label}); final IconData icon; final String label; @override Widget build(BuildContext context) { return Chip(avatar: Icon(icon, size: 16), label: Text(label)); } } class _NoticeLine extends StatelessWidget { const _NoticeLine({required this.icon, required this.text}); final IconData icon; final String text; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 18), const SizedBox(width: 8), Expanded(child: Text(text)), ], ); } } class _EmptyAlarmas extends StatelessWidget { const _EmptyAlarmas(); @override Widget build(BuildContext context) { return const PluriGlassSurface( child: Column( children: [ _AssetIcon('assets/icons/alarmas/alarm_music.png', size: 92), SizedBox(height: 12), Text('Todavía no hay alarmas.'), SizedBox(height: 4), Text('Creá una para diseñar tu despertar musical.'), ], ), ); } } String _hora(AlarmaMusical alarma) => '${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}'; String _programacion(AlarmaMusical alarma) { return switch (alarma.tipoProgramacion) { TipoProgramacionAlarma.unica => 'Una vez · ${_fechaCorta(alarma.fechaUnica ?? DateTime.now())}', TipoProgramacionAlarma.diaria => 'Diaria', TipoProgramacionAlarma.diasSemana => 'Días: ${alarma.diasSemana.map(_diaCorto).join(', ')}', }; } String _fechaHora(DateTime fecha) { final local = fecha.toLocal(); return '${_diaLargo(local.weekday)} ${local.day} de ${_mes(local.month)} a las ' '${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}'; } String _fechaCorta(DateTime fecha) => '${fecha.day.toString().padLeft(2, '0')}/${fecha.month.toString().padLeft(2, '0')}/${fecha.year}'; String _diaCorto(int dia) => switch (dia) { DateTime.monday => 'Lun', DateTime.tuesday => 'Mar', DateTime.wednesday => 'Mié', DateTime.thursday => 'Jue', DateTime.friday => 'Vie', DateTime.saturday => 'Sáb', DateTime.sunday => 'Dom', _ => '?', }; String _diaLargo(int dia) => switch (dia) { DateTime.monday => 'lunes', DateTime.tuesday => 'martes', DateTime.wednesday => 'miércoles', DateTime.thursday => 'jueves', DateTime.friday => 'viernes', DateTime.saturday => 'sábado', DateTime.sunday => 'domingo', _ => 'día', }; String _mes(int mes) => switch (mes) { 1 => 'enero', 2 => 'febrero', 3 => 'marzo', 4 => 'abril', 5 => 'mayo', 6 => 'junio', 7 => 'julio', 8 => 'agosto', 9 => 'septiembre', 10 => 'octubre', 11 => 'noviembre', 12 => 'diciembre', _ => 'mes', };