Overview
The “Mis Reservas” (My Reservations) screen allows customers to view all their reservations for a specific restaurant after verifying their phone number via SMS.Accessing Your Reservations
Initial Verification Screen
When you first access “Mis Reservas”, you’ll see a verification prompt:Widget _buildVerificacionInicial() {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
child: Column(
children: [
// Icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Color(0xFF27AE60).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.event_note, size: 50, color: Color(0xFF27AE60)),
),
SizedBox(height: 32),
// Title
Text('¿Querés ver tus reservas?',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
SizedBox(height: 12),
// Subtitle
Text(
'Verificá tu identidad con SMS para ver las reservas asociadas a tu número de teléfono.',
style: TextStyle(fontSize: 16, color: Color(0xFF7F8C8D)),
textAlign: TextAlign.center,
),
SizedBox(height: 40),
// Main button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _mostrarDialogoTelefono(context),
icon: Icon(Icons.search, size: 22),
label: Text('Buscar Mis Reservas', style: TextStyle(fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF27AE60),
padding: EdgeInsets.symmetric(vertical: 16),
),
),
),
SizedBox(height: 24),
// Link to make new reservation
TextButton.icon(
onPressed: () => context.push('/disponibilidad', extra: widget.negocioId),
icon: Icon(Icons.add_circle_outline, color: Color(0xFF3498DB)),
label: Text('Hacer una nueva reserva',
style: TextStyle(color: Color(0xFF3498DB), fontSize: 16)),
),
],
),
),
);
}
Phone Number Verification
Click “Buscar Mis Reservas” to enter your phone number:void _mostrarDialogoTelefono(BuildContext context) {
final telefonoController = TextEditingController();
String? errorTelefono;
final servicioVerificacion = getIt<ServicioVerificacionCliente>();
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Row(
children: [
Icon(Icons.phone_android, color: Color(0xFF27AE60)),
SizedBox(width: 8),
Text('Verificar tu teléfono'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Ingresá tu número de teléfono para verificar tu identidad y ver tus reservas.',
style: TextStyle(color: Color(0xFF7F8C8D)),
),
SizedBox(height: 20),
TextField(
controller: telefonoController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'Teléfono',
hintText: 'Ej: 2614567890',
prefixIcon: Icon(Icons.phone),
prefixText: '+54 ',
border: OutlineInputBorder(),
errorText: errorTelefono,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Cancelar'),
),
ElevatedButton(
onPressed: () {
final telefono = telefonoController.text.trim();
final validacion = servicioVerificacion.validarTelefono(telefono);
if (!validacion['valido']) {
setDialogState(() {
errorTelefono = validacion['error'];
});
return;
}
Navigator.of(dialogContext).pop();
_mostrarVerificacionSMS(context, validacion['formateado']);
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF27AE60),
),
child: Text('Enviar SMS'),
),
],
),
),
);
}
Phone numbers are automatically normalized to E.164 format (+54 for Argentina). The system handles various input formats.
SMS Verification Dialog
After entering your phone number, you’ll receive an SMS code. See SMS Verification for details.Loading Reservations
Once verified, the system loads your reservations:// From mis_reservas_cubit.dart
Future<void> cargarReservasFiltradas({
required String telefono,
required String negocioId,
}) async {
try {
emit(MisReservasCargando());
_telefonoVerificado = telefono;
_negocioId = negocioId;
final reservas = await _reservaRepositorio.obtenerReservasPorTelefonoYNegocio(
telefonoCliente: telefono,
negocioId: negocioId,
);
emit(MisReservasExitoso(reservas));
} catch (e) {
emit(MisReservasConError('Error al cargar las reservas: ${e.toString()}'));
}
}
void _onVerificacionExitosa(String telefono) {
final negocioId = widget.negocioId ?? 'default';
setState(() {
_verificado = true;
});
// Load reservations filtered by phone and restaurant
context.read<MisReservasCubit>().cargarReservasFiltradas(
telefono: telefono,
negocioId: negocioId,
);
}
Reservations are filtered by:
- Your verified phone number
- The specific restaurant (negocioId)
Viewing Reservations
Reservation List
Your reservations are displayed as cards:Widget _buildReservasView() {
return BlocConsumer<MisReservasCubit, MisReservasState>(
listener: (context, state) {
if (state is ReservaCancelada) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.mensaje),
backgroundColor: Colors.green,
),
);
} else if (state is ReservaCancelacionError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.mensaje),
backgroundColor: Color(0xFFE74C3C),
),
);
}
},
builder: (context, state) {
if (state is MisReservasCargando) {
return Center(child: CircularProgressIndicator());
}
if (state is MisReservasExitoso) {
if (state.reservas.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
padding: EdgeInsets.all(16.0),
itemCount: state.reservas.length,
itemBuilder: (context, index) {
return _buildReservaCard(context, state.reservas[index]);
},
);
}
return SizedBox.shrink();
},
);
}
Empty State
If you have no reservations:Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.event_busy, size: 80,
color: Color(0xFF27AE60).withValues(alpha: 0.5)),
SizedBox(height: 24),
Text('No tenés reservas',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
SizedBox(height: 12),
Text(
'No se encontraron reservas asociadas a tu número de teléfono en este restaurante.',
style: TextStyle(fontSize: 16, color: Color(0xFF7F8C8D)),
textAlign: TextAlign.center,
),
SizedBox(height: 32),
ElevatedButton(
onPressed: () => context.push('/disponibilidad', extra: widget.negocioId),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF27AE60),
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: Text('Hacer una Reserva'),
),
],
),
);
}
Reservation Card
Each reservation shows:Widget _buildReservaCard(BuildContext context, Reserva reserva) {
final dateFormat = DateFormat('dd/MM/yyyy');
final timeFormat = DateFormat('HH:mm');
return Card(
elevation: 3,
margin: EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with ID and status badge
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Reserva #${reserva.id.substring(0, 8)}',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
BadgeEstado(
texto: getEstadoTexto(reserva.estado),
color: getEstadoColor(reserva.estado),
icon: getEstadoIcono(reserva.estado),
),
],
),
Divider(height: 24),
// Reservation details
_buildInfoRow(Icons.calendar_today, 'Fecha',
dateFormat.format(reserva.fechaHora)),
SizedBox(height: 12),
_buildInfoRow(Icons.access_time, 'Hora',
timeFormat.format(reserva.fechaHora)),
SizedBox(height: 12),
_buildInfoRow(Icons.table_restaurant, 'Mesa',
_NombreMesa(mesaId: reserva.mesaId)),
SizedBox(height: 12),
_buildInfoRow(Icons.people, 'Personas',
'${reserva.numeroPersonas}'),
// Action buttons (if not finalized)
if (!reserva.fechaHora.isBefore(DateTime.now()) &&
reserva.estado != EstadoReserva.cancelada) ..[
SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _showDetalleDialog(context, reserva),
icon: Icon(Icons.info_outline),
label: Text('Detalles'),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _showCancelarDialog(context, reserva.id),
icon: Icon(Icons.cancel),
label: Text('Cancelar'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFE74C3C),
),
),
),
],
),
],
// Finalized label
if (reserva.fechaHora.isBefore(DateTime.now()) &&
reserva.estado != EstadoReserva.cancelada) ..[
SizedBox(height: 16),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle_outline, color: Colors.grey[600]),
SizedBox(width: 8),
Text('Reserva Finalizada',
style: TextStyle(color: Colors.grey[600], fontWeight: FontWeight.bold)),
],
),
),
],
],
),
),
);
}
Reservation Status
Reservations can have three states:Confirmada
Reservation is confirmed and active
Pendiente
Awaiting confirmation (rarely used)
Cancelada
Reservation has been cancelled
Color getEstadoColor(EstadoReserva estado) {
switch (estado) {
case EstadoReserva.confirmada:
return const Color(0xFF27AE60);
case EstadoReserva.pendiente:
return const Color(0xFFF39C12);
case EstadoReserva.cancelada:
return const Color(0xFFE74C3C);
}
}
String getEstadoTexto(EstadoReserva estado) {
switch (estado) {
case EstadoReserva.confirmada:
return 'Confirmada';
case EstadoReserva.pendiente:
return 'Pendiente';
case EstadoReserva.cancelada:
return 'Cancelada';
}
}
IconData getEstadoIcono(EstadoReserva estado) {
switch (estado) {
case EstadoReserva.confirmada:
return Icons.check_circle;
case EstadoReserva.pendiente:
return Icons.schedule;
case EstadoReserva.cancelada:
return Icons.cancel;
}
}
Viewing Reservation Details
Click “Detalles” to see full reservation information:void _showDetalleDialog(BuildContext context, Reserva reserva) {
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text('Detalles de la Reserva'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${reserva.id}'),
SizedBox(height: 8),
Text('Fecha y Hora: ${dateFormat.format(reserva.fechaHora)}'),
SizedBox(height: 8),
Row(
children: [
Text('Mesa: '),
_NombreMesa(mesaId: reserva.mesaId),
],
),
SizedBox(height: 8),
Text('Número de Personas: ${reserva.numeroPersonas}'),
SizedBox(height: 8),
Text('Cliente: ${reserva.nombreCliente ?? reserva.contactoCliente ?? "Sin datos"}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Cerrar'),
),
],
),
);
}
Canceling Reservations
Cancellation Dialog
Click “Cancelar” to cancel a reservation:void _showCancelarDialog(BuildContext context, String reservaId) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text('Cancelar Reserva'),
content: Text(
'¿Estás seguro de que deseas cancelar esta reserva?\n\n'
'Esta acción no se puede deshacer.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('No, mantener'),
),
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
context.read<MisReservasCubit>().cancelarReserva(reservaId);
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFE74C3C),
),
child: Text('Sí, cancelar'),
),
],
),
);
}
Cancellation Logic
// From mis_reservas_cubit.dart
Future<void> cancelarReserva(String reservaId) async {
try {
emit(MisReservasCargando());
final idNegocio = _negocioId ?? 'default';
await _cancelarReserva.ejecutar(reservaId, negocioId: idNegocio);
emit(ReservaCancelada('Reserva cancelada exitosamente'));
// Reload filtered reservations
await recargarReservas();
} catch (e) {
emit(ReservaCancelacionError('Error al cancelar: ${e.toString().replaceAll("Exception: ", "")}'));
try { await recargarReservas(); } catch (_) {}
}
}
Future<void> recargarReservas() async {
if (_telefonoVerificado != null && _negocioId != null) {
await cargarReservasFiltradas(
telefono: _telefonoVerificado!,
negocioId: _negocioId!,
);
}
}
After cancellation, the reservation list is automatically reloaded to show the updated status.
Reservation States
Active Reservations
Reservations that haven’t occurred yet and aren’t cancelled show:- Detalles button to view full information
- Cancelar button to cancel the reservation
Finalized Reservations
Reservations in the past (but not cancelled) show:- “Reserva Finalizada” label
- No action buttons
Cancelled Reservations
Cancelled reservations show:- Red “Cancelada” badge
- No action buttons
Table Name Resolution
Table names are fetched asynchronously:class _NombreMesa extends StatelessWidget {
final String mesaId;
const _NombreMesa({required this.mesaId});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: getIt<MesaRepositorio>().obtenerMesaPorId(mesaId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return SizedBox();
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
return Text(mesaId,
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600));
}
final mesa = snapshot.data!;
return Text(
mesa.nombre.isNotEmpty ? mesa.nombre : mesaId,
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
);
},
);
}
}
If the table name can’t be loaded, the system falls back to displaying the table ID.
Error Handling
Loading Error
Loading Error
If reservations can’t be loaded:
if (state is MisReservasConError) {
return Center(
child: Column(
children: [
Icon(Icons.error_outline, color: Color(0xFFE74C3C), size: 64),
Text(state.mensaje),
ElevatedButton(
onPressed: () => context.read<MisReservasCubit>().recargarReservas(),
child: Text('Reintentar'),
),
],
),
);
}
Cancellation Error
Cancellation Error
If cancellation fails:
listener: (context, state) {
if (state is ReservaCancelacionError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.mensaje),
backgroundColor: Color(0xFFE74C3C),
duration: Duration(seconds: 4),
),
);
}
}