fix(vacaciones): replace the progress bar with the prototype's date pair
Item 21 / audit 9b.4-9b.5 (t4:451-462, 469-479): the active vacation range showed a LinearProgressIndicator the prototype never draws. Replace it with the screen's real signature element -- a start/end date pair (day+month, weekday, connector rule) -- reused with a flat connector for the "programados"/"pasados" rows, replacing their plain ListTiles with proper cards.
This commit is contained in:
@@ -11,3 +11,14 @@ import 'package:intl/intl.dart';
|
||||
/// this safely.
|
||||
String fechaCortaLocalizada(String localeTag, DateTime fecha) =>
|
||||
DateFormat.yMd(localeTag).format(fecha);
|
||||
|
||||
/// Day + abbreviated month, locale-aware (e.g. "28 jul" for `es`, "Jul 28"
|
||||
/// for `en`) — item 21 / audit 9b.4 (t4:459), the big digit half of the
|
||||
/// vacation screen's date-pair.
|
||||
String diaMesLocalizado(String localeTag, DateTime fecha) =>
|
||||
DateFormat.MMMd(localeTag).format(fecha);
|
||||
|
||||
/// Full weekday name, locale-aware (e.g. "lunes"/"Monday") — item 21 /
|
||||
/// audit 9b.4 (t4:459), the small caption under the day-month.
|
||||
String nombreDiaSemanaLocalizado(String localeTag, DateTime fecha) =>
|
||||
DateFormat.EEEE(localeTag).format(fecha);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -68,7 +69,8 @@ class PantallaVacaciones extends StatelessWidget {
|
||||
|
||||
/// 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
|
||||
/// 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});
|
||||
@@ -83,6 +85,7 @@ class _HeroRangoActivo extends StatelessWidget {
|
||||
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),
|
||||
@@ -96,14 +99,24 @@ class _HeroRangoActivo extends StatelessWidget {
|
||||
).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)),
|
||||
// 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: 10),
|
||||
const SizedBox(height: 12),
|
||||
Text(l10n.vacationImpactPausedLabel(_horas(impacto.pausadas))),
|
||||
],
|
||||
if (impacto.noAfectadas.isNotEmpty) ...[
|
||||
@@ -117,13 +130,6 @@ class _HeroRangoActivo extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
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<AlarmaMusical> alarmas) => alarmas
|
||||
.map(
|
||||
(a) =>
|
||||
@@ -148,13 +154,30 @@ class _SeccionProgramados extends StatelessWidget {
|
||||
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)),
|
||||
),
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -177,22 +200,197 @@ class _SeccionRangosPasados extends StatelessWidget {
|
||||
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)),
|
||||
),
|
||||
// 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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _rangoFechas(AppLocalizations l10n, RangoVacaciones rango) =>
|
||||
'${fechaCortaLocalizada(l10n.localeName, rango.inicioDia)} → '
|
||||
'${fechaCortaLocalizada(l10n.localeName, rango.finDia)}';
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user