Skip to main content

Overview

The availability system allows you to search for tables based on multiple criteria including zone, date, time, and party size. The system automatically finds the best available table matching your requirements.

Search Criteria

Zone Selection

Zones represent different dining areas in the restaurant. Each zone has distinct characteristics:

Terraza

Outdoor terrace dining area

Salón

Main indoor dining room

Jardín

Garden seating area

Bar

Bar area seating

VIP

VIP/private dining area
// Zone selection implementation
Widget _buildSelectorZona() {
  return BlocBuilder<DisponibilidadCubit, DisponibilidadState>(
    builder: (context, state) {
      // Get all zones configured for the restaurant
      Set<String> zonas = {};
      if (state is DisponibilidadExitosa) {
        // Get ALL zones registered in the business
        if (state.negocio != null && state.negocio!.zonas.isNotEmpty) {
          zonas = state.negocio!.zonas.toSet();
        } else {
          // Fallback: get zones from available tables
          zonas = state.mesasDisponibles.map((m) => m.zona).toSet();
        }
      }

      return Card(
        child: DropdownButton<String>(
          value: _zonaSeleccionada,
          hint: const Text('Seleccionar zona'),
          items: zonas.map((zona) {
            return DropdownMenuItem<String>(
              value: zona,
              child: Row(
                children: [
                  Icon(_obtenerIconoZona(zona), color: _obtenerColorZona(zona)),
                  SizedBox(width: 8),
                  Text(zona),
                ],
              ),
            );
          }).toList(),
          onChanged: (zona) {
            setState(() => _zonaSeleccionada = zona);
          },
        ),
      );
    },
  );
}

IconData _obtenerIconoZona(String zona) {
  final z = zona.toLowerCase();
  if (z.contains('terraza')) return Icons.deck;
  if (z.contains('salon') || z.contains('salón')) return Icons.chair;
  if (z.contains('jardin') || z.contains('jardín')) return Icons.grass;
  if (z.contains('bar')) return Icons.local_bar;
  if (z.contains('vip')) return Icons.star;
  return Icons.table_restaurant;
}

Color _obtenerColorZona(String zona) {
  final z = zona.toLowerCase();
  if (z.contains('terraza')) return const Color(0xFFE67E22);
  if (z.contains('salon') || z.contains('salón')) return const Color(0xFF3498DB);
  if (z.contains('jardin') || z.contains('jardín')) return const Color(0xFF27AE60);
  if (z.contains('bar')) return const Color(0xFF9B59B6);
  if (z.contains('vip')) return const Color(0xFFF1C40F);
  return Colors.grey;
}
The system loads all zones configured for the restaurant, even if they don’t currently have tables assigned. This ensures customers see all available dining areas.

Date Selection

Customers can select dates up to 14 days in advance:
Widget _buildSelectorFecha() {
  return Card(
    child: ListTile(
      leading: const Icon(Icons.calendar_today, color: Color(0xFF3498DB)),
      title: const Text('Fecha', style: TextStyle(fontWeight: FontWeight.w600)),
      subtitle: Text(
        _fechaSeleccionada == null
            ? 'Seleccionar fecha'
            : '${_fechaSeleccionada!.day}/${_fechaSeleccionada!.month}/${_fechaSeleccionada!.year}',
      ),
      trailing: const Icon(Icons.arrow_forward_ios, size: 16),
      onTap: () async {
        final fecha = await showDatePicker(
          context: context,
          initialDate: DateTime.now(),
          firstDate: DateTime.now(),           // Can't book in the past
          lastDate: DateTime.now().add(const Duration(days: 14)),  // 14 days ahead
        );
        if (fecha != null) {
          setState(() {
            _fechaSeleccionada = fecha;
            _intervaloSeleccionado = null;  // Clear time selection
            // Load available time slots for the selected date
            _intervalosFuture = context
                .read<DisponibilidadCubit>()
                .obtenerIntervalosHorarioNegocio(fecha);
          });
        }
      },
    ),
  );
}
When you select a new date, the time slot selection is automatically reset, and available time slots are reloaded based on the restaurant’s hours for that specific date.

Time Slot Selection

Time slots are generated based on:
  • Restaurant operating hours for the selected day
  • Configured interval duration (typically 60 minutes)
  • Existing reservations
