Agrego frontend app

This commit is contained in:
Pablo
2026-08-22 19:08:19 -03:00
parent 824e092b29
commit 2d0797e627
254 changed files with 37231 additions and 0 deletions
@@ -0,0 +1,926 @@
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gimnasio_soma/core/services/whatsapp_service.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
import 'package:gimnasio_soma/core/widgets/soma_header_help.dart';
import 'package:gimnasio_soma/core/widgets/soma_toast.dart';
import 'package:gimnasio_soma/features/huerfanas/domain/entities/reserva_huerfana.dart';
import 'package:gimnasio_soma/features/huerfanas/presentation/providers/huerfanas_provider.dart';
import 'package:gimnasio_soma/features/huerfanas/presentation/widgets/bulk_notify_dialog.dart';
import 'package:gimnasio_soma/features/huerfanas/presentation/widgets/turno_picker_sheet.dart';
import 'package:gimnasio_soma/features/turnos/domain/entities/turno.dart';
const _meses = [
'', 'ene', 'feb', 'mar', 'abr', 'may', 'jun',
'jul', 'ago', 'sep', 'oct', 'nov', 'dic',
];
class HuerfanasScreen extends ConsumerWidget {
const HuerfanasScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(huerfanasProvider);
final notifier = ref.read(huerfanasProvider.notifier);
final currentEstado = notifier.currentEstado;
final isWide = MediaQuery.of(context).size.width >= 800;
final theme = Theme.of(context);
final modoSeleccion = ref.watch(huerfanasModoSeleccionProvider);
final seleccionadas = ref.watch(huerfanasSeleccionProvider);
final seleccionNotifier = ref.read(huerfanasSeleccionProvider.notifier);
// Sólo aplica a pendientes
final lista = state.valueOrNull ?? [];
final pendientes = lista
.where((r) => r.estado == EstadoHuerfana.pendiente)
.toList();
final seleccionadasValidas = seleccionadas
.where((id) => pendientes.any((r) => r.huerfanaId == id))
.toSet();
void toggleModoSeleccion() {
if (modoSeleccion) {
seleccionNotifier.limpiar();
}
ref.read(huerfanasModoSeleccionProvider.notifier).state = !modoSeleccion;
}
void abrirBulkNotify() {
final items = pendientes
.where((r) => seleccionadasValidas.contains(r.huerfanaId))
.toList();
if (items.isEmpty) return;
showDialog(
context: context,
builder: (_) => BulkNotifyDialog(seleccionadas: items),
).then((_) {
// limpiar selección al cerrar el dialog
seleccionNotifier.limpiar();
ref.read(huerfanasModoSeleccionProvider.notifier).state = false;
});
}
void abrirPickerSheet(ReservaHuerfana reserva) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => TurnoPickerSheet(
reserva: reserva,
onReubicadoExito: (Turno turno, DateTime fecha) {
if (!context.mounted) return;
final fechaStr =
'${fecha.day} ${_meses[fecha.month]} ${turno.horaInicio}';
SomaToast.show(
context,
message: 'Reubicado al $fechaStr',
type: ToastType.success,
action: reserva.telefono != null
? SnackBarAction(
label: 'Avisar por WhatsApp',
textColor: SomaColors.onPrimary,
onPressed: () => WhatsAppService.abrirChat(
telefono: reserva.telefono,
mensaje: 'Hola ${reserva.nombre}, te reasignamos al turno de '
'${turno.actividad.nombre} del $fechaStr. ¡Te esperamos!',
),
)
: null,
);
},
),
);
}
Future<void> abrirWhatsApp(ReservaHuerfana reserva) async {
final nombre = reserva.nombre;
final actividad = reserva.actividadNombre;
final d = DateTime.tryParse(reserva.fechaOriginal);
final fecha = d != null ? '${d.day} ${_meses[d.month]} ${d.year}' : reserva.fechaOriginal;
final ok = await WhatsAppService.abrirChat(
telefono: reserva.telefono,
mensaje: 'Hola $nombre, te contactamos desde el gimnasio SOMA. '
'Tu reserva de $actividad del $fecha quedó sin turno disponible. '
'Por favor, coordiná una nueva reserva cuando puedas. ¡Muchas gracias!',
);
if (!context.mounted) return;
if (!ok) {
final sinNumero = WhatsAppService.normalizarNumeroAr(reserva.telefono) == null;
SomaToast.show(
context,
message: sinNumero
? '${reserva.displayName} no tiene número de teléfono registrado. '
'Podés agregarlo desde la pantalla de Usuarios.'
: 'No se pudo abrir WhatsApp.',
type: ToastType.error,
);
return;
}
// Sólo ofrecer marcar si está pendiente
if (reserva.estado == EstadoHuerfana.pendiente) {
SomaToast.show(
context,
message: 'WhatsApp abierto',
type: ToastType.info,
action: SnackBarAction(
label: 'Marcar resuelta',
textColor: SomaColors.onPrimary,
onPressed: () async {
final error = await ref
.read(huerfanasProvider.notifier)
.resolver(reserva.huerfanaId, 'resuelta');
if (!context.mounted) return;
if (error != null) {
SomaToast.show(context, message: error, type: ToastType.error);
}
},
),
);
}
}
return Scaffold(
body: Column(
children: [
// ── Header ──────────────────────────────────────────────────────────
Padding(
padding: EdgeInsets.fromLTRB(
isWide ? 32 : 16,
isWide ? 28 : 16,
isWide ? 32 : 16,
0,
),
child: Row(
children: [
const Text(
'Reservas sin turno',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
),
const SomaHeaderHelp(
items: [
SomaHelpItem(
icon: Icons.filter_alt_outlined,
text: 'Filtrá por estado: pendientes, reubicadas, '
'resueltas o todas.',
),
SomaHelpItem(
icon: Icons.checklist_outlined,
text: 'Modo selección: elegí varias reservas para '
'notificarlas por WhatsApp de una sola vez.',
),
SomaHelpItem(
icon: Icons.refresh,
text: 'Recarga la lista de reservas sin turno.',
),
],
),
const Spacer(),
// Toggle selección múltiple
IconButton(
icon: Icon(
modoSeleccion
? Icons.checklist_rounded
: Icons.checklist_outlined,
size: 20,
color: modoSeleccion
? SomaColors.primary
: null,
),
tooltip: modoSeleccion ? 'Cancelar selección' : 'Seleccionar',
onPressed: toggleModoSeleccion,
),
IconButton(
icon: const Icon(Icons.refresh, size: 20),
tooltip: 'Recargar',
onPressed: () {
seleccionNotifier.limpiar();
ref.read(huerfanasModoSeleccionProvider.notifier).state =
false;
notifier.load();
},
),
],
),
),
// ── Filter chips (ocultos en modo selección) ─────────────────────
if (!modoSeleccion)
Padding(
padding: EdgeInsets.fromLTRB(
isWide ? 32 : 16, 12, isWide ? 32 : 16, 0,
),
child: Row(
children: [
_FilterChip(
label: 'Pendientes',
selected: currentEstado == 'pendiente',
color: SomaColors.error,
onTap: () => notifier.filtrar('pendiente'),
),
const SizedBox(width: 8),
_FilterChip(
label: 'Reubicadas',
selected: currentEstado == 'reubicado',
color: SomaColors.primary,
onTap: () => notifier.filtrar('reubicado'),
),
const SizedBox(width: 8),
_FilterChip(
label: 'Resueltas',
selected: currentEstado == 'resuelta',
color: theme.colorScheme.secondary,
onTap: () => notifier.filtrar('resuelta'),
),
const SizedBox(width: 8),
_FilterChip(
label: 'Todas',
selected: currentEstado == null,
color: theme.colorScheme.onSurface,
onTap: () => notifier.filtrar(null),
),
],
),
)
else
// Etiqueta modo selección
Padding(
padding: EdgeInsets.fromLTRB(
isWide ? 32 : 16, 12, isWide ? 32 : 16, 0,
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 14,
color: theme.colorScheme.onSurface.withAlpha(120),
),
const SizedBox(width: 6),
Text(
'Seleccioná los clientes pendientes que querés notificar en lote',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface.withAlpha(140),
),
),
],
),
),
const SizedBox(height: 8),
// ── Content ──────────────────────────────────────────────────────
Expanded(
child: state.when(
loading: () => const Center(
child: CircularProgressIndicator(color: SomaColors.primary),
),
error: (e, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline,
size: 48,
color: theme.colorScheme.onSurface.withAlpha(100)),
const SizedBox(height: 12),
Text(
e.toString().replaceFirst('Exception: ', ''),
textAlign: TextAlign.center,
style: TextStyle(
color: theme.colorScheme.onSurface.withAlpha(153),
),
),
const SizedBox(height: 16),
TextButton.icon(
onPressed: () => notifier.load(),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Reintentar'),
),
],
),
),
data: (lista) {
// En modo selección sólo mostramos pendientes
final listaFiltrada =
modoSeleccion ? pendientes : lista;
if (listaFiltrada.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle_outline,
size: 56,
color:
theme.colorScheme.onSurface.withAlpha(60)),
const SizedBox(height: 12),
Text(
modoSeleccion
? 'Sin reservas pendientes para notificar'
: currentEstado == 'pendiente'
? 'Sin reservas pendientes'
: 'Sin reservas en este estado',
style: TextStyle(
fontSize: 15,
color: theme.colorScheme.onSurface.withAlpha(130),
),
),
],
),
);
}
return ListView.separated(
padding: EdgeInsets.fromLTRB(
isWide ? 32 : 16, 8, isWide ? 32 : 16, 32,
),
itemCount: listaFiltrada.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final reserva = listaFiltrada[index];
return _HuerfanaCard(
reserva: reserva,
modoSeleccion: modoSeleccion,
seleccionada: seleccionadasValidas
.contains(reserva.huerfanaId),
onToggleSeleccion: () =>
seleccionNotifier.toggle(reserva.huerfanaId),
onWhatsApp:
() => abrirWhatsApp(reserva),
onReubicar: () => abrirPickerSheet(reserva),
onResolver: (nuevoEstado) async {
final error = await ref
.read(huerfanasProvider.notifier)
.resolver(reserva.huerfanaId, nuevoEstado);
if (!context.mounted) return;
if (error != null) {
SomaToast.show(context,
message: error, type: ToastType.error);
} else {
SomaToast.show(context,
message: 'Estado actualizado',
type: ToastType.success);
}
},
);
},
);
},
),
),
// ── Bottom bar de selección ──────────────────────────────────────
if (modoSeleccion)
_SelectionBar(
count: seleccionadasValidas.length,
onNotificar: seleccionadasValidas.isEmpty ? null : abrirBulkNotify,
onCancelar: toggleModoSeleccion,
),
],
),
);
}
}
// ── Filter chip ───────────────────────────────────────────────────────────────
class _FilterChip extends StatelessWidget {
final String label;
final bool selected;
final Color color;
final VoidCallback onTap;
const _FilterChip({
required this.label,
required this.selected,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
mouseCursor: SystemMouseCursors.click,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: selected ? color.withAlpha(22) : Colors.transparent,
border: Border.all(
color: selected ? color.withAlpha(100) : color.withAlpha(40),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
color: selected ? color : color.withAlpha(150),
),
),
),
);
}
}
// ── Card de reserva huérfana ──────────────────────────────────────────────────
class _HuerfanaCard extends StatelessWidget {
final ReservaHuerfana reserva;
final bool modoSeleccion;
final bool seleccionada;
final VoidCallback onToggleSeleccion;
final Future<void> Function() onWhatsApp;
final VoidCallback onReubicar;
final Future<void> Function(String nuevoEstado) onResolver;
const _HuerfanaCard({
required this.reserva,
required this.modoSeleccion,
required this.seleccionada,
required this.onToggleSeleccion,
required this.onWhatsApp,
required this.onReubicar,
required this.onResolver,
});
String _fmtFecha(String fechaIso) {
final d = DateTime.tryParse(fechaIso);
if (d == null) return fechaIso;
return '${d.day} ${_meses[d.month]} ${d.year}';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isPendiente = reserva.estado == EstadoHuerfana.pendiente;
return GestureDetector(
onTap: modoSeleccion ? onToggleSeleccion : null,
onSecondaryTapDown: modoSeleccion
? null
: (details) {
final entries = <ContextMenuEntry<String>>[];
if (isPendiente) {
entries.add(MenuItem(
label: const Text('Reubicar'),
icon: const Icon(Icons.swap_horiz, size: 16),
value: 'reubicar',
));
if (reserva.telefono != null) {
entries.add(MenuItem(
label: const Text('Notificar por WhatsApp'),
icon: const Icon(Icons.chat_outlined,
size: 16, color: Color(0xFF25D366)),
value: 'whatsapp',
));
}
entries.add(MenuItem(
label: const Text('Marcar como resuelta'),
icon: const Icon(Icons.notifications_none, size: 16),
value: 'resuelta',
));
} else {
if (reserva.telefono != null) {
entries.add(MenuItem(
label: const Text('Notificar por WhatsApp'),
icon: const Icon(Icons.chat_outlined,
size: 16, color: Color(0xFF25D366)),
value: 'whatsapp',
));
}
entries.add(MenuItem(
label: const Text('Volver a pendiente'),
icon: const Icon(Icons.undo, size: 16),
value: 'pendiente',
));
}
showContextMenu<String>(
context,
contextMenu: ContextMenu<String>(
position: details.globalPosition,
entries: entries,
),
onItemSelected: (v) {
switch (v) {
case 'reubicar':
onReubicar();
case 'whatsapp':
onWhatsApp();
case 'resuelta':
onResolver('resuelta');
case 'pendiente':
onResolver('pendiente');
}
},
);
},
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.surface,
border: Border.all(
color: modoSeleccion && seleccionada
? SomaColors.primary.withAlpha(140)
: theme.colorScheme.surfaceContainerHighest,
width: modoSeleccion && seleccionada ? 1.5 : 0.5,
),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Checkbox en modo selección / rail de estado normal
if (modoSeleccion)
_SelectionRail(seleccionada: seleccionada)
else
Container(
width: 4,
color: switch (reserva.estado) {
EstadoHuerfana.pendiente => SomaColors.error.withAlpha(180),
EstadoHuerfana.reubicado => SomaColors.primary.withAlpha(180),
EstadoHuerfana.resuelta =>
theme.colorScheme.secondary.withAlpha(180),
},
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 12, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row superior: avatar + info + badge
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 18,
backgroundColor: SomaColors.primary.withAlpha(30),
child: Text(
reserva.initials,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: SomaColors.primaryText,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
reserva.displayName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (reserva.telefono != null)
Text(
reserva.telefono!,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface
.withAlpha(130),
),
),
],
),
),
if (!modoSeleccion) _EstadoBadge(estado: reserva.estado),
],
),
const SizedBox(height: 10),
// Actividad + fecha
Row(
children: [
Icon(Icons.sports_gymnastics_outlined,
size: 14,
color: theme.colorScheme.onSurface.withAlpha(120)),
const SizedBox(width: 5),
Expanded(
child: Text(
reserva.actividadNombre,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface.withAlpha(180),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Icon(Icons.calendar_today_outlined,
size: 13,
color: theme.colorScheme.onSurface.withAlpha(100)),
const SizedBox(width: 4),
Text(
'${_fmtFecha(reserva.fechaOriginal)} ${reserva.horaInicioOriginal}',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface.withAlpha(150),
fontFeatures: const [
FontFeature.tabularFigures()
],
),
),
],
),
// Acciones (sólo cuando no estamos en modo selección)
if (!modoSeleccion) ...[
if (isPendiente) ...[
const SizedBox(height: 10),
Row(
children: [
_ActionButton(
icon: Icons.chat_outlined,
label: 'WhatsApp',
color: const Color(0xFF25D366),
onTap: onWhatsApp,
),
const SizedBox(width: 8),
_ActionButton(
icon: Icons.swap_horiz,
label: 'Reubicar',
color: SomaColors.primary,
onTap: () => onReubicar(),
),
const SizedBox(width: 8),
_ActionButton(
icon: Icons.notifications_none,
label: 'Resuelta',
color: theme.colorScheme.secondary,
onTap: () => onResolver('resuelta'),
),
],
),
] else ...[
const SizedBox(height: 6),
Row(
children: [
_ActionButton(
icon: Icons.chat_outlined,
label: 'WhatsApp',
color: const Color(0xFF25D366),
onTap: onWhatsApp,
),
const SizedBox(width: 8),
InkWell(
onTap: () => onResolver('pendiente'),
borderRadius: BorderRadius.circular(6),
mouseCursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Text(
'Volver a pendiente',
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurface
.withAlpha(120),
decoration: TextDecoration.underline,
decorationColor:
theme.colorScheme.onSurface
.withAlpha(80),
),
),
),
),
],
),
],
],
],
),
),
),
],
),
),
),
);
}
}
// ── Rail de selección ─────────────────────────────────────────────────────────
class _SelectionRail extends StatelessWidget {
final bool seleccionada;
const _SelectionRail({required this.seleccionada});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 40,
color: seleccionada
? SomaColors.primary.withAlpha(20)
: Colors.transparent,
child: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 18,
height: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: seleccionada ? SomaColors.primary : Colors.transparent,
border: Border.all(
color: seleccionada
? SomaColors.primary
: Theme.of(context).colorScheme.onSurface.withAlpha(80),
width: 1.5,
),
),
child: seleccionada
? const Icon(Icons.check, size: 12, color: SomaColors.onPrimary)
: null,
),
),
);
}
}
// ── Badge de estado ───────────────────────────────────────────────────────────
class _EstadoBadge extends StatelessWidget {
final EstadoHuerfana estado;
const _EstadoBadge({required this.estado});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final (label, color) = switch (estado) {
EstadoHuerfana.pendiente => ('Pendiente', SomaColors.error),
EstadoHuerfana.reubicado => ('Reubicado', SomaColors.primary),
EstadoHuerfana.resuelta =>
('Resuelta', theme.colorScheme.secondary),
};
final textColor = estado == EstadoHuerfana.reubicado
? SomaColors.primaryText
: color;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: color.withAlpha(18),
borderRadius: BorderRadius.circular(5),
border: Border.all(color: color.withAlpha(60), width: 0.5),
),
child: Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: textColor,
),
),
);
}
}
// ── Botón de acción ───────────────────────────────────────────────────────────
class _ActionButton extends StatefulWidget {
final IconData icon;
final String label;
final Color color;
final dynamic Function() onTap;
const _ActionButton({
required this.icon,
required this.label,
required this.color,
required this.onTap,
});
@override
State<_ActionButton> createState() => _ActionButtonState();
}
class _ActionButtonState extends State<_ActionButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: () => widget.onTap(),
child: AnimatedContainer(
duration: const Duration(milliseconds: 120),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
color: widget.color.withAlpha(_hovered ? 38 : 16),
border: Border.all(
color: widget.color.withAlpha(_hovered ? 110 : 60),
width: _hovered ? 0.8 : 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 13, color: widget.color),
const SizedBox(width: 5),
Text(
widget.label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: widget.color,
),
),
],
),
),
),
);
}
}
// ── Bottom bar de selección ───────────────────────────────────────────────────
class _SelectionBar extends StatelessWidget {
final int count;
final VoidCallback? onNotificar;
final VoidCallback onCancelar;
const _SelectionBar({
required this.count,
required this.onNotificar,
required this.onCancelar,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 14),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border(
top: BorderSide(
color: theme.colorScheme.surfaceContainerHighest,
width: 1,
),
),
),
child: Row(
children: [
Text(
count == 0
? 'Sin selección'
: count == 1
? '1 seleccionado'
: '$count seleccionados',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface.withAlpha(160),
),
),
const Spacer(),
TextButton(
onPressed: onCancelar,
child: const Text('Cancelar'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(minimumSize: const Size(0, 38)),
onPressed: onNotificar,
icon: const Icon(Icons.chat_outlined, size: 16),
label: Text(
count == 0
? 'Notificar en lote'
: 'Notificar $count por WhatsApp',
),
),
],
),
);
}
}