import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; import '../../models/booking.dart'; // Grid cell dimensions const double _kSlotW = 82; const double _kCourtW = 94; const double _kRowH = 40; const double _kHeaderH = 34; class BookingScreen extends StatefulWidget { const BookingScreen({super.key}); @override State createState() => _BookingScreenState(); } class _BookingScreenState extends State { final _repo = bookingRepository(); List _activities = []; BookingActivity? _selectedActivity; DateTime _selectedDate = DateTime.now(); BookingGrid? _grid; bool _gridLoading = false; String? _gridError; ({int row, int court})? _selectedCell; BookingDetail? _bookingDetail; bool _detailLoading = false; @override void initState() { super.initState(); _loadActivities(); } Future _loadActivities() async { try { final list = await _repo.activities(); setState(() { _activities = list; if (list.isNotEmpty) { _selectedActivity = list.first; _loadGrid(); } }); } catch (_) {} } Future _loadGrid() async { final activity = _selectedActivity; if (activity == null) return; setState(() { _gridLoading = true; _gridError = null; _selectedCell = null; _bookingDetail = null; }); try { final grid = await _repo.grid( activityId: activity.id, date: _fmtDate(_selectedDate), ); setState(() { _grid = grid; _gridLoading = false; }); } catch (e) { setState(() { _gridError = e.toString(); _gridLoading = false; }); } } Future _onCellTap(int row, int court, BookingCell cell) async { setState(() { _selectedCell = (row: row, court: court); _bookingDetail = null; _detailLoading = cell.isBooked; }); if (!cell.isBooked) return; try { final detail = await _repo.singleBooking( activityId: _selectedActivity!.id, date: _fmtDate(_selectedDate), slotIndex: row, courtIndex: court, ); setState(() { _bookingDetail = detail; _detailLoading = false; }); } catch (_) { setState(() => _detailLoading = false); } } void _handleAction(String action) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$action — pendiente de implementar'), duration: const Duration(seconds: 2), ), ); } Widget _gridCard(BuildContext context) { final child = _gridLoading ? const Center(child: CircularProgressIndicator()) : _gridError != null ? Center(child: Text(_gridError!)) : _grid == null ? Center( child: Text('Selecciona una fecha y actividad', style: TextStyle( color: Theme.of(context).colorScheme.outline)), ) : LayoutBuilder(builder: (_, cst) => _GridView( grid: _grid!, availableWidth: cst.maxWidth, selectedCell: _selectedCell, onCellTap: _onCellTap, )); return Card.outlined(child: child); } Widget _selectorSideBySide() => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _CalendarCard( selectedDate: _selectedDate, onDateChanged: (d) { setState(() => _selectedDate = d); _loadGrid(); }, ), const SizedBox(width: 12), Expanded( child: _ActivityCard( activities: _activities, selected: _selectedActivity, onSelected: (a) { setState(() => _selectedActivity = a); _loadGrid(); }, ), ), ], ), ); Widget _selectorStacked() => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _ActivityDropdown( activities: _activities, selected: _selectedActivity, onSelected: (a) { setState(() => _selectedActivity = a); _loadGrid(); }, ), const SizedBox(height: 12), _CalendarCard( selectedDate: _selectedDate, onDateChanged: (d) { setState(() => _selectedDate = d); _loadGrid(); }, ), ], ), ); List _leftContents(BuildContext context, {required bool sideBySide}) => [ if (_grid?.celebrations != null) _CelebrationBanner(text: _grid!.celebrations!), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Text('Reservas', style: Theme.of(context).textTheme.headlineSmall), ), sideBySide ? _selectorSideBySide() : _selectorStacked(), const SizedBox(height: 12), ]; Widget _rightPanel() => _RightPanel( detail: _bookingDetail, detailLoading: _detailLoading, onAction: _handleAction, ); @override Widget build(BuildContext context) { return LayoutBuilder(builder: (_, constraints) { final w = constraints.maxWidth; // ── Narrow < 910px: right panel debajo del grid ─────────────────────── final windowW = MediaQuery.sizeOf(context).width; if (w < 910) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ..._leftContents(context, sideBySide: windowW >= 900), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: _gridCard(context), ), const Divider(height: 24), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: _rightPanel(), ), ], ), ); } // ── Medium/wide ≥ 910px: right panel fijo a la derecha ──────────────── return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ..._leftContents(context, sideBySide: true), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: _gridCard(context), ), ), ], ), ), VerticalDivider( width: 1, color: Theme.of(context).colorScheme.outlineVariant, ), SizedBox(width: 300, child: _rightPanel()), ], ); }); } } // ── Celebration banner ──────────────────────────────────────────────────────── class _CelebrationBanner extends StatelessWidget { final String text; const _CelebrationBanner({required this.text}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: cs.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.info_outline, size: 18, color: cs.primary), const SizedBox(width: 8), Expanded( child: Text( 'Celebraciones de hoy: $text', style: TextStyle(color: cs.onPrimaryContainer, fontSize: 13), ), ), ], ), ); } } // ── Calendar card ───────────────────────────────────────────────────────────── class _CalendarCard extends StatelessWidget { final DateTime selectedDate; final void Function(DateTime) onDateChanged; const _CalendarCard({ required this.selectedDate, required this.onDateChanged, }); @override Widget build(BuildContext context) { return Card.outlined( child: Padding( padding: const EdgeInsets.all(8), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('Fecha', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold)), ), SizedBox( width: 280, child: CalendarDatePicker( initialDate: selectedDate, firstDate: DateTime(2020), lastDate: DateTime(2030), onDateChanged: onDateChanged, ), ), ], ), ), ); } } // ── Activity card ───────────────────────────────────────────────────────────── class _ActivityCard extends StatelessWidget { final List activities; final BookingActivity? selected; final void Function(BookingActivity) onSelected; const _ActivityCard({ required this.activities, required this.selected, required this.onSelected, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Card.outlined( child: Padding( padding: const EdgeInsets.all(8), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Text('Actividad', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: cs.primary, fontWeight: FontWeight.bold)), ), const SizedBox(height: 4), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: activities.length, itemBuilder: (context, i) { final a = activities[i]; final isSelected = a.id == selected?.id; return InkWell( onTap: () => onSelected(a), borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 7), decoration: BoxDecoration( color: isSelected ? cs.primaryContainer : null, borderRadius: BorderRadius.circular(4), ), child: Text( a.name, style: TextStyle( fontSize: 13, fontWeight: isSelected ? FontWeight.bold : null, color: isSelected ? cs.onPrimaryContainer : null, ), ), ), ); }, ), ], ), ), ); } } // ── Activity dropdown (compact, for narrow screens) ─────────────────────────── class _ActivityDropdown extends StatelessWidget { final List activities; final BookingActivity? selected; final void Function(BookingActivity) onSelected; const _ActivityDropdown({ required this.activities, required this.selected, required this.onSelected, }); @override Widget build(BuildContext context) { if (activities.isEmpty) return const SizedBox.shrink(); final cs = Theme.of(context).colorScheme; return DropdownButtonFormField( initialValue: selected, decoration: InputDecoration( labelText: 'Actividad', labelStyle: TextStyle(color: cs.primary, fontWeight: FontWeight.bold), isDense: true, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), items: activities .map((a) => DropdownMenuItem(value: a, child: Text(a.name))) .toList(), onChanged: (a) { if (a != null) onSelected(a); }, ); } } // ── Grid view ───────────────────────────────────────────────────────────────── class _GridView extends StatelessWidget { final BookingGrid grid; final double availableWidth; final ({int row, int court})? selectedCell; final void Function(int row, int court, BookingCell cell) onCellTap; const _GridView({ required this.grid, required this.availableWidth, required this.selectedCell, required this.onCellTap, }); /// Compute court cell width so all courts fill [availableWidth]. /// Falls back to [_kCourtW] if available space is too narrow. double _courtWidth() { final numCourts = grid.courtTitles.length; if (numCourts == 0) return _kCourtW; final remaining = availableWidth - _kSlotW; final expanded = remaining / numCourts; return expanded >= _kCourtW ? expanded : _kCourtW; } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final courtW = _courtWidth(); final totalW = _kSlotW + courtW * grid.courtTitles.length; final needsHScroll = totalW > availableWidth; Widget content = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row Row( children: [ _headerCell('Horarios', _kSlotW, cs), ...grid.courtTitles.map( (t) => _headerCell(t, courtW, cs), ), ], ), // Data rows ...grid.rows.asMap().entries.map((e) { final rowIdx = e.key; final row = e.value; final isOdd = rowIdx.isOdd; return Row( children: [ // Time slot cell Container( width: _kSlotW, height: _kRowH, color: isOdd ? cs.surfaceContainerLow : cs.surfaceContainerLowest, alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 8), child: Text(row.timeLabel, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500)), ), // Court cells ...row.cells.asMap().entries.map((ce) { final courtIdx = ce.key + 1; // 1-based final cell = ce.value; final isSelected = selectedCell?.row == rowIdx && selectedCell?.court == courtIdx; return _GridCell( cell: cell, width: courtW, isSelected: isSelected, isOdd: isOdd, onTap: () => onCellTap(rowIdx, courtIdx, cell), ); }), ], ); }), ], ); if (needsHScroll) { content = SingleChildScrollView( scrollDirection: Axis.horizontal, child: content, ); } return SingleChildScrollView( scrollDirection: Axis.vertical, child: content, ); } Widget _headerCell(String title, double width, ColorScheme cs) { return Container( width: width, height: _kHeaderH, color: cs.surfaceContainerHigh, alignment: Alignment.center, child: Text( title, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: cs.onSurface, ), ), ); } } class _GridCell extends StatelessWidget { final BookingCell cell; final double width; final bool isSelected; final bool isOdd; final VoidCallback onTap; const _GridCell({ required this.cell, required this.width, required this.isSelected, required this.isOdd, required this.onTap, }); Color _bgColor(BuildContext context) { final cs = Theme.of(context).colorScheme; if (cell.isPaid) return Colors.green.shade400; if (cell.isBooked) return Colors.red.shade400; return isOdd ? cs.surfaceContainerLow : cs.surfaceContainerLowest; } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Container( width: width, height: _kRowH, decoration: BoxDecoration( color: _bgColor(context), border: isSelected ? Border.all(color: cs.primary, width: 2) : Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)), ), alignment: Alignment.center, child: cell.isBooked ? Text( cell.raw, style: const TextStyle( fontSize: 11, color: Colors.white, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ) : null, ), ); } } // ── Right panel ─────────────────────────────────────────────────────────────── class _RightPanel extends StatelessWidget { final BookingDetail? detail; final bool detailLoading; final void Function(String) onAction; const _RightPanel({ required this.detail, required this.detailLoading, required this.onAction, }); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _ActionButtons(onAction: onAction), const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 12), if (detailLoading) const Center( child: Padding( padding: EdgeInsets.all(24), child: CircularProgressIndicator(), )) else if (detail == null) _EmptyDetailPanel() else _TicketPanel(detail: detail!), ], ), ); } } // ── Action buttons ──────────────────────────────────────────────────────────── class _ActionButtons extends StatelessWidget { final void Function(String) onAction; const _ActionButtons({required this.onAction}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Column( children: [ // Row 1: booking actions + print actions Row( children: [ // Left group: booking ops Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ActionIcon( icon: Icons.person_add_outlined, tooltip: 'Añadir suplemento', onTap: () => onAction('supplement'), ), _ActionIcon( icon: Icons.lightbulb_outline, tooltip: 'Añadir luz', onTap: () => onAction('light'), ), _ActionIcon( icon: Icons.point_of_sale_outlined, tooltip: 'Cobrar', onTap: () => onAction('charge'), ), ], ), ), Container( width: 1, height: 36, color: cs.outlineVariant), // Right group: print/cancel Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ActionIcon( icon: Icons.print_outlined, tooltip: 'Imprimir ticket', onTap: () => onAction('print'), ), _ActionIcon( icon: Icons.undo, tooltip: 'Devolver', onTap: () => onAction('return'), color: cs.error, ), _ActionIcon( icon: Icons.qr_code_2, tooltip: 'Imprimir QR', onTap: () => onAction('qr_print'), ), ], ), ), ], ), const SizedBox(height: 8), // Row 2: QR + payment methods Row( children: [ // QR button Padding( padding: const EdgeInsets.only(left: 8), child: _ActionIcon( icon: Icons.qr_code, tooltip: 'QR', onTap: () => onAction('qr'), size: 40, ), ), const Spacer(), // Payment buttons _PayButton( label: 'Efectivo', color: Colors.green.shade600, icon: Icons.payments_outlined, onTap: () => onAction('cash'), ), const SizedBox(width: 6), _PayButton( label: 'Tarjeta', color: Colors.amber.shade700, icon: Icons.credit_card, onTap: () => onAction('card'), ), const SizedBox(width: 6), _PayButton( label: 'Bizum', color: Colors.teal.shade600, icon: Icons.phone_android, onTap: () => onAction('bizum'), ), const SizedBox(width: 8), ], ), ], ); } } class _ActionIcon extends StatelessWidget { final IconData icon; final String tooltip; final VoidCallback onTap; final Color? color; final double size; const _ActionIcon({ required this.icon, required this.tooltip, required this.onTap, this.color, this.size = 36, }); @override Widget build(BuildContext context) { return Tooltip( message: tooltip, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(6), child: Container( width: size, height: size, decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outlineVariant), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, size: 20, color: color ?? Theme.of(context).colorScheme.onSurface), ), ), ); } } class _PayButton extends StatelessWidget { final String label; final Color color; final IconData icon; final VoidCallback onTap; const _PayButton({ required this.label, required this.color, required this.icon, required this.onTap, }); @override Widget build(BuildContext context) { return Tooltip( message: label, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(6), child: Container( width: 52, height: 36, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(6), ), child: Icon(icon, size: 20, color: Colors.white), ), ), ); } } // ── Ticket panel ────────────────────────────────────────────────────────────── class _EmptyDetailPanel extends StatelessWidget { @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _FieldBox(label: 'Concepto', value: ''), const SizedBox(height: 10), Row( children: [ Expanded(child: _FieldBox(label: 'Base imponible', value: '0,00')), const SizedBox(width: 8), Expanded(child: _FieldBox(label: 'I.V.A.', value: '0,00')), ], ), const SizedBox(height: 10), _FieldBox(label: 'Total', value: '0,00'), const SizedBox(height: 12), Text('Fecha reserva: —', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline)), const SizedBox(height: 8), _LineItemsHeader(), ], ); } } class _TicketPanel extends StatelessWidget { final BookingDetail detail; const _TicketPanel({required this.detail}); @override Widget build(BuildContext context) { final t = detail.ticket; final cs = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _FieldBox(label: 'Concepto', value: t?.concepto ?? '—'), const SizedBox(height: 10), Row( children: [ Expanded( child: _FieldBox( label: 'Base imponible', value: t != null ? _fmtMoney(t.base) : '—')), const SizedBox(width: 8), Expanded( child: _FieldBox( label: 'I.V.A.', value: t != null ? _fmtMoney(t.iva) : '—')), ], ), const SizedBox(height: 10), _FieldBox( label: 'Total', value: t != null ? _fmtMoney(t.total) : '—', highlight: true), const SizedBox(height: 12), Row( children: [ Text('Fecha reserva: ', style: TextStyle(fontSize: 12, color: cs.outline)), Text(detail.date, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500)), const Spacer(), if (detail.paid) Chip( label: const Text('Pagada', style: TextStyle(fontSize: 11, color: Colors.white)), backgroundColor: Colors.green.shade600, padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ) else Chip( label: Text('Pendiente', style: TextStyle( fontSize: 11, color: cs.onErrorContainer)), backgroundColor: cs.errorContainer, padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], ), const SizedBox(height: 8), _LineItemsHeader(), if (t != null) ...t.lines.map((line) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ Expanded( child: Text(line.concepto, style: const TextStyle(fontSize: 12))), Text(_fmtMoney(line.precio), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500)), ], ), )), ], ); } } class _LineItemsHeader extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 0), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: cs.outlineVariant)), ), child: Row( children: [ Expanded( child: Text('Concepto', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: cs.outline))), Text('Precio', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: cs.outline)), ], ), ); } } class _FieldBox extends StatelessWidget { final String label; final String value; final bool highlight; const _FieldBox({ required this.label, required this.value, this.highlight = false, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle( fontSize: 11, color: cs.outline, fontWeight: FontWeight.w500)), const SizedBox(height: 3), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: highlight ? cs.primaryContainer : cs.surfaceContainerLow, borderRadius: BorderRadius.circular(6), border: Border.all(color: cs.outlineVariant), ), child: Text( value, style: TextStyle( fontSize: 13, fontWeight: highlight ? FontWeight.bold : null, color: highlight ? cs.onPrimaryContainer : null, ), ), ), ], ); } } // ── Helpers ─────────────────────────────────────────────────────────────────── String _fmtDate(DateTime d) => '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year}'; String _fmtMoney(double v) => '${v.toStringAsFixed(2).replaceAll('.', ',')} €';