// lib/screens/medical_certificates_page.dart import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:url_launcher/url_launcher.dart'; import '../models/school.dart'; import '../services/medical_certificates_api.dart'; import '../config/api_config.dart'; import '../widgets/app_drawer.dart'; import '../widgets/app_bottom_nav.dart'; import 'home_page.dart'; import 'lessons_page.dart'; import 'meditation_page.dart'; import '../widgets/yogibook_background.dart'; class MedicalCertificatesPage extends StatefulWidget { final String token; final School school; final String? userFirstName; const MedicalCertificatesPage({ super.key, required this.token, required this.school, this.userFirstName, }); @override State createState() => _MedicalCertificatesPageState(); } class _MedicalCertificatesPageState extends State { static const Color kBg = Color(0xFFF6F6FB); bool loading = true; String error = ''; List> certs = []; // upload flow File? pickedFile; String pickedLabel = ''; final docNameCtrl = TextEditingController(text: 'certificato'); final notesCtrl = TextEditingController(); DateTime? expiryDate; bool uploading = false; // ✅ questa pagina è "sezione account" int bottomIndex = 2; @override void initState() { super.initState(); _reload(); } @override void dispose() { docNameCtrl.dispose(); notesCtrl.dispose(); super.dispose(); } Future _reload() async { setState(() { loading = true; error = ''; }); try { final list = await MedicalCertificatesApi.list(token: widget.token); setState(() => certs = list); } catch (e) { setState(() => error = e.toString()); } finally { setState(() => loading = false); } } Future _pickFromChooser() async { final choice = await showModalBottomSheet( context: context, showDragHandle: true, builder: (_) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.photo_camera), title: const Text('Fotocamera'), onTap: () => Navigator.pop(context, 'camera'), ), ListTile( leading: const Icon(Icons.photo_library), title: const Text('Galleria'), onTap: () => Navigator.pop(context, 'gallery'), ), ListTile( leading: const Icon(Icons.picture_as_pdf), title: const Text('PDF'), onTap: () => Navigator.pop(context, 'pdf'), ), const SizedBox(height: 10), ], ), ), ); if (choice == null) return; File? f; String label = ''; if (choice == 'camera' || choice == 'gallery') { final picker = ImagePicker(); final x = await picker.pickImage( source: choice == 'camera' ? ImageSource.camera : ImageSource.gallery, imageQuality: 85, ); if (x == null) return; f = File(x.path); label = x.name; } if (choice == 'pdf') { final res = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: const ['pdf'], withData: false, ); if (res == null || res.files.isEmpty) return; final path = res.files.single.path; if (path == null) return; f = File(path); label = res.files.single.name; } if (!mounted) return; setState(() { pickedFile = f; pickedLabel = label; docNameCtrl.text = 'certificato'; notesCtrl.clear(); expiryDate = null; }); } Future _pickExpiry() async { final now = DateTime.now(); final d = await showDatePicker( context: context, firstDate: DateTime(now.year, now.month, now.day), lastDate: DateTime(now.year + 10), initialDate: expiryDate ?? now, ); if (d == null) return; setState(() => expiryDate = d); } String _fmtYYYYMMDD(DateTime d) { final mm = d.month.toString().padLeft(2, '0'); final dd = d.day.toString().padLeft(2, '0'); return '${d.year}-$mm-$dd'; } String _fmtDDMMYYYY(DateTime d) { final dd = d.day.toString().padLeft(2, '0'); final mm = d.month.toString().padLeft(2, '0'); return '$dd/$mm/${d.year}'; } Future _doUpload() async { if (pickedFile == null) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Seleziona prima un file.'))); return; } if (expiryDate == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Seleziona la data di scadenza.')), ); return; } setState(() { uploading = true; error = ''; }); try { await MedicalCertificatesApi.upload( token: widget.token, file: pickedFile!, documentName: docNameCtrl.text, expiryDateYYYYMMDD: _fmtYYYYMMDD(expiryDate!), notes: notesCtrl.text, ); setState(() { pickedFile = null; pickedLabel = ''; docNameCtrl.text = 'certificato'; notesCtrl.clear(); expiryDate = null; }); await _reload(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Certificato caricato correttamente.')), ); } catch (e) { setState(() => error = e.toString()); } finally { setState(() => uploading = false); } } Future _openFileUrl(String url) async { // url tipico: "/userarea/certificate/xxx" // fix plural errato se presente var fixed = url.replaceFirst( '/userarea/certificates/', '/userarea/certificate/', ); // se non ha /public davanti, lo aggiungiamo if (!fixed.startsWith('/public/')) { fixed = '/public${fixed.startsWith('/') ? '' : '/'}$fixed'; } final abs = '${ApiConfig.scheme}://${ApiConfig.host}$fixed'; final uri = Uri.parse(abs); final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); if (!ok && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Impossibile aprire il file.')), ); } } Future _deleteCert(int id) async { final ok = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Elimina certificato'), content: const Text('Vuoi davvero eliminare questo certificato?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Annulla'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), child: const Text('Elimina'), ), ], ), ); if (ok != true) return; try { await MedicalCertificatesApi.delete(token: widget.token, certId: id); await _reload(); } catch (e) { setState(() => error = e.toString()); } } bool _isImagePath(String p) { final x = p.toLowerCase(); return x.endsWith('.jpg') || x.endsWith('.jpeg') || x.endsWith('.png') || x.endsWith('.heic') || x.endsWith('.heif'); } bool _isPdfPath(String p) => p.toLowerCase().endsWith('.pdf'); Future _goTo(Widget page) async { if (!mounted) return; Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => page)); } void _handleBottomNav(int i) { if (i == bottomIndex) return; setState(() => bottomIndex = i); if (i == 0) { _goTo( HomePage( token: widget.token, school: widget.school, userFirstName: widget.userFirstName, ), ); return; } if (i == 1) { _goTo( LessonsPage( token: widget.token, school: widget.school, userFirstName: widget.userFirstName, ), ); return; } if (i == 2) { // siamo già in "area account", non facciamo nulla (o TODO) return; } if (i == 3) { _goTo( MeditationPage( token: widget.token, school: widget.school, userFirstName: widget.userFirstName, ), ); return; } } @override Widget build(BuildContext context) { return Scaffold( // ✅ Drawer standard (come MeditationPage) drawer: AppDrawer( token: widget.token, school: widget.school, userFirstName: widget.userFirstName, ), appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, title: const Text( 'Certificati medici', style: TextStyle(fontWeight: FontWeight.w900), ), actions: [ IconButton( onPressed: loading ? null : _reload, icon: const Icon(Icons.refresh), ), ], ), // ✅ Bottom nav standard (come MeditationPage) bottomNavigationBar: AppBottomNav( currentIndex: bottomIndex, onTap: _handleBottomNav, ), body: YogibookBackground( child: SafeArea( top: false, child: RefreshIndicator( onRefresh: _reload, child: ListView( padding: const EdgeInsets.fromLTRB(16, 14, 16, 22), children: [ // 1 SOLO bottone in alto SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: uploading ? null : _pickFromChooser, icon: const Icon(Icons.cloud_upload), label: const Text('Carica certificato'), ), ), const SizedBox(height: 12), // Preview + form (solo se file selezionato) if (pickedFile != null) ...[ _UploadCard( filename: pickedLabel.isNotEmpty ? pickedLabel : pickedFile!.path.split(Platform.pathSeparator).last, isImage: _isImagePath(pickedFile!.path), isPdf: _isPdfPath(pickedFile!.path), file: pickedFile!, documentNameController: docNameCtrl, notesController: notesCtrl, expiryDate: expiryDate, onPickExpiry: _pickExpiry, onUpload: uploading ? null : _doUpload, uploading: uploading, expiryLabel: expiryDate == null ? 'Seleziona data' : _fmtDDMMYYYY(expiryDate!), ), const SizedBox(height: 14), ], if (error.isNotEmpty) ...[ Text(error, style: const TextStyle(color: Colors.red)), const SizedBox(height: 10), ], Text( 'Certificati caricati (${certs.length})', style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 16, ), ), const SizedBox(height: 10), if (loading) const Padding( padding: EdgeInsets.only(top: 30), child: Center(child: CircularProgressIndicator()), ) else if (certs.isEmpty) const Padding( padding: EdgeInsets.only(top: 26), child: Center(child: Text('Nessun certificato caricato')), ) else ...certs.map((c) { final expired = c['is_expired'] == true; final docName = (c['document_name'] ?? '').toString(); final filename = (c['filename'] ?? '').toString(); final uploadedAt = (c['uploaded_at'] ?? '').toString(); final expiry = (c['expiry_date'] ?? '').toString(); final notes = (c['notes'] ?? '').toString(); final fileUrl = (c['file_url'] ?? '').toString(); return Card( elevation: 0, color: expired ? const Color(0xFFFFEBEE) : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( docName.isNotEmpty ? docName : 'certificato', style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 15, ), ), ), if (expired) Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFDC3545), borderRadius: BorderRadius.circular(999), ), child: const Text( 'SCADUTO', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 11, ), ), ), ], ), const SizedBox(height: 6), Text( filename, style: const TextStyle( color: Colors.black54, fontWeight: FontWeight.w600, fontSize: 12, ), ), const SizedBox(height: 10), Row( children: [ const Icon( Icons.event, size: 16, color: Colors.black54, ), const SizedBox(width: 6), Expanded( child: Text( 'Caricato: $uploadedAt', style: const TextStyle( color: Colors.black54, fontSize: 12, ), ), ), ], ), const SizedBox(height: 6), Row( children: [ const Icon( Icons.timer, size: 16, color: Colors.black54, ), const SizedBox(width: 6), Expanded( child: Text( 'Scadenza: ${expiry.isEmpty ? '—' : expiry}', style: TextStyle( color: expired ? const Color(0xFFDC3545) : Colors.black54, fontSize: 12, ), ), ), ], ), if (notes.trim().isNotEmpty) ...[ const SizedBox(height: 10), Text(notes, style: const TextStyle(fontSize: 12)), ], const SizedBox(height: 10), Row( children: [ TextButton.icon( onPressed: () => _openFileUrl(fileUrl), icon: const Icon(Icons.open_in_new), label: const Text('Apri'), ), const Spacer(), TextButton.icon( onPressed: () => _deleteCert((c['id'] as num).toInt()), icon: const Icon( Icons.delete_outline, color: Colors.red, ), label: const Text( 'Elimina', style: TextStyle(color: Colors.red), ), ), ], ), ], ), ), ); }), ], ), ), ), ), ); } } class _UploadCard extends StatelessWidget { final String filename; final bool isImage; final bool isPdf; final File file; final TextEditingController documentNameController; final TextEditingController notesController; final DateTime? expiryDate; final String expiryLabel; final VoidCallback onPickExpiry; final VoidCallback? onUpload; final bool uploading; const _UploadCard({ required this.filename, required this.isImage, required this.isPdf, required this.file, required this.documentNameController, required this.notesController, required this.expiryDate, required this.expiryLabel, required this.onPickExpiry, required this.onUpload, required this.uploading, }); @override Widget build(BuildContext context) { return Card( elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), child: Padding( padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Anteprima', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 14), ), const SizedBox(height: 10), if (isImage) ClipRRect( borderRadius: BorderRadius.circular(14), child: Image.file( file, height: 160, width: double.infinity, fit: BoxFit.cover, ), ) else Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFF6F6FB), borderRadius: BorderRadius.circular(14), ), child: Row( children: [ Icon( isPdf ? Icons.picture_as_pdf : Icons.insert_drive_file, size: 30, ), const SizedBox(width: 10), Expanded( child: Text( filename, style: const TextStyle(fontWeight: FontWeight.w700), overflow: TextOverflow.ellipsis, ), ), ], ), ), const SizedBox(height: 14), TextField( controller: documentNameController, decoration: const InputDecoration( labelText: 'Nome documento', border: OutlineInputBorder(), ), ), const SizedBox(height: 10), OutlinedButton.icon( onPressed: onPickExpiry, icon: const Icon(Icons.date_range), label: Text(expiryLabel), ), const SizedBox(height: 10), TextField( controller: notesController, minLines: 2, maxLines: 4, decoration: const InputDecoration( labelText: 'Note (opzionale)', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: onUpload, icon: uploading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.check), label: Text(uploading ? 'Caricamento...' : 'Conferma upload'), ), ), ], ), ), ); } }