Files
yogibook_aury_app/lib/screens/teacher_panel_page.dart
T

623 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/school.dart';
import '../services/teacher_api.dart';
import '../widgets/yogibook_background.dart';
import 'teacher_class_detail_page.dart';
/// Pannello Insegnante (staff: Admin/teacher).
/// Fase 1: sola lettura. Classi del mese con partecipanti, focus su oggi.
class TeacherPanelPage extends StatefulWidget {
final String token;
final School school;
final String? userFirstName;
const TeacherPanelPage({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
@override
State<TeacherPanelPage> createState() => _TeacherPanelPageState();
}
class _TeacherPanelPageState extends State<TeacherPanelPage> {
static const Color kGreen = Color(0xFF10B981);
static const Color kDarkGreen = Color(0xFF065F46);
bool loading = true;
String error = '';
String currentMonth = DateFormat('yyyy-MM').format(DateTime.now());
List<TeacherClass> classes = [];
// per l'auto-scroll a oggi
final ScrollController _scrollController = ScrollController();
final Map<int, GlobalKey> _dayKeys = {}; // index classe -> key
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Future<void> _load() async {
setState(() {
loading = true;
error = '';
});
try {
final data = await TeacherApi.fetchClasses(
token: widget.token,
month: currentMonth,
);
setState(() {
classes = data;
});
// dopo il render, prova a scrollare a oggi (con un attimo di ritardo
// per essere sicuri che le card siano state disposte)
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 250), _scrollToToday);
});
} catch (e) {
setState(() => error = 'Errore: $e');
} finally {
if (mounted) setState(() => loading = false);
}
}
// Trova la prima classe con data >= oggi e scrolla lì.
void _scrollToToday() {
if (classes.isEmpty) return;
final today = DateTime.now();
int targetIndex = 0;
for (int i = 0; i < classes.length; i++) {
final dt = DateTime.tryParse(classes[i].dateTime);
if (dt != null &&
!dt.isBefore(DateTime(today.year, today.month, today.day))) {
targetIndex = i;
break;
}
targetIndex = i; // se tutte passate, resta sull'ultima
}
final key = _dayKeys[targetIndex];
final ctx = key?.currentContext;
if (ctx != null) {
Scrollable.ensureVisible(
ctx,
duration: const Duration(milliseconds: 400),
alignment: 0.1,
);
}
}
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 iso) {
final dt = DateTime.tryParse(iso);
if (dt == null) return '';
final s = DateFormat('EEEE', 'it_IT').format(dt);
return s[0].toUpperCase() + s.substring(1);
}
String _dayNum(String iso) {
final dt = DateTime.tryParse(iso);
if (dt == null) return '--';
return DateFormat('dd').format(dt);
}
String _time(String iso) {
final dt = DateTime.tryParse(iso);
if (dt == null) return '';
return DateFormat('HH:mm').format(dt);
}
bool _isToday(String iso) {
final dt = DateTime.tryParse(iso);
if (dt == null) return false;
final now = DateTime.now();
return dt.year == now.year && dt.month == now.month && dt.day == now.day;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
'Pannello classi',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
),
body: YogibookBackground(
child: SafeArea(
top: false,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: _MonthPill(
label: _monthLabel(currentMonth),
onPrev: _goPrevMonth,
onNext: _goNextMonth,
),
),
Expanded(
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? _ErrorBox(message: error, onRetry: _load)
: classes.isEmpty
? const _EmptyState()
: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
itemCount: classes.length,
itemBuilder: (_, i) {
final key = _dayKeys.putIfAbsent(
i,
() => GlobalKey(),
);
final c = classes[i];
final dt = DateTime.tryParse(c.dateTime);
final now = DateTime.now();
final isPast =
dt != null &&
dt.isBefore(
DateTime(now.year, now.month, now.day),
);
return Container(
key: key,
child: _ClassCard(
cls: c,
weekday: _weekdayLabel(c.dateTime),
dayNum: _dayNum(c.dateTime),
time: _time(c.dateTime),
highlightToday: _isToday(c.dateTime),
isPast: isPast,
onTap: () async {
final changed = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => TeacherClassDetailPage(
token: widget.token,
cls: c,
),
),
);
if (changed == true) _load();
},
),
);
},
),
),
],
),
),
),
);
}
}
// Card classe compatta: al tap apre la pagina dettaglio.
class _ClassCard extends StatelessWidget {
final TeacherClass cls;
final String weekday;
final String dayNum;
final String time;
final bool highlightToday;
final bool isPast;
final VoidCallback onTap;
const _ClassCard({
required this.cls,
required this.weekday,
required this.dayNum,
required this.time,
required this.highlightToday,
required this.isPast,
required this.onTap,
});
static const Color kGreen = Color(0xFF10B981);
static const Color kDarkGreen = Color(0xFF065F46);
@override
Widget build(BuildContext context) {
final isFull = cls.isFull;
final nearFull =
!isFull &&
cls.maxCapacity > 0 &&
cls.bookedCount >= cls.maxCapacity - 1;
final fillColor = isFull
? const Color(0xFFC0392B)
: nearFull
? const Color(0xFFE67E22)
: const Color(0xFF1A7F52);
final card = Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: highlightToday
? Border.all(color: kGreen, width: 2)
: Border.all(color: const Color(0x11000000)),
boxShadow: const [
BoxShadow(
blurRadius: 14,
color: Color(0x10000000),
offset: Offset(0, 6),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// pannello data
Container(
width: 74,
color: highlightToday ? kGreen : const Color(0xFF0F766E),
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 6,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
dayNum,
style: const TextStyle(
fontSize: 26,
height: 1,
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
const SizedBox(height: 2),
Text(
weekday.length > 3 ? weekday.substring(0, 3) : weekday,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: Colors.white70,
),
),
],
),
),
// corpo
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 10, 12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
cls.className,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (highlightToday)
Container(
margin: const EdgeInsets.only(left: 6),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: kGreen,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'OGGI',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 6),
Row(
children: [
const Icon(
Icons.schedule,
size: 14,
color: Colors.black45,
),
const SizedBox(width: 4),
Text(
time,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFF404040),
),
),
const SizedBox(width: 12),
Icon(Icons.group, size: 14, color: fillColor),
const SizedBox(width: 4),
Text(
'${cls.bookedCount}/${cls.maxCapacity}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: fillColor,
),
),
],
),
],
),
),
),
// chevron
const Padding(
padding: EdgeInsets.only(right: 10),
child: Icon(Icons.chevron_right, color: Colors.black26),
),
],
),
),
),
);
return Opacity(
opacity: isPast && !highlightToday ? 0.55 : 1.0,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: card,
),
),
);
}
}
class _ParticipantRow extends StatelessWidget {
final TeacherParticipant p;
const _ParticipantRow({required this.p});
// colore avatar deterministico dal nome (come adminpanel)
Color _avatarColor(String seed) {
const palette = [
Color(0xFF1EBF73),
Color(0xFF2980B9),
Color(0xFF8E44AD),
Color(0xFFE67E22),
Color(0xFF16A085),
Color(0xFFC0392B),
Color(0xFF2C3E50),
Color(0xFFD35400),
];
int h = 0;
for (final c in seed.codeUnits) {
h = (h + c) % palette.length;
}
return palette[h];
}
String get _initials {
final parts = p.fullName.trim().split(RegExp(r'\s+'));
if (parts.isEmpty || parts.first.isEmpty) return '?';
final a = parts.first[0];
final b = parts.length > 1 ? parts.last[0] : '';
return (a + b).toUpperCase();
}
@override
Widget build(BuildContext context) {
final lost = p.isLost;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: lost ? const Color(0xFFFBF1F0) : const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: lost ? const Color(0xFFF3D6D2) : const Color(0xFFEEF0F3),
),
),
child: Row(
children: [
Container(
width: 38,
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
color: lost ? const Color(0xFF7F8C8D) : _avatarColor(p.fullName),
shape: BoxShape.circle,
),
child: Text(
_initials,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 14,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
p.fullName,
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
decoration: BoxDecoration(
color: lost ? const Color(0xFF1F2937) : const Color(0xFFE3F2EC),
borderRadius: BorderRadius.circular(20),
),
child: Text(
lost ? 'Persa' : 'Prenotata',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: lost ? Colors.white : const Color(0xFF1A7F52),
),
),
),
],
),
);
}
}
class _MonthPill extends StatelessWidget {
final String label;
final VoidCallback onPrev;
final VoidCallback onNext;
const _MonthPill({
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,
color: 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,
color: green,
),
],
),
);
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return const Center(
child: Padding(
padding: EdgeInsets.all(18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.event_busy, size: 64, color: Colors.black26),
SizedBox(height: 14),
Text(
'Nessuna classe questo mese',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: Colors.black54,
),
),
],
),
),
);
}
}
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')),
],
),
),
);
}
}