Files
yogibook_aury_app/lib/screens/teacher_add_participant_page.dart
T

600 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../services/teacher_api.dart';
import '../widgets/yogibook_background.dart';
/// Aggiungi partecipante a una classe (vista insegnante).
/// Flusso: cerca/crea utente -> scegli modalità -> conferma.
/// Ritorna `true` (Navigator.pop) se l'inserimento è avvenuto.
class TeacherAddParticipantPage extends StatefulWidget {
final String token;
final TeacherClass cls;
const TeacherAddParticipantPage({
super.key,
required this.token,
required this.cls,
});
@override
State<TeacherAddParticipantPage> createState() =>
_TeacherAddParticipantPageState();
}
class _TeacherAddParticipantPageState extends State<TeacherAddParticipantPage> {
static const Color kGreen = Color(0xFF10B981);
final _firstCtrl = TextEditingController();
final _lastCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
// ricerca
List<TeacherUser> _results = [];
bool _searching = false;
// utente selezionato (null = nuovo utente da creare)
TeacherUser? _selectedUser;
bool _isNewUser = false;
// ordini dell'utente selezionato
List<TeacherOrder> _orders = [];
bool _loadingOrders = false;
// modalità: 'scala' | 'omaggio' | 'nuovo'
String _mode = 'omaggio';
int? _selectedOrderId;
DateTime _newExpiry = DateTime.now().add(const Duration(days: 90));
bool _submitting = false;
@override
void dispose() {
_firstCtrl.dispose();
_lastCtrl.dispose();
_emailCtrl.dispose();
super.dispose();
}
void _snack(String msg) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
Future<void> _search() async {
final f = _firstCtrl.text.trim();
final l = _lastCtrl.text.trim();
if (f.length < 2 && l.length < 2) {
setState(() => _results = []);
return;
}
setState(() => _searching = true);
try {
final r = await TeacherApi.searchUsers(
token: widget.token,
first: f,
last: l,
);
if (!mounted) return;
setState(() => _results = r);
} catch (e) {
_snack('$e');
} finally {
if (mounted) setState(() => _searching = false);
}
}
Future<void> _selectUser(TeacherUser u) async {
setState(() {
_selectedUser = u;
_isNewUser = false;
_results = [];
_firstCtrl.text = u.firstName;
_lastCtrl.text = u.lastName;
_emailCtrl.text = u.email;
_loadingOrders = true;
_orders = [];
});
try {
final o = await TeacherApi.userOrders(token: widget.token, userId: u.id);
if (!mounted) return;
// default modalità: scala se ha pacchetti con residui, altrimenti omaggio
final usable = o.where((x) => x.remaining > 0).toList();
setState(() {
_orders = o;
if (usable.isNotEmpty) {
_mode = 'scala';
_selectedOrderId = usable.first.idOrderBook;
} else {
_mode = 'omaggio';
}
});
} catch (e) {
_snack('$e');
} finally {
if (mounted) setState(() => _loadingOrders = false);
}
}
// segna che vogliamo creare un nuovo utente coi dati digitati
void _useAsNewUser() {
final f = _firstCtrl.text.trim();
final l = _lastCtrl.text.trim();
if (f.isEmpty || l.isEmpty) {
_snack('Inserisci nome e cognome del nuovo utente.');
return;
}
setState(() {
_selectedUser = null;
_isNewUser = true;
_results = [];
_orders = [];
_mode = 'omaggio'; // nuovo utente non ha pacchetti
});
}
void _clearSelection() {
setState(() {
_selectedUser = null;
_isNewUser = false;
_orders = [];
_selectedOrderId = null;
_mode = 'omaggio';
});
}
Future<void> _confirm() async {
// deve esserci un utente selezionato o un nuovo utente valido
if (_selectedUser == null && !_isNewUser) {
_snack('Seleziona un utente o creane uno nuovo.');
return;
}
if (_mode == 'scala' && _selectedOrderId == null) {
_snack('Seleziona un pacchetto da cui scalare.');
return;
}
setState(() => _submitting = true);
try {
await TeacherApi.addBooking(
token: widget.token,
scheduleId: widget.cls.scheduleId,
serviceId: widget.cls.serviceId,
bookingStart: widget.cls.dateTime,
mode: _mode,
userId: _selectedUser?.id ?? 0,
name: _isNewUser ? _firstCtrl.text.trim() : '',
surname: _isNewUser ? _lastCtrl.text.trim() : '',
email: _emailCtrl.text.trim(),
orderId: _mode == 'scala' ? _selectedOrderId : null,
newExpiry: _mode == 'nuovo'
? DateFormat('yyyy-MM-dd').format(_newExpiry)
: null,
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (e) {
_snack('$e');
setState(() => _submitting = false);
}
}
@override
Widget build(BuildContext context) {
final hasSelection = _selectedUser != null || _isNewUser;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
'Aggiungi partecipante',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
),
body: YogibookBackground(
child: SafeArea(
top: false,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
children: [
_classBanner(),
const SizedBox(height: 14),
_searchFields(),
if (!hasSelection) ...[const SizedBox(height: 8), _resultsBox()],
if (hasSelection) ...[
const SizedBox(height: 14),
_selectionCard(),
const SizedBox(height: 14),
_modeSection(),
const SizedBox(height: 20),
_confirmButton(),
],
],
),
),
),
);
}
Widget _classBanner() {
final dt = DateTime.tryParse(widget.cls.dateTime);
final when = dt == null
? widget.cls.dateTime
: DateFormat("EEE d MMM, HH:mm", 'it_IT').format(dt);
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: const [
BoxShadow(
blurRadius: 12,
color: Color(0x0F000000),
offset: Offset(0, 6),
),
],
),
child: Row(
children: [
const Icon(Icons.class_, color: kGreen),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.cls.className,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
),
),
Text(
'$when · ${widget.cls.bookedCount}/${widget.cls.maxCapacity} posti',
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
),
],
),
);
}
Widget _searchFields() {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: const [
BoxShadow(
blurRadius: 12,
color: Color(0x0F000000),
offset: Offset(0, 6),
),
],
),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _firstCtrl,
onChanged: (_) => _search(),
decoration: const InputDecoration(
labelText: 'Nome',
isDense: true,
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: _lastCtrl,
onChanged: (_) => _search(),
decoration: const InputDecoration(
labelText: 'Cognome',
isDense: true,
border: OutlineInputBorder(),
),
),
),
],
),
const SizedBox(height: 10),
TextField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email (per nuovo utente)',
isDense: true,
border: OutlineInputBorder(),
),
),
],
),
);
}
Widget _resultsBox() {
if (_searching) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
final f = _firstCtrl.text.trim();
final l = _lastCtrl.text.trim();
if (f.length < 2 && l.length < 2) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(
'Digita almeno 2 lettere per cercare.',
style: TextStyle(color: Colors.black45),
),
);
}
return Column(
children: [
..._results.map(
(u) => Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: kGreen,
child: Text(
((u.firstName.isNotEmpty ? u.firstName[0] : '') +
(u.lastName.isNotEmpty ? u.lastName[0] : ''))
.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
),
title: Text(
u.fullName,
style: const TextStyle(fontWeight: FontWeight.w700),
),
subtitle: Text(u.email),
onTap: () => _selectUser(u),
),
),
),
// opzione "crea nuovo utente" con i dati digitati
Container(
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: const Color(0xFFF3F0FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFD9CFFB)),
),
child: ListTile(
leading: const Icon(Icons.person_add, color: Color(0xFF4F46E5)),
title: const Text(
'Crea nuovo utente',
style: TextStyle(fontWeight: FontWeight.w800),
),
subtitle: const Text('Usa nome, cognome ed email digitati sopra'),
onTap: _useAsNewUser,
),
),
],
);
}
Widget _selectionCard() {
final title = _isNewUser
? '${_firstCtrl.text.trim()} ${_lastCtrl.text.trim()} (nuovo)'
: _selectedUser!.fullName;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFE7F8F1),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: kGreen),
),
child: Row(
children: [
const Icon(Icons.check_circle, color: kGreen),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
TextButton(onPressed: _clearSelection, child: const Text('Cambia')),
],
),
);
}
Widget _modeSection() {
final usable = _orders.where((o) => o.remaining > 0).toList();
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: const [
BoxShadow(
blurRadius: 12,
color: Color(0x0F000000),
offset: Offset(0, 6),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Modalità',
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 14),
),
const SizedBox(height: 6),
if (_loadingOrders)
const Padding(
padding: EdgeInsets.all(12),
child: Center(child: CircularProgressIndicator()),
)
else ...[
// scala (solo se ci sono pacchetti usabili)
RadioListTile<String>(
contentPadding: EdgeInsets.zero,
activeColor: kGreen,
value: 'scala',
groupValue: _mode,
onChanged: usable.isEmpty
? null
: (v) => setState(() => _mode = v!),
title: Text(
usable.isEmpty
? 'Scala da pacchetto (nessuno disponibile)'
: 'Scala da un pacchetto',
style: TextStyle(
fontWeight: FontWeight.w700,
color: usable.isEmpty ? Colors.black38 : Colors.black87,
),
),
),
if (_mode == 'scala' && usable.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 12, bottom: 8),
child: Column(
children: usable
.map(
(o) => RadioListTile<int>(
contentPadding: EdgeInsets.zero,
activeColor: kGreen,
value: o.idOrderBook,
groupValue: _selectedOrderId,
onChanged: (v) =>
setState(() => _selectedOrderId = v),
title: Text(
o.serviceName,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
subtitle: Text(
'${o.remaining}/${o.tickets} residui'
'${o.expireOn != null ? ' · scade ${o.expireOn}' : ''}',
style: const TextStyle(fontSize: 12),
),
),
)
.toList(),
),
),
// omaggio
RadioListTile<String>(
contentPadding: EdgeInsets.zero,
activeColor: kGreen,
value: 'omaggio',
groupValue: _mode,
onChanged: (v) => setState(() => _mode = v!),
title: const Text(
'Aggiungi come omaggio',
style: TextStyle(fontWeight: FontWeight.w700),
),
subtitle: const Text(
'Non consuma pacchetti, non spostabile',
style: TextStyle(fontSize: 12),
),
),
// nuovo ordine
RadioListTile<String>(
contentPadding: EdgeInsets.zero,
activeColor: kGreen,
value: 'nuovo',
groupValue: _mode,
onChanged: (v) => setState(() => _mode = v!),
title: const Text(
'Crea nuovo ordine (1 lezione)',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
if (_mode == 'nuovo')
Padding(
padding: const EdgeInsets.only(left: 12, top: 4),
child: Row(
children: [
const Icon(Icons.event, size: 18, color: Colors.black45),
const SizedBox(width: 8),
Text(
'Scadenza: ${DateFormat('dd/MM/yyyy').format(_newExpiry)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const Spacer(),
TextButton(
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: _newExpiry,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(
const Duration(days: 730),
),
);
if (picked != null) {
setState(() => _newExpiry = picked);
}
},
child: const Text('Cambia'),
),
],
),
),
],
],
),
);
}
Widget _confirmButton() {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: kGreen,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: _submitting ? null : _confirm,
child: _submitting
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Aggiungi alla classe',
style: TextStyle(
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
),
);
}
}