diff --git a/creche_app/lib/features/medication/medication_screen.dart b/creche_app/lib/features/medication/medication_screen.dart deleted file mode 100644 index 7deb777..0000000 --- a/creche_app/lib/features/medication/medication_screen.dart +++ /dev/null @@ -1,364 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:intl/intl.dart'; -import 'package:uuid/uuid.dart'; -import '/core/auth_provider.dart'; -import '/models/child.dart'; - -const _bg = Color(0xFF0D1117); -const _card = Color(0xFF161B22); -const _blue = Color(0xFF4FC3F7); -const _red = Color(0xFFE74C3C); -const _green = Color(0xFF2ECC71); -const _amber = Color(0xFFFFB300); - -class MedicationScreen extends ConsumerStatefulWidget { - const MedicationScreen({super.key}); - @override - ConsumerState createState() => _State(); -} - -class _State extends ConsumerState with SingleTickerProviderStateMixin { - late TabController _tabs; - - @override - void initState() { super.initState(); _tabs = TabController(length: 2, vsync: this); } - @override - void dispose() { _tabs.dispose(); super.dispose(); } - - @override - Widget build(BuildContext context) { - final profile = ref.watch(currentProfileProvider).valueOrNull; - final isParent = profile?.role == 'parent'; - - return Scaffold( - backgroundColor: _bg, - appBar: AppBar( - backgroundColor: _card, elevation: 0, - title: const Text('Medicação', style: TextStyle(color: _blue, fontWeight: FontWeight.bold)), - bottom: TabBar( - controller: _tabs, - indicatorColor: _blue, labelColor: _blue, - unselectedLabelColor: Colors.white38, - tabs: [ - const Tab(text: 'Activa'), - Tab(text: isParent ? 'Registar' : 'Histórico', icon: null), - ], - ), - ), - body: TabBarView(controller: _tabs, children: [ - _ActiveMeds(isParent: isParent ?? false), - isParent ? const _AddMedication() : const _MedHistory(), - ]), - floatingActionButton: isParent == true ? null : FloatingActionButton.extended( - backgroundColor: _amber, - icon: const Icon(Icons.medication, color: Colors.white), - label: const Text('Registar Toma', style: TextStyle(color: Colors.white)), - onPressed: () => _showAdministerDialog(context), - ), - ); - } - - void _showAdministerDialog(BuildContext ctx) { - showModalBottomSheet(context: ctx, isScrollControlled: true, - backgroundColor: _card, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20))), - builder: (_) => const _AdministerForm()); - } -} - -// ── Lista medicação activa ───────────────────────────────────────── -class _ActiveMeds extends ConsumerWidget { - final bool isParent; - const _ActiveMeds({required this.isParent}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sb = Supabase.instance.client; - final profile = ref.watch(currentProfileProvider).valueOrNull; - - return StreamBuilder>>( - stream: sb.from('medications').stream(primaryKey: ['id']) - .eq('active', true).order('child_id'), - builder: (context, snapshot) { - if (snapshot.hasError) return _err('Erro: ${snapshot.error}'); - if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: _blue)); - final meds = snapshot.data!; - if (meds.isEmpty) return _empty('Nenhuma medicação activa'); - - // Filtrar por encarregado se parent - return FutureBuilder>( - future: isParent ? _myChildIds(sb, profile?.id) : Future.value(null), - builder: (ctx, childIds) { - final filtered = childIds.data != null - ? meds.where((m) => childIds.data!.contains(m['child_id'])).toList() - : meds; - if (filtered.isEmpty) return _empty('Sem medicação activa para os teus filhos'); - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: filtered.length, - itemBuilder: (_, i) => _MedCard(med: filtered[i], isParent: isParent), - ); - }, - ); - }, - ); - } - - Future> _myChildIds(SupabaseClient sb, String? guardianId) async { - if (guardianId == null) return []; - final rows = await sb.from('child_guardians').select('child_id').eq('guardian_id', guardianId); - return rows.map((r) => r['child_id'] as String).toList(); - } -} - -class _MedCard extends StatelessWidget { - final Map med; - final bool isParent; - const _MedCard({required this.med, required this.isParent}); - - @override - Widget build(BuildContext context) { - final name = med['medication_name'] ?? ''; - final dose = med['dosage'] ?? ''; - final times = (med['schedule'] as List?)?.join(', ') ?? ''; - final notes = med['notes'] ?? ''; - final childName = med['child_name'] ?? 'Criança'; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: _card, borderRadius: BorderRadius.circular(16), - border: Border.all(color: _amber.withOpacity(0.3)), - boxShadow: [BoxShadow(color: _amber.withOpacity(0.05), blurRadius: 12)], - ), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(color: _amber.withOpacity(0.12), shape: BoxShape.circle), - child: const Icon(Icons.medication, color: _amber, size: 20), - ), - const SizedBox(width: 12), - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(name, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold)), - Text(childName, style: const TextStyle(color: Color(0xFF888888), fontSize: 12)), - ])), - if (!isParent) - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration(color: _green.withOpacity(0.12), borderRadius: BorderRadius.circular(20)), - child: const Text('Activa', style: TextStyle(color: _green, fontSize: 11)), - ), - ]), - const SizedBox(height: 12), - if (dose.isNotEmpty) _InfoRow(icon: Icons.scale, text: 'Dosagem: $dose'), - if (times.isNotEmpty) _InfoRow(icon: Icons.schedule, text: 'Horários: $times'), - if (notes.isNotEmpty) _InfoRow(icon: Icons.notes, text: notes), - ]), - ); - } -} - -class _InfoRow extends StatelessWidget { - final IconData icon; final String text; - const _InfoRow({required this.icon, required this.text}); - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row(children: [ - Icon(icon, size: 14, color: const Color(0xFF888888)), - const SizedBox(width: 6), - Expanded(child: Text(text, style: const TextStyle(color: Color(0xFF888888), fontSize: 12))), - ]), - ); -} - -// ── Encarregado regista medicação ────────────────────────────────── -class _AddMedication extends ConsumerStatefulWidget { - const _AddMedication(); - @override - ConsumerState<_AddMedication> createState() => _AddMedState(); -} - -class _AddMedState extends ConsumerState<_AddMedication> { - final _nameCtrl = TextEditingController(); - final _doseCtrl = TextEditingController(); - final _notesCtrl = TextEditingController(); - String? _childId; - final List _schedules = []; - final _timeCtrl = TextEditingController(); - bool _loading = false; - List _children = []; - - @override - void initState() { super.initState(); _loadChildren(); } - @override - void dispose() { _nameCtrl.dispose(); _doseCtrl.dispose(); _notesCtrl.dispose(); _timeCtrl.dispose(); super.dispose(); } - - Future _loadChildren() async { - final sb = Supabase.instance.client; - final profile = await ref.read(currentProfileProvider.future); - if (profile == null) return; - final rows = await sb.from('child_guardians').select('children(*)').eq('guardian_id', profile.id); - if (mounted) setState(() { - _children = rows.map((r) => Child.fromMap(r['children'] as Map)).toList(); - }); - } - - Future _save() async { - if (_childId == null || _nameCtrl.text.trim().isEmpty) { - _snack('Preenche o medicamento e a criança.'); return; - } - setState(() => _loading = true); - try { - final sb = Supabase.instance.client; - final profile = await ref.read(currentProfileProvider.future); - await sb.from('medications').insert({ - 'id': const Uuid().v4(), - 'child_id': _childId, - 'medication_name': _nameCtrl.text.trim(), - 'dosage': _doseCtrl.text.trim(), - 'schedule': _schedules, - 'notes': _notesCtrl.text.trim(), - 'reported_by': profile?.id, - 'active': true, - }); - _nameCtrl.clear(); _doseCtrl.clear(); _notesCtrl.clear(); - setState(() { _schedules.clear(); _childId = null; }); - _snack('Medicação registada! A equipa foi notificada.', ok: true); - } catch (e) { _snack('Erro: $e'); } - finally { if (mounted) setState(() => _loading = false); } - } - - void _snack(String msg, {bool ok = false}) => - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(msg, style: const TextStyle(color: Colors.white)), - backgroundColor: ok ? _green : _red, behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)))); - - @override - Widget build(BuildContext context) => SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sec('👶 Criança'), - DropdownButtonFormField( - value: _childId, dropdownColor: _card, - style: const TextStyle(color: Colors.white), - decoration: _dec('Selecciona o teu filho', Icons.child_care), - items: _children.map((c) => DropdownMenuItem(value: c.id, - child: Text(c.fullName, style: const TextStyle(color: Colors.white)))).toList(), - onChanged: (v) => setState(() => _childId = v), - ), - const SizedBox(height: 16), - _sec('💊 Medicamento'), - TextField(controller: _nameCtrl, style: const TextStyle(color: Colors.white), - decoration: _dec('Nome do medicamento (ex: Paracetamol)', Icons.medication)), - const SizedBox(height: 12), - TextField(controller: _doseCtrl, style: const TextStyle(color: Colors.white), - decoration: _dec('Dosagem (ex: 5ml, 1 comprimido)', Icons.scale)), - const SizedBox(height: 16), - _sec('⏰ Horários de Toma'), - Row(children: [ - Expanded(child: TextField( - controller: _timeCtrl, style: const TextStyle(color: Colors.white), - decoration: _dec('Ex: 08:00, depois do almoço', Icons.schedule), - )), - const SizedBox(width: 8), - GestureDetector( - onTap: () { if (_timeCtrl.text.trim().isNotEmpty) { - setState(() { _schedules.add(_timeCtrl.text.trim()); _timeCtrl.clear(); }); - }}, - child: Container( - padding: const EdgeInsets.all(14), - decoration: const BoxDecoration(color: _blue, shape: BoxShape.circle), - child: const Icon(Icons.add, color: Colors.white, size: 20), - ), - ), - ]), - if (_schedules.isNotEmpty) Wrap(spacing: 6, children: _schedules.asMap().entries.map((e) => - Chip(label: Text(e.value, style: const TextStyle(color: Colors.white, fontSize: 12)), - backgroundColor: _blue.withOpacity(0.2), - deleteIconColor: Colors.white54, - onDeleted: () => setState(() => _schedules.removeAt(e.key)))).toList()), - const SizedBox(height: 12), - _sec('📝 Observações (opcional)'), - TextField(controller: _notesCtrl, maxLines: 3, style: const TextStyle(color: Colors.white), - decoration: _dec('Instruções especiais, alergias, avisos...', Icons.notes)), - const SizedBox(height: 24), - GestureDetector( - onTap: _loading ? null : _save, - child: Container( - height: 52, width: double.infinity, - decoration: BoxDecoration( - gradient: const LinearGradient(colors: [_amber, Color(0xFFFF8F00)]), - borderRadius: BorderRadius.circular(14), - boxShadow: [BoxShadow(color: _amber.withOpacity(0.3), blurRadius: 16, offset: const Offset(0,6))], - ), - child: Center(child: _loading - ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) - : const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.send_outlined, color: Colors.white, size: 18), - SizedBox(width: 10), - Text('Enviar à Creche', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15)), - ])), - ), - ), - const SizedBox(height: 32), - ]), - ); - - Widget _sec(String t) => Padding(padding: const EdgeInsets.only(bottom: 8), - child: Text(t, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold))); - - InputDecoration _dec(String hint, IconData icon) => InputDecoration( - hintText: hint, hintStyle: const TextStyle(color: Color(0xFF555555), fontSize: 13), - prefixIcon: Icon(icon, color: _blue.withOpacity(0.6), size: 18), - filled: true, fillColor: Colors.white.withOpacity(0.04), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.white.withOpacity(0.09))), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: _blue, width: 1.5)), - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - ); -} - -// ── Histórico (staff) ────────────────────────────────────────────── -class _MedHistory extends StatelessWidget { - const _MedHistory(); - @override - Widget build(BuildContext context) { - final sb = Supabase.instance.client; - return StreamBuilder>>( - stream: sb.from('medications').stream(primaryKey: ['id']).order('created_at', ascending: false), - builder: (ctx, snap) { - if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: _blue)); - if (snap.data!.isEmpty) return _empty('Sem registos de medicação'); - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: snap.data!.length, - itemBuilder: (_, i) => _MedCard(med: snap.data![i], isParent: false), - ); - }, - ); - } -} - -class _AdministerForm extends StatelessWidget { - const _AdministerForm(); - @override - Widget build(BuildContext context) => const Padding( - padding: EdgeInsets.all(20), - child: Text('Formulário de toma (em breve)', style: TextStyle(color: Colors.white)), - ); -} - -Widget _empty(String msg) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.medication_outlined, size: 60, color: Colors.white.withOpacity(0.1)), - const SizedBox(height: 12), - Text(msg, style: const TextStyle(color: Color(0xFF888888), fontSize: 13)), -])); - -Widget _err(String msg) => Center(child: Text(msg, style: const TextStyle(color: Colors.red)));