Widget _buildSelectorHora() {
  // Must select date first
  if (_fechaSeleccionada == null) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Icon(Icons.access_time, size: 48, color: Colors.grey[400]),
            const SizedBox(height: 12),
            Text('Selecciona primero una fecha',
              style: TextStyle(fontSize: 16, color: Colors.grey[600])),
          ],
        ),
      ),
    );
  }

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              Icon(Icons.access_time, color: Color(0xFF3498DB)),
              SizedBox(width: 12),
              Text('Horarios Disponibles',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            ],
          ),
          BlocBuilder<DisponibilidadCubit, DisponibilidadState>(
            builder: (context, state) {
              int duracion = 60;
              if (state is DisponibilidadExitosa) {
                duracion = state.duracionPromedioMinutos;
              } else if (state is MesaEncontrada) {
                duracion = state.duracionPromedioMinutos;
              }
              return Text(
                'Selecciona un horario (intervalos de $duracion minutos)',
                style: TextStyle(fontSize: 13, color: Colors.grey[600]),
              );
            },
          ),
          SizedBox(height: 16),
          // Display available time slots
          FutureBuilder<List<String>>(
            future: _intervalosFuture,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return Center(child: CircularProgressIndicator());
              }

              if (!snapshot.hasData || snapshot.data!.isEmpty) {
                return Text('No hay horarios disponibles para esta fecha',
                  style: TextStyle(color: Colors.grey[600]));
              }

              final intervalos = snapshot.data!;
              return Wrap(
                spacing: 8,
                runSpacing: 8,
                children: intervalos.map((intervalo) {
                  final seleccionado = _intervaloSeleccionado == intervalo;
                  return ChoiceChip(
                    label: Text(intervalo),
                    selected: seleccionado,
                    onSelected: (selected) {
                      setState(() {
                        _intervaloSeleccionado = selected ? intervalo : null;
                      });
                    },
                    selectedColor: const Color(0xFF27AE60),
                    backgroundColor: Colors.grey[200],
                    labelStyle: TextStyle(
                      color: seleccionado ? Colors.white : Colors.black87,
                      fontWeight: seleccionado ? FontWeight.bold : FontWeight.normal,
                    ),
                  );
                }).toList(),
              );
            },
          ),
        ],
      ),
    ),
  );
}
// From disponibilidad_cubit.dart
/// Get available time slots for a specific date
Future<List<String>> obtenerIntervalosHorarioNegocio(DateTime fecha) async {
  try {
    final id = _negocioActual?.id ?? _negocioId;
    if (id == null) return [];
    
    return await _horarioAperturaRepo.obtenerIntervalosDisponibles(
      id,
      fecha,
      intervaloMinutos: _negocioActual?.duracionPromedioMinutos ?? 60,
    );
  } catch (e) {
    return [];
  }
}
Time slots are displayed as chips (e.g., “08:00 - 09:00”, “09:00 - 10:00”). The interval duration is configured per restaurant and typically set to 60 minutes.

Party Size

Select the number of people (1-20) using the increment/decrement buttons:
Widget _buildSelectorPersonas() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Row(
        children: [
          Icon(Icons.people, color: Color(0xFF3498DB)),
          SizedBox(width: 16),
          Text('Número de Personas:',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
          Spacer(),
          IconButton(
            icon: Icon(Icons.remove_circle_outline),
            color: Color(0xFF3498DB),
            onPressed: () {
              if (_numeroPersonas > 1) {
                setState(() => _numeroPersonas--);
              }
            },
          ),
          Container(
            padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            decoration: BoxDecoration(
              color: Color(0xFF3498DB).withOpacity(0.1),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text('$_numeroPersonas',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          ),
          IconButton(
            icon: Icon(Icons.add_circle_outline),
            color: Color(0xFF3498DB),
            onPressed: () {
              if (_numeroPersonas < 20) {
                setState(() => _numeroPersonas++);
              }
            },
          ),
        ],
      ),
    ),
  );
}

Searching for Tables

Search Button

Once all criteria are selected, click the “Buscar Mesa Disponible” button:
Widget _buildBotonBuscar(BuildContext context) {
  return SizedBox(
    height: 55,
    child: ElevatedButton.icon(
      onPressed: () {
        // Validate zone selection
        if (_zonaSeleccionada == null) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Por favor selecciona una zona'),
              backgroundColor: Colors.orange,
            ),
          );
          return;
        }

        // Validate date and time
        if (_fechaSeleccionada == null || _intervaloSeleccionado == null) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Por favor selecciona fecha y horario'),
              backgroundColor: Colors.orange,
            ),
          );
          return;
        }

        // Extract hour from interval (e.g., "08:00 - 09:00" -> 8)
        final partes = _intervaloSeleccionado!.split(' - ');
        final horaInicio = int.parse(partes[0].split(':')[0]);

        final fechaHora = DateTime(
          _fechaSeleccionada!.year,
          _fechaSeleccionada!.month,
          _fechaSeleccionada!.day,
          horaInicio,
          0,
        );

        // Search for available table in selected zone
        context.read<DisponibilidadCubit>().buscarMesaEnZona(
          zona: _zonaSeleccionada!,
          fecha: _fechaSeleccionada!,
          hora: fechaHora,
          numeroPersonas: _numeroPersonas,
        );
      },
      icon: Icon(Icons.search, size: 24),
      label: Text('Buscar Mesa Disponible',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
      style: ElevatedButton.styleFrom(
        backgroundColor: Color(0xFF3498DB),
        foregroundColor: Colors.white,
        elevation: 4,
      ),
    ),
  );
}

Search Logic

The system automatically finds the best available table:
// From disponibilidad_cubit.dart
Future<void> buscarMesaEnZona({
  required String zona,
  required DateTime fecha,
  required DateTime hora,
  required int numeroPersonas,
}) async {
  try {
    emit(DisponibilidadCargando());

    final negocioId = _negocioActual?.id ?? _negocioId ?? 'default';
    final mesa = await _mesaRepositorio.buscarMesaDisponibleEnZona(
      zona: zona,
      fecha: fecha,
      hora: hora,
      numeroPersonas: numeroPersonas,
      negocioId: negocioId,
    );

    if (mesa == null) {
      emit(DisponibilidadConError(
        'No hay mesas disponibles en $zona para $numeroPersonas personas en ese horario.\n\n'
        'Intenta con otra zona o un horario diferente.',
      ));
    } else {
      // Emit state with found table and duration
      emit(MesaEncontrada(
        mesa,
        zona,
        _negocioActual?.duracionPromedioMinutos ?? 60,
      ));
    }
  } catch (e) {
    emit(DisponibilidadConError('Error al buscar mesa: ${e.toString()}'));
  }
}
The search automatically finds a suitable table in the selected zone that:
  • Is available at the requested date and time
  • Can accommodate the specified party size
  • Doesn’t conflict with existing reservations

Search Results

Table Found

When a table is found, you’ll see a detailed card showing:
Widget _buildTarjetaMesaEncontrada(
  Mesa mesa,
  String zona,
  DateTime? fecha,
  String? intervaloSeleccionado,
  int numeroPersonas,
) {
  return Card(
    elevation: 4,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(16),
      side: BorderSide(color: _obtenerColorZona(zona), width: 2),
    ),
    child: Column(
      children: [
        // Header with zone information
        Container(
          padding: EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: _obtenerColorZona(zona).withOpacity(0.1),
          ),
          child: Row(
            children: [
              Icon(_obtenerIconoZona(zona), color: _obtenerColorZona(zona)),
              Expanded(
                child: Column(
                  children: [
                    Text('¡Mesa encontrada!',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                    Text('Zona: $zona'),
                  ],
                ),
              ),
              Icon(Icons.check_circle, color: Color(0xFF27AE60)),
            ],
          ),
        ),
        // Table details and reservation summary
        Padding(
          padding: EdgeInsets.all(20),
          child: Column(
            children: [
              // Table name and capacity
              Row(
                children: [
                  Icon(Icons.table_restaurant),
                  Text(mesa.nombre, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                ],
              ),
              Text('Capacidad: ${mesa.capacidad} personas'),
              // Reservation summary
              Container(
                padding: EdgeInsets.all(16),
                child: Column(
                  children: [
                    _buildItemResumen(Icons.calendar_today, 'Fecha',
                      fecha != null ? '${fecha.day}/${fecha.month}/${fecha.year}' : '-'),
                    _buildItemResumen(Icons.access_time, 'Horario',
                      intervaloSeleccionado ?? '-'),
                    _buildItemResumen(Icons.people, 'Personas', '$numeroPersonas'),
                  ],
                ),
              ),
              // Reserve button
              ElevatedButton.icon(
                onPressed: () {
                  _showConfirmarReservaDialog(context, mesa, fecha!,
                    intervaloSeleccionado!, numeroPersonas);
                },
                icon: Icon(Icons.check),
                label: Text('Reservar Esta Mesa'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Color(0xFF27AE60),
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

No Tables Available

If no tables match your criteria:
Widget _buildErrorCard(String message) {
  final esErrorHorario = message.toLowerCase().contains('horario') ||
      message.toLowerCase().contains('cerrado');

  return Card(
    elevation: 3,
    color: esErrorHorario ? Colors.orange.shade50 : Colors.red.shade50,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(16),
      side: BorderSide(
        color: esErrorHorario ? Colors.orange.shade300 : Colors.red.shade300,
        width: 2,
      ),
    ),
    child: Padding(
      padding: EdgeInsets.all(24.0),
      child: Column(
        children: [
          Container(
            padding: EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: esErrorHorario ? Colors.orange.shade100 : Colors.red.shade100,
              shape: BoxShape.circle,
            ),
            child: Icon(
              esErrorHorario ? Icons.access_time_filled : Icons.error_outline,
              color: esErrorHorario ? Colors.orange.shade700 : Colors.red.shade700,
              size: 40,
            ),
          ),
          SizedBox(height: 16),
          Text(
            esErrorHorario ? 'Horario No Disponible' : 'Error',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 12),
          Text(message, textAlign: TextAlign.center),
        ],
      ),
    ),
  );
}
Errors are color-coded:
  • Orange: Scheduling issues (no tables available, restaurant closed)
  • Red: System errors

Tips for Better Results

Be Flexible

Try different zones or time slots if your first choice isn’t available

Book Early

Popular time slots fill up quickly - book as far in advance as possible

Off-Peak Hours

Consider lunch or early dinner times for better availability

Check Multiple Dates

If one date is full, try adjacent dates

Build docs developers (and LLMs) love