Eliminar creche_app/lib/features/children/child_detail_screen.dart

This commit is contained in:
Alberto 2026-03-11 19:24:27 +00:00
parent 6afbb10fbd
commit 5434959e20
1 changed files with 0 additions and 592 deletions

View File

@ -1,592 +0,0 @@
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<ChildDetailScreen> createState() => _State();
}
class _State extends ConsumerState<ChildDetailScreen> with SingleTickerProviderStateMixin {
late TabController _tabs;
Child? _child;
bool _loading = true;
bool _isNew = false;
bool _saving = false;
final _formKey = GlobalKey<FormState>();
// 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<String> _allergyList = [];
final List<String> _foodRestList = [];
// Dropdown data from DB
List<Map<String, dynamic>> _rooms = [];
List<Profile> _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<void> _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<Map<String, dynamic>>.from(rooms);
_teachers = teachers.map((t) => Profile.fromMap(t)).toList();
});
} catch (_) {}
}
Future<void> _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<void> _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<void> _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<String>(
value: _roomId,
hint: 'Seleccionar Sala',
icon: Icons.meeting_room_outlined,
items: _rooms.map((r) => DropdownMenuItem<String>(
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<String>(
value: _teacherId,
hint: 'Seleccionar Educadora',
icon: Icons.supervisor_account_outlined,
items: _teachers.map((t) => DropdownMenuItem<String>(
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<void> _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<String> 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<List<Map<String, dynamic>>>(
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<List<Map<String, dynamic>>>(
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>({T? value, required String hint, required IconData icon,
required List<DropdownMenuItem<T>> items, required ValueChanged<T?> onChanged}) =>
DropdownButtonFormField<T>(
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<List<Map<String, dynamic>>>(
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'])),
]),
);
},
);
},
),
);
}
}