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