import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; import 'package:go_router/go_router.dart'; import '/core/supabase_client.dart'; import '/models/child.dart'; import '/models/profile.dart'; import '/shared/widgets/custom_button.dart'; import '/core/auth_provider.dart'; const _bg = Color(0xFF0D1117); const _card = Color(0xFF161B22); const _blue = Color(0xFF4FC3F7); const _green = Color(0xFF2ECC71); const _red = Color(0xFFE74C3C); const _amber = Color(0xFFFFB300); class ChildDetailScreen extends ConsumerStatefulWidget { final String id; const ChildDetailScreen({super.key, required this.id}); @override ConsumerState createState() => _State(); } class _State extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabs; Child? _child; bool _loading = true; bool _isNew = false; bool _saving = false; final _formKey = GlobalKey(); // Profile tab final _firstCtrl = TextEditingController(); final _lastCtrl = TextEditingController(); DateTime _birth = DateTime.now().subtract(const Duration(days: 730)); String? _photoUrl; String? _classId; String? _teacherId; String? _roomId; // Health tab final _allergyCtrl = TextEditingController(); final _foodRestCtrl = TextEditingController(); final _medicalNotesCtrl = TextEditingController(); final List _allergyList = []; final List _foodRestList = []; // Dropdown data from DB List> _rooms = []; List _teachers = []; @override void initState() { super.initState(); _tabs = TabController(length: 4, vsync: this); _isNew = widget.id == 'new'; _loadDropdowns(); if (!_isNew) _loadChild(); else setState(() => _loading = false); } @override void dispose() { _tabs.dispose(); _firstCtrl.dispose(); _lastCtrl.dispose(); _allergyCtrl.dispose(); _foodRestCtrl.dispose(); _medicalNotesCtrl.dispose(); super.dispose(); } Future _loadDropdowns() async { final sb = ref.read(supabaseProvider); try { final rooms = await sb.from('rooms').select().order('name'); final teachers = await sb.from('profiles').select().inFilter('role', ['teacher','staff']).order('full_name'); if (mounted) setState(() { _rooms = List>.from(rooms); _teachers = teachers.map((t) => Profile.fromMap(t)).toList(); }); } catch (_) {} } Future _loadChild() async { final sb = ref.read(supabaseProvider); try { final data = await sb.from('children').select().eq('id', widget.id).single(); final child = Child.fromMap(data); setState(() { _child = child; _firstCtrl.text = child.firstName; _lastCtrl.text = child.lastName; _birth = child.birthDate; _photoUrl = child.photoUrl; _classId = child.classId.isEmpty ? null : child.classId; _teacherId = child.teacherId.isEmpty ? null : child.teacherId; _roomId = child.roomId; // Parse allergies if (child.allergies != null && child.allergies!.isNotEmpty) { _allergyList.addAll(child.allergies!.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty)); } if (child.foodRestrictions != null && child.foodRestrictions!.isNotEmpty) { _foodRestList.addAll(child.foodRestrictions!.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty)); } _loading = false; }); } catch (e) { if (mounted) { _snack('Erro ao carregar: $e'); setState(() => _loading = false); } } } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); final sb = ref.read(supabaseProvider); try { final data = { 'first_name': _firstCtrl.text.trim(), 'last_name': _lastCtrl.text.trim(), 'birth_date': _birth.toIso8601String().split('T')[0], 'photo_url': _photoUrl, 'class_id': _classId ?? '', 'teacher_id': _teacherId ?? '', 'room_id': _roomId, 'status': 'active', 'allergies': _allergyList.join(', '), 'food_restrictions': _foodRestList.join(', '), }; if (_isNew) { await sb.from('children').insert(data); } else { await sb.from('children').update(data).eq('id', widget.id); } if (mounted) { _snack('Guardado! ✓', ok: true); await Future.delayed(const Duration(milliseconds: 500)); if (mounted) context.go('/children'); } } catch (e) { if (mounted) _snack('Erro: $e'); } finally { if (mounted) setState(() => _saving = false); } } Future _pickPhoto() async { final img = await ImagePicker().pickImage(source: ImageSource.gallery, imageQuality: 70); if (img == null) return; final sb = ref.read(supabaseProvider); final bytes = await img.readAsBytes(); final path = 'children/${const Uuid().v4()}.jpg'; await sb.storage.from('photos').uploadBinary(path, bytes); final url = sb.storage.from('photos').getPublicUrl(path); setState(() => _photoUrl = url); } 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) { if (_loading) return const Scaffold(backgroundColor: _bg, body: Center(child: CircularProgressIndicator(color: _blue))); return Scaffold( backgroundColor: _bg, appBar: AppBar( backgroundColor: _card, elevation: 0, title: Text(_isNew ? 'Nova Criança' : (_child?.fullName ?? 'Criança'), style: const TextStyle(color: _blue, fontWeight: FontWeight.bold)), bottom: _isNew ? null : TabBar( controller: _tabs, indicatorColor: _blue, labelColor: _blue, unselectedLabelColor: Colors.white38, isScrollable: true, tabAlignment: TabAlignment.start, tabs: const [Tab(text: 'Perfil'), Tab(text: 'Saúde'), Tab(text: 'Diário'), Tab(text: 'Presença')], ), ), body: _isNew ? Form(key: _formKey, child: _buildProfileForm()) : TabBarView(controller: _tabs, children: [ Form(key: _formKey, child: _buildProfileForm()), _buildHealthTab(), _buildDiaryTab(), _buildAttendanceTab(), ]), ); } // ── ABA PERFIL ───────────────────────────────────────────────── Widget _buildProfileForm() => SingleChildScrollView( padding: const EdgeInsets.all(18), child: Column(children: [ // Foto Center(child: Stack(children: [ Container(width: 100, height: 100, decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: _blue.withOpacity(0.3), width: 2), color: _blue.withOpacity(0.08)), child: _photoUrl != null ? ClipOval(child: Image.network(_photoUrl!, fit: BoxFit.cover)) : const Icon(Icons.child_care, size: 50, color: _blue), ), Positioned(bottom: 0, right: 0, child: GestureDetector( onTap: _pickPhoto, child: Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration(color: _blue, shape: BoxShape.circle, border: Border.all(color: _bg, width: 2)), child: const Icon(Icons.camera_alt, color: Colors.white, size: 15), ), )), ])), const SizedBox(height: 22), _field(_firstCtrl, 'Nome', Icons.person_outline, req: true), const SizedBox(height: 12), _field(_lastCtrl, 'Sobrenome', Icons.person, req: true), const SizedBox(height: 12), // Data de nascimento GestureDetector( onTap: () async { final d = await showDatePicker(context: context, initialDate: _birth, firstDate: DateTime(2015), lastDate: DateTime.now(), builder: (ctx, child) => Theme( data: ThemeData.dark().copyWith(colorScheme: const ColorScheme.dark(primary: _blue)), child: child!)); if (d != null) setState(() => _birth = d); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration(color: Colors.white.withOpacity(0.04), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withOpacity(0.09))), child: Row(children: [ Icon(Icons.cake, color: _blue.withOpacity(0.7), size: 19), const SizedBox(width: 12), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Data de Nascimento', style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 11)), Text(DateFormat('dd/MM/yyyy').format(_birth), style: const TextStyle(color: Colors.white, fontSize: 14)), ]), const Spacer(), Icon(Icons.edit_calendar, color: Colors.white.withOpacity(0.2), size: 16), ]), ), ), const SizedBox(height: 12), // Sala (do DB) _dropdown( value: _roomId, hint: 'Seleccionar Sala', icon: Icons.meeting_room_outlined, items: _rooms.map((r) => DropdownMenuItem( value: r['id'] as String, child: Text(r['name'] ?? '', style: const TextStyle(color: Colors.white)))).toList(), onChanged: (v) => setState(() => _roomId = v), ), const SizedBox(height: 12), // Educadora (do DB) _dropdown( value: _teacherId, hint: 'Seleccionar Educadora', icon: Icons.supervisor_account_outlined, items: _teachers.map((t) => DropdownMenuItem( value: t.id, child: Text(t.fullName, style: const TextStyle(color: Colors.white)))).toList(), onChanged: (v) => setState(() => _teacherId = v), ), const SizedBox(height: 28), CustomButton(text: _isNew ? 'Criar Criança' : 'Guardar', isLoading: _saving, onPressed: _save, icon: Icons.save_outlined), const SizedBox(height: 20), ]), ); // ── ABA SAÚDE ────────────────────────────────────────────────── Widget _buildHealthTab() => SingleChildScrollView( padding: const EdgeInsets.all(18), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ _healthCard( title: '⚠️ Alergias', color: _red, chips: _allergyList, ctrl: _allergyCtrl, hint: 'Ex: Amendoim, Leite, Glúten...', onAdd: () { if (_allergyCtrl.text.trim().isNotEmpty) { setState(() { _allergyList.add(_allergyCtrl.text.trim()); _allergyCtrl.clear(); }); _saveHealthData(); }}, onRemove: (i) { setState(() => _allergyList.removeAt(i)); _saveHealthData(); }, ), const SizedBox(height: 14), _healthCard( title: '🚫 Alimentos Não Permitidos', color: _amber, chips: _foodRestList, ctrl: _foodRestCtrl, hint: 'Ex: Carne de porco, frutos do mar...', onAdd: () { if (_foodRestCtrl.text.trim().isNotEmpty) { setState(() { _foodRestList.add(_foodRestCtrl.text.trim()); _foodRestCtrl.clear(); }); _saveHealthData(); }}, onRemove: (i) { setState(() => _foodRestList.removeAt(i)); _saveHealthData(); }, ), const SizedBox(height: 14), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withOpacity(0.07))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('📋 Observações Médicas', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)), const SizedBox(height: 10), TextField(controller: _medicalNotesCtrl, maxLines: 4, style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( hintText: 'Condições médicas, medicação habitual, contacto de emergência...', hintStyle: const TextStyle(color: Color(0xFF555555), fontSize: 12), filled: true, fillColor: Colors.white.withOpacity(0.04), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.white.withOpacity(0.09))), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), )), const SizedBox(height: 10), CustomButton(text: 'Guardar Observações', onPressed: _saveHealthData, icon: Icons.save_outlined), ]), ), const SizedBox(height: 14), // Link para medicação GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute( builder: (_) => _MedQuickView(childId: widget.id, childName: _child?.fullName ?? ''))), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: _amber.withOpacity(0.07), borderRadius: BorderRadius.circular(14), border: Border.all(color: _amber.withOpacity(0.3)), ), child: const Row(children: [ Icon(Icons.medication, color: _amber), SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Medicação', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), Text('Ver / gerir medicação activa desta criança', style: TextStyle(color: Color(0xFF888888), fontSize: 11)), ])), Icon(Icons.chevron_right, color: _amber), ]), ), ), ]), ); Future _saveHealthData() async { if (_isNew || widget.id.isEmpty) return; final sb = ref.read(supabaseProvider); try { await sb.from('children').update({ 'allergies': _allergyList.join(', '), 'food_restrictions': _foodRestList.join(', '), }).eq('id', widget.id); } catch (_) {} } Widget _healthCard({ required String title, required Color color, required List chips, required TextEditingController ctrl, required String hint, required VoidCallback onAdd, required Function(int) onRemove, }) => Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: color.withOpacity(0.2))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)), const SizedBox(height: 10), if (chips.isEmpty) Text('Nenhum registo', style: TextStyle(color: Colors.white.withOpacity(0.25), fontSize: 12)) else Wrap(spacing: 6, runSpacing: 4, children: chips.asMap().entries.map((e) => Chip( label: Text(e.value, style: const TextStyle(color: Colors.white, fontSize: 12)), backgroundColor: color.withOpacity(0.12), deleteIconColor: color.withOpacity(0.6), side: BorderSide(color: color.withOpacity(0.3)), onDeleted: () => onRemove(e.key), )).toList()), const SizedBox(height: 10), Row(children: [ Expanded(child: TextField( controller: ctrl, style: const TextStyle(color: Colors.white, fontSize: 13), onSubmitted: (_) => onAdd(), decoration: InputDecoration( hintText: hint, hintStyle: const TextStyle(color: Color(0xFF555555), fontSize: 12), filled: true, fillColor: Colors.white.withOpacity(0.04), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.white.withOpacity(0.09))), ), )), const SizedBox(width: 8), GestureDetector( onTap: onAdd, child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: color.withOpacity(0.15), shape: BoxShape.circle, border: Border.all(color: color.withOpacity(0.3))), child: Icon(Icons.add, color: color, size: 18), ), ), ]), ]), ); // ── ABA DIÁRIO ───────────────────────────────────────────────── Widget _buildDiaryTab() { final sb = ref.read(supabaseProvider); return FutureBuilder>>( future: sb.from('daily_diaries').select() .eq('child_id', widget.id).order('date', ascending: false).limit(20), builder: (ctx, snap) { if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: _blue)); if (snap.data!.isEmpty) return _empty('Sem entradas no diário'); return ListView.builder( padding: const EdgeInsets.all(14), itemCount: snap.data!.length, itemBuilder: (_, i) { final d = snap.data![i]; final date = DateTime.tryParse(d['date'] ?? '') ?? DateTime.now(); return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withOpacity(0.07))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.book_outlined, color: _blue, size: 16), const SizedBox(width: 6), Text(DateFormat('EEEE, d MMM yyyy', 'pt_PT').format(date), style: const TextStyle(color: _blue, fontWeight: FontWeight.bold, fontSize: 13)), ]), if ((d['activities'] ?? '').isNotEmpty) ...[ const SizedBox(height: 6), Text(d['activities'], style: const TextStyle(color: Colors.white70, fontSize: 13)), ], if ((d['institution_notes'] ?? '').isNotEmpty) ...[ const SizedBox(height: 6), Row(children: [ const Icon(Icons.business_outlined, size: 12, color: _amber), const SizedBox(width: 4), Expanded(child: Text(d['institution_notes'], style: const TextStyle(color: _amber, fontSize: 12))), ]), ], ]), ); }, ); }, ); } // ── ABA PRESENÇA ─────────────────────────────────────────────── Widget _buildAttendanceTab() { final sb = ref.read(supabaseProvider); return FutureBuilder>>( future: sb.from('attendance').select() .eq('child_id', widget.id).order('date', ascending: false).limit(30), builder: (ctx, snap) { if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: _blue)); if (snap.data!.isEmpty) return _empty('Sem registos de presença'); return ListView.builder( padding: const EdgeInsets.all(14), itemCount: snap.data!.length, itemBuilder: (_, i) { final a = snap.data![i]; final present = a['present'] as bool? ?? false; final date = DateTime.tryParse(a['date'] ?? '') ?? DateTime.now(); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(12), border: Border.all(color: present ? _green.withOpacity(0.2) : _red.withOpacity(0.2))), child: Row(children: [ Icon(present ? Icons.check_circle : Icons.cancel, color: present ? _green : _red, size: 20), const SizedBox(width: 10), Text(DateFormat('EEEE, d/MM/yyyy', 'pt_PT').format(date), style: const TextStyle(color: Colors.white, fontSize: 13)), const Spacer(), Text(present ? 'Presente' : 'Ausente', style: TextStyle(color: present ? _green : _red, fontSize: 12)), ]), ); }, ); }, ); } // ── HELPERS ──────────────────────────────────────────────────── Widget _field(TextEditingController c, String label, IconData icon, {bool req = false}) => TextFormField( controller: c, style: const TextStyle(color: Colors.white, fontSize: 14), validator: req ? (v) => (v?.trim().isEmpty ?? true) ? 'Obrigatório' : null : null, decoration: InputDecoration( labelText: label, labelStyle: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 13), prefixIcon: Icon(icon, color: _blue.withOpacity(0.7), size: 19), 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)), errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _red)), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), ); Widget _dropdown({T? value, required String hint, required IconData icon, required List> items, required ValueChanged onChanged}) => DropdownButtonFormField( value: value, dropdownColor: _card, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle(color: Colors.white.withOpacity(0.3), fontSize: 13), prefixIcon: Icon(icon, color: _blue.withOpacity(0.7), size: 19), filled: true, fillColor: Colors.white.withOpacity(0.04), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.white.withOpacity(0.09))), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ), items: items, onChanged: onChanged, ); Widget _empty(String msg) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline, size: 48, color: Colors.white.withOpacity(0.1)), const SizedBox(height: 10), Text(msg, style: const TextStyle(color: Color(0xFF888888), fontSize: 13)), ])); } // ── Quick view medicação ligada à criança ────────────────────────── class _MedQuickView extends StatelessWidget { final String childId, childName; const _MedQuickView({required this.childId, required this.childName}); @override Widget build(BuildContext context) { final sb = Supabase.instance.client; return Scaffold( backgroundColor: _bg, appBar: AppBar(backgroundColor: _card, elevation: 0, title: Text('Medicação — $childName', style: const TextStyle(color: _amber, fontSize: 15))), body: StreamBuilder>>( stream: sb.from('medications').stream(primaryKey: ['id']).eq('child_id', childId), builder: (ctx, snap) { if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: _blue)); if (snap.data!.isEmpty) return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.medication_outlined, size: 50, color: Color(0xFF333333)), const SizedBox(height: 10), const Text('Sem medicação registada', style: TextStyle(color: Color(0xFF888888))), ])); return ListView.builder( padding: const EdgeInsets.all(14), itemCount: snap.data!.length, itemBuilder: (_, i) { final m = snap.data![i]; final active = m['active'] as bool? ?? false; return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: active ? _amber.withOpacity(0.3) : Colors.white.withOpacity(0.06))), child: Row(children: [ Icon(Icons.medication, color: active ? _amber : Colors.white38), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(m['medication_name'] ?? '', style: TextStyle( color: active ? Colors.white : Colors.white38, fontWeight: FontWeight.bold)), if ((m['dosage'] ?? '').isNotEmpty) Text(m['dosage'], style: const TextStyle(color: Color(0xFF888888), fontSize: 12)), ])), Switch(value: active, activeColor: _amber, onChanged: (v) => sb.from('medications').update({'active': v}).eq('id', m['id'])), ]), ); }, ); }, ), ); } }