Eliminar creche_app/lib/features/children/child_detail_screen.dart
This commit is contained in:
parent
6afbb10fbd
commit
5434959e20
|
|
@ -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'])),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue