YogiBook_App/lib/screens/medical_certificates_page.dart

713 lines
22 KiB
Dart

// 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<MedicalCertificatesPage> createState() =>
_MedicalCertificatesPageState();
}
class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
static const Color kBg = Color(0xFFF6F6FB);
bool loading = true;
String error = '';
List<Map<String, dynamic>> 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<void> _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<void> _pickFromChooser() async {
final choice = await showModalBottomSheet<String>(
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<void> _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<void> _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<void> _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<void> _deleteCert(int id) async {
final ok = await showDialog<bool>(
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<void> _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'),
),
),
],
),
),
);
}
}