Files
yogibook_aury_app/lib/screens/reschedule_page.dart
T
2026-08-22 08:26:27 +02:00

372 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/lesson.dart';
import '../services/lessons_api.dart';
import '../widgets/yogibook_background.dart';
/// Schermata per scegliere uno slot.
/// Due modalità:
/// - RIPROGRAMMA: passa bookingId (+ serviceId della lezione)
/// - PRENOTA: passa orderId (+ serviceId dell'ordine)
class ReschedulePage extends StatefulWidget {
final String token;
final int serviceId;
final String className;
// Modalità riprogramma
final int? bookingId;
// Modalità prenota da ticket
final int? orderId;
const ReschedulePage({
super.key,
required this.token,
required this.serviceId,
required this.className,
this.bookingId,
this.orderId,
});
bool get isBooking => orderId != null;
@override
State<ReschedulePage> createState() => _ReschedulePageState();
}
class _ReschedulePageState extends State<ReschedulePage> {
bool loading = true;
String error = '';
String currentMonth = DateFormat('yyyy-MM').format(DateTime.now());
List<AvailableSlot> slots = [];
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
loading = true;
error = '';
});
try {
final data = await LessonsApi.fetchAvailableSlots(
token: widget.token,
serviceId: widget.serviceId,
month: currentMonth,
);
setState(() => slots = data);
} catch (e) {
setState(() => error = 'Errore: $e');
} finally {
setState(() => loading = false);
}
}
String _shiftMonth(String yyyyMm, int delta) {
final dt = DateFormat('yyyy-MM').parse(yyyyMm);
final shifted = DateTime(dt.year, dt.month + delta, 1);
return DateFormat('yyyy-MM').format(shifted);
}
String _monthLabel(String yyyyMm) {
final dt = DateFormat('yyyy-MM').parse(yyyyMm);
return DateFormat('MMMM yyyy', 'it_IT').format(dt);
}
String _weekdayLabel(String ymd) {
final dt = DateFormat('yyyy-MM-dd').parse(ymd);
final s = DateFormat('EEEE', 'it_IT').format(dt);
return s[0].toUpperCase() + s.substring(1);
}
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
final titolo = widget.isBooking ? 'Prenota lezione' : 'Riprogramma';
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Column(
children: [
Text(
titolo,
style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
Text(
widget.className,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black54,
),
),
],
),
centerTitle: true,
),
body: YogibookBackground(
child: SafeArea(
top: false,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
color: green,
onPressed: () {
setState(
() => currentMonth = _shiftMonth(currentMonth, -1),
);
_load();
},
),
Expanded(
child: Text(
_monthLabel(currentMonth),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
),
),
),
IconButton(
icon: const Icon(Icons.chevron_right),
color: green,
onPressed: () {
setState(
() => currentMonth = _shiftMonth(currentMonth, 1),
);
_load();
},
),
],
),
),
),
Expanded(
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? Center(child: Text(error))
: slots.isEmpty
? const Center(
child: Text('Nessuno slot disponibile questo mese'),
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
itemCount: slots.length,
itemBuilder: (_, i) {
final s = slots[i];
return _SlotCard(
slot: s,
weekday: _weekdayLabel(s.date),
onChoose: () => _confirm(s),
);
},
),
),
],
),
),
),
);
}
Future<void> _confirm(AvailableSlot s) async {
final azione = widget.isBooking ? 'prenotare' : 'spostare';
final ok = await showDialog<bool>(
context: context,
builder: (dialogCtx) => AlertDialog(
title: Text('Confermi?'),
content: Text(
'Vuoi $azione "${s.className}" del ${s.date} alle ${s.time}?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogCtx, false),
child: const Text('Annulla'),
),
ElevatedButton(
onPressed: () => Navigator.pop(dialogCtx, true),
child: const Text('Conferma'),
),
],
),
);
if (ok != true) return;
try {
final String msg;
if (widget.isBooking) {
msg = await LessonsApi.bookFromTicket(
token: widget.token,
orderId: widget.orderId!,
newScheduleId: s.scheduleId,
);
} else {
msg = await LessonsApi.reschedule(
token: widget.token,
bookingId: widget.bookingId!,
newScheduleId: s.scheduleId,
);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e')));
}
}
}
class _SlotCard extends StatelessWidget {
final AvailableSlot slot;
final String weekday;
final VoidCallback onChoose;
const _SlotCard({
required this.slot,
required this.weekday,
required this.onChoose,
});
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
const orange = Color(0xFFF59E0B);
// Determina lo stato dello slot
final bool isBooked = slot.alreadyBooked;
final bool isFull = !slot.bookable && !isBooked;
final bool isSelectable = slot.bookable && !isBooked;
// Colori in base allo stato
Color bgColor;
Color borderColor;
if (isBooked) {
bgColor = const Color(0xFFFFF4E5); // arancio chiaro
borderColor = orange;
} else if (isFull) {
bgColor = const Color(0xFFF3F3F5); // grigio chiaro
borderColor = const Color(0xFFD9D9E0);
} else {
bgColor = Colors.white;
borderColor = const Color(0x14000000);
}
// Etichetta / badge a destra
Widget trailing;
if (isSelectable) {
trailing = ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: onChoose,
child: const Text('Scegli'),
);
} else if (isBooked) {
trailing = _StatusBadge(text: 'Già prenotato', color: orange);
} else {
trailing = _StatusBadge(
text: 'Al completo',
color: const Color(0xFF9AA0A6),
);
}
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.fromLTRB(14, 12, 12, 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: borderColor, width: 1.5),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
slot.className,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
),
),
const SizedBox(height: 4),
Text(
'$weekday ${slot.date}${slot.time}',
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
'Posti liberi: ${slot.freePlaces}/${slot.maxCapacity}',
style: TextStyle(
fontSize: 12,
color: isFull ? orange : Colors.black45,
fontWeight: FontWeight.w700,
),
),
],
),
),
const SizedBox(width: 10),
trailing,
],
),
);
}
}
class _StatusBadge extends StatelessWidget {
final String text;
final Color color;
const _StatusBadge({required this.text, required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color, width: 1.5),
),
child: Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.w800,
fontSize: 12,
),
),
);
}
}