Files
yogibook_aury_app/lib/screens/lessons_page.dart
T
2026-08-11 07:54:22 +02:00

794 lines
26 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/lesson.dart';
import '../models/school.dart';
import '../services/lessons_api.dart';
import 'select_school_page.dart';
import 'home_page.dart';
import 'meditation_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
import 'reschedule_page.dart';
import 'orders_page.dart';
class LessonsPage extends StatefulWidget {
final String token;
final School school;
final String? userFirstName;
const LessonsPage({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
@override
State<LessonsPage> createState() => _LessonsPageState();
}
class _LessonsPageState extends State<LessonsPage> {
bool loading = true;
String error = '';
String currentMonth = DateFormat('yyyy-MM').format(DateTime.now());
LessonsSummary? summary;
List<Lesson> lessons = [];
int bottomIndex = 1;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
loading = true;
error = '';
});
try {
final data = await LessonsApi.fetchMyLessons(
token: widget.token,
month: currentMonth,
);
setState(() {
summary = data.summary;
lessons = data.lessons;
});
} catch (e) {
setState(() => error = 'Errore: $e');
} finally {
setState(() => loading = false);
}
}
// Navigazione mesi calcolata lato client (l'API non restituisce prev/next).
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);
}
void _goPrevMonth() {
setState(() => currentMonth = _shiftMonth(currentMonth, -1));
_load();
}
void _goNextMonth() {
setState(() => currentMonth = _shiftMonth(currentMonth, 1));
_load();
}
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);
}
String _dayNum(String ymd) {
final dt = DateFormat('yyyy-MM-dd').parse(ymd);
return DateFormat('dd').format(dt);
}
String get _name => (widget.userFirstName ?? '').trim();
String get _avatarLetter => _name.isNotEmpty ? _name[0].toUpperCase() : 'U';
void _handleBottomNav(int i) {
if (i == bottomIndex) return;
setState(() => bottomIndex = i);
if (i == 0) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
);
return;
}
if (i == 1) return; // sei già su Lezioni
if (i == 2) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('TODO: vai ad Account')));
return;
}
if (i == 3) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MeditationPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
);
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: AppDrawer(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: Column(
children: [
const Text(
'Le mie lezioni',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
const SizedBox(height: 2),
Text(
widget.school.name,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black54,
),
),
],
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: CircleAvatar(
radius: 16,
backgroundColor: const Color(0xFF10B981),
child: Text(
_avatarLetter,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 12,
),
),
),
),
],
),
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
),
body: YogibookBackground(
child: SafeArea(
top: false,
child: Column(
children: [
// Riepilogo conteggi (le card pastello della webapp)
if (summary != null)
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: _SummaryStrip(summary: summary!),
),
// Selettore mese
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 4),
child: _MonthPillCompact(
label: _monthLabel(currentMonth),
onPrev: _goPrevMonth,
onNext: _goNextMonth,
),
),
// Bottone "Programma lezioni" (solo se ci sono ticket residui)
if (summary != null && summary!.toSchedule > 0)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.add, color: Colors.white),
label: Text(
'Programma lezioni (${summary!.toSchedule} disponibili)',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OrdersPage(token: widget.token),
),
);
_load();
},
),
),
),
Expanded(
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? _ErrorBox(message: error, onRetry: _load)
: lessons.isEmpty
? _EmptyState(
onBrowse: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TODO: vedi corsi')),
);
},
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
itemCount: lessons.length + 1,
itemBuilder: (_, i) {
if (i == lessons.length) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6),
child: Center(
child: Image.asset(
'assets/images/yogibook_logo.png',
height: 56,
),
),
);
}
final l = lessons[i];
return _LessonGreenCardCompact(
lesson: l,
weekday: _weekdayLabel(l.date),
dayNum: _dayNum(l.date),
onReschedule: l.canReschedule
? () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ReschedulePage(
token: widget.token,
bookingId: l.bookingId,
serviceId: l.serviceId,
className: l.className,
),
),
);
_load();
}
: null,
onCancel: l.canDelete
? () async {
final conferma = await showDialog<bool>(
context: context,
builder: (dialogCtx) => AlertDialog(
title: const Text(
'Cancellare la lezione?',
),
content: Text(
'Vuoi cancellare "${l.className}" del ${l.date}? '
'Ricordati di riprogrammarla entro la scadenza.',
),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(dialogCtx, false),
child: const Text('Annulla'),
),
ElevatedButton(
onPressed: () =>
Navigator.pop(dialogCtx, true),
child: const Text('Cancella'),
),
],
),
);
if (conferma != true) return;
try {
await LessonsApi.cancelLesson(
token: widget.token,
bookingId: l.bookingId,
);
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(
const SnackBar(
content: Text('Lezione cancellata'),
),
);
_load();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(content: Text('$e')),
);
}
}
: null,
);
},
),
),
],
),
),
),
);
}
}
class _SummaryStrip extends StatelessWidget {
final LessonsSummary summary;
const _SummaryStrip({required this.summary});
@override
Widget build(BuildContext context) {
final items = <_SummaryItem>[
_SummaryItem('Acquistate', summary.purchased, const Color(0xFFE2C4FB)),
_SummaryItem('Praticate', summary.practiced, const Color(0xFFC4E1FB)),
_SummaryItem('Prenotate', summary.booked, const Color(0xFFCDFBC4)),
_SummaryItem('Da confermare', summary.pending, const Color(0xFFFBFAC4)),
_SummaryItem(
'Da programmare',
summary.toSchedule,
const Color(0xFFFBE4C4),
),
_SummaryItem('Perse', summary.lost, const Color(0xFFFBC7C4)),
];
return SizedBox(
height: 76,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (_, i) {
final it = items[i];
return Container(
width: 96,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: it.color,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${it.value}',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 2),
Text(
it.label,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
);
}
}
class _SummaryItem {
final String label;
final int value;
final Color color;
_SummaryItem(this.label, this.value, this.color);
}
class _MonthPillCompact extends StatelessWidget {
final String label;
final VoidCallback? onPrev;
final VoidCallback? onNext;
const _MonthPillCompact({
required this.label,
required this.onPrev,
required this.onNext,
});
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
blurRadius: 14,
color: Color(0x11000000),
offset: Offset(0, 8),
),
],
),
child: Row(
children: [
IconButton(
onPressed: onPrev,
icon: const Icon(Icons.chevron_left),
iconSize: 22,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
color: onPrev == null ? Colors.black26 : green,
),
Expanded(
child: Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w900),
),
),
IconButton(
onPressed: onNext,
icon: const Icon(Icons.chevron_right),
iconSize: 22,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
color: onNext == null ? Colors.black26 : green,
),
],
),
);
}
}
class _LessonGreenCardCompact extends StatelessWidget {
final Lesson lesson;
final String weekday;
final String dayNum;
final VoidCallback? onReschedule;
final VoidCallback? onCancel;
const _LessonGreenCardCompact({
required this.lesson,
required this.weekday,
required this.dayNum,
required this.onReschedule,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
const darkGreen = Color(0xFF065F46);
final time = _shortTime(lesson.time);
final canModify = lesson.canReschedule || lesson.canDelete;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: const Color(0xFFE7F8F1),
borderRadius: BorderRadius.circular(18),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color(0x12000000),
offset: Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Row(
children: [
// left panel
Container(
width: 92,
color: const Color(0xFFBFF3DE),
padding: const EdgeInsets.fromLTRB(10, 12, 10, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dayNum,
style: const TextStyle(
fontSize: 28,
height: 1,
fontWeight: FontWeight.w900,
color: darkGreen,
),
),
const SizedBox(height: 4),
Text(
weekday,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: darkGreen,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.schedule,
size: 14,
color: Colors.black54,
),
const SizedBox(width: 6),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
time,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
),
),
),
],
),
),
],
),
),
// right body
Expanded(
child: Container(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
color: const Color(0xFFE7F8F1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lesson.className,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Row(
children: [
const Icon(
Icons.place_outlined,
size: 16,
color: Colors.black54,
),
const SizedBox(width: 6),
Expanded(
child: Text(
lesson.location.trim().isEmpty
? 'Sala da definire'
: lesson.location,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF404040),
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: SizedBox(
height: 30,
child: ElevatedButton(
onPressed: onReschedule,
style: ElevatedButton.styleFrom(
backgroundColor: green,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Riprogramma',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 30,
child: OutlinedButton(
onPressed: onCancel,
style: OutlinedButton.styleFrom(
foregroundColor: green,
side: const BorderSide(color: green, width: 2),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Cancella',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
),
),
),
],
),
if (!canModify) ...[
const SizedBox(height: 6),
const Text(
'Non modificabile',
style: TextStyle(fontSize: 11, color: Colors.black54),
),
],
],
),
),
),
],
),
),
);
}
static String _shortTime(String t) => t.length >= 5 ? t.substring(0, 5) : t;
}
class _EmptyState extends StatelessWidget {
final VoidCallback onBrowse;
const _EmptyState({required this.onBrowse});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.event_busy, size: 64, color: Colors.black26),
const SizedBox(height: 14),
const Text(
'Nessuna lezione prenotata',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: Colors.black54,
),
),
const SizedBox(height: 6),
const Text(
'Le tue lezioni appariranno qui',
style: TextStyle(color: Colors.black45),
),
const SizedBox(height: 14),
ElevatedButton(
onPressed: onBrowse,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text('Vedi i corsi'),
),
],
),
),
);
}
}
class _ErrorBox extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorBox({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 58, color: Colors.redAccent),
const SizedBox(height: 10),
Text(message, textAlign: TextAlign.center),
const SizedBox(height: 12),
ElevatedButton(onPressed: onRetry, child: const Text('Riprova')),
],
),
),
);
}
}