import 'dart:convert'; import 'package:http/http.dart' as http; import '../config/api_config.dart'; /// Un partecipante a una classe (vista insegnante). class TeacherParticipant { final int bookingId; final String fullName; final bool isLost; final bool isPending; const TeacherParticipant({ required this.bookingId, required this.fullName, required this.isLost, this.isPending = false, }); factory TeacherParticipant.fromMap(Map m) => TeacherParticipant( bookingId: (m['booking_id'] as num?)?.toInt() ?? 0, fullName: (m['full_name'] ?? '').toString().trim(), isLost: m['is_lost'] == true, isPending: m['is_pending'] == true, ); } /// Una classe programmata con i suoi partecipanti. class TeacherClass { final int scheduleId; final int serviceId; final String className; final String? colorClass; final String dateTime; // "YYYY-MM-DD HH:MM:SS" final int maxCapacity; final int bookedCount; final bool isFull; final List participants; const TeacherClass({ required this.scheduleId, required this.serviceId, required this.className, required this.colorClass, required this.dateTime, required this.maxCapacity, required this.bookedCount, required this.isFull, required this.participants, }); factory TeacherClass.fromMap(Map m) => TeacherClass( scheduleId: (m['schedule_id'] as num?)?.toInt() ?? 0, serviceId: (m['service_id'] as num?)?.toInt() ?? 0, className: (m['class_name'] ?? '').toString(), colorClass: (m['color_class'])?.toString(), dateTime: (m['date_time'] ?? '').toString(), maxCapacity: (m['max_capacity'] as num?)?.toInt() ?? 0, bookedCount: (m['booked_count'] as num?)?.toInt() ?? 0, isFull: m['is_full'] == true, participants: ((m['participants'] as List?) ?? []) .cast>() .map(TeacherParticipant.fromMap) .toList(), ); } /// Utente trovato nella ricerca (per aggiungi partecipante). class TeacherUser { final int id; final String firstName; final String lastName; final String email; const TeacherUser({ required this.id, required this.firstName, required this.lastName, required this.email, }); String get fullName => '$firstName $lastName'.trim(); factory TeacherUser.fromMap(Map m) => TeacherUser( id: (m['id'] as num?)?.toInt() ?? 0, firstName: (m['first_name'] ?? '').toString(), lastName: (m['last_name'] ?? '').toString(), email: (m['email'] ?? '').toString(), ); } /// Ordine/pacchetto di un utente con residui. class TeacherOrder { final int idOrderBook; final String serviceName; final int tickets; final int used; final int remaining; final String? expireOn; const TeacherOrder({ required this.idOrderBook, required this.serviceName, required this.tickets, required this.used, required this.remaining, required this.expireOn, }); factory TeacherOrder.fromMap(Map m) => TeacherOrder( idOrderBook: (m['idorderbook'] as num?)?.toInt() ?? 0, serviceName: (m['service_name'] ?? '').toString(), tickets: (m['tickets'] as num?)?.toInt() ?? 0, used: (m['used'] as num?)?.toInt() ?? 0, remaining: (m['remaining'] as num?)?.toInt() ?? 0, expireOn: (m['expireon'])?.toString(), ); } class TeacherApi { static Map _headers(String token) => { 'Accept': 'application/json', 'Authorization': 'Bearer $token', }; /// Classi di un mese (YYYY-MM) con partecipanti. Solo staff lato server. static Future> fetchClasses({ required String token, required String month, }) async { final query = month.isNotEmpty ? '?month=$month' : ''; final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_classes.php$query', ); final res = await http.get(uri, headers: _headers(token)); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception(data['error'] ?? data['message'] ?? 'Errore classi'); } return ((data['classes'] as List?) ?? []) .cast>() .map(TeacherClass.fromMap) .toList(); } /// Segna una prenotazione come persa (lost=true) o la ripristina (false). static Future setLost({ required String token, required int bookingId, required bool lost, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_mark_lost.php', ); final res = await http.post( uri, headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer $token', }, body: 'booking_id=$bookingId&lost=${lost ? 'Y' : 'N'}', ); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception( data['error'] ?? data['message'] ?? 'Errore aggiornamento', ); } } /// Elimina una prenotazione (libera il ticket all'allievo). static Future deleteBooking({ required String token, required int bookingId, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_delete_booking.php', ); final res = await http.post( uri, headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer $token', }, body: 'booking_id=$bookingId', ); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception( data['error'] ?? data['message'] ?? 'Errore eliminazione', ); } } /// Classi destinazione per riprogrammare una prenotazione /// (future, non piene, escluse quelle dove è già iscritto). static Future> availableClasses({ required String token, required int bookingId, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_available_classes.php?booking_id=$bookingId', ); final res = await http.get( uri, headers: {'Accept': 'application/json', 'Authorization': 'Bearer $token'}, ); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception(data['error'] ?? data['message'] ?? 'Errore classi'); } return ((data['classes'] as List?) ?? []) .cast>() .map(TeacherClass.fromMap) .toList(); } /// Esegue la riprogrammazione su una nuova classe. static Future reprogram({ required String token, required int bookingId, required int scheduleId, required bool countAsReprogram, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_reprogram.php', ); final res = await http.post( uri, headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer $token', }, body: 'booking_id=$bookingId&schedule_id=$scheduleId' '&is_reprogrammed=${countAsReprogram ? 'Y' : 'N'}', ); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception( data['error'] ?? data['message'] ?? 'Errore riprogrammazione', ); } } /// Ricerca utenti per nome/cognome (min 2 caratteri in uno dei due). static Future> searchUsers({ required String token, required String first, required String last, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_search_users.php' '?first=${Uri.encodeQueryComponent(first)}' '&last=${Uri.encodeQueryComponent(last)}', ); final res = await http.get(uri, headers: _headers(token)); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception(data['error'] ?? data['message'] ?? 'Errore ricerca'); } return ((data['results'] as List?) ?? []) .cast>() .map(TeacherUser.fromMap) .toList(); } /// Ordini con residui di un utente. static Future> userOrders({ required String token, required int userId, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_user_orders.php?user_id=$userId', ); final res = await http.get(uri, headers: _headers(token)); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception(data['error'] ?? data['message'] ?? 'Errore ordini'); } return ((data['orders'] as List?) ?? []) .cast>() .map(TeacherOrder.fromMap) .toList(); } /// Aggiunge un partecipante a una classe. /// [mode] = 'scala' | 'omaggio' | 'nuovo'. /// Per utente esistente passare [userId] > 0; per nuovo utente passare /// [userId] = 0 con name/surname (email opzionale). static Future addBooking({ required String token, required int scheduleId, required int serviceId, required String bookingStart, required String mode, int userId = 0, String name = '', String surname = '', String email = '', int? orderId, String? newExpiry, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_add_booking.php', ); final body = { 'idserviceschedule': '$scheduleId', 'idservice': '$serviceId', 'bookingstart': bookingStart, 'bkmode': mode, 'user_id': '$userId', 'name': name, 'surname': surname, 'email': email, if (orderId != null) 'idorder': '$orderId', if (newExpiry != null) 'new_expiry': newExpiry, }; final res = await http.post( uri, headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer $token', }, body: body.entries .map( (e) => '${Uri.encodeQueryComponent(e.key)}=' '${Uri.encodeQueryComponent(e.value)}', ) .join('&'), ); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception(data['error'] ?? data['message'] ?? 'Errore inserimento'); } } /// Conferma una prenotazione in attesa (pending -> booked). static Future confirmPending({ required String token, required int bookingId, }) async { final uri = Uri.parse( '${ApiConfig.laravelApiBase}/api_teacher_confirm_pending.php', ); final res = await http.post( uri, headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer $token', }, body: 'booking_id=$bookingId', ); Map data; try { data = jsonDecode(res.body) as Map; } catch (_) { throw Exception('RAW(${res.statusCode}): ${res.body}'); } if (res.statusCode != 200 || data['success'] != true) { throw Exception(data['error'] ?? data['message'] ?? 'Errore conferma'); } } }