393 lines
17 KiB
Dart
393 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../core/auth_provider.dart';
|
|
import '../../models/child.dart';
|
|
|
|
const _bg = Color(0xFF0D1117);
|
|
const _card = Color(0xFF161B22);
|
|
const _blue = Color(0xFF4FC3F7);
|
|
const _green = Color(0xFF2ECC71);
|
|
const _amber = Color(0xFFFFB300);
|
|
const _red = Color(0xFFE74C3C);
|
|
|
|
// Opções de refeição
|
|
const _mealOpts = ['bem', 'pouco', 'nao_aceita'];
|
|
const _mealLabels = {'bem': '😊 Bem', 'pouco': '😐 Pouco', 'nao_aceita': '😞 Não aceita'};
|
|
const _hygieneOpts = ['normal', 'diarreia', 'rastoso'];
|
|
const _hygieneLabels = {'normal': '✅ Normal', 'diarreia': '⚠️ Diarreia', 'rastoso': '😷 Rastoso'};
|
|
|
|
class NewDiaryScreen extends ConsumerStatefulWidget {
|
|
final String? childId;
|
|
const NewDiaryScreen({super.key, this.childId});
|
|
@override
|
|
ConsumerState<NewDiaryScreen> createState() => _State();
|
|
}
|
|
|
|
class _State extends ConsumerState<NewDiaryScreen> {
|
|
final _actCtrl = TextEditingController();
|
|
final _notesCtrl = TextEditingController();
|
|
final _instNotesCtrl = TextEditingController(); // notas da instituição
|
|
String? _childId;
|
|
List<Child> _children = [];
|
|
bool _loading = false;
|
|
bool _loadingChildren = true;
|
|
|
|
// Sono
|
|
bool _sleepMorning = false;
|
|
bool _sleepAfternoon = false;
|
|
|
|
// Alimentação
|
|
String _breakfast = '';
|
|
String _lunch = '';
|
|
String _snackMeal = '';
|
|
|
|
// Higiene
|
|
int _hygieneFreq = 0;
|
|
String _hygieneState = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_childId = widget.childId;
|
|
_loadChildren();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_actCtrl.dispose(); _notesCtrl.dispose(); _instNotesCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadChildren() async {
|
|
try {
|
|
final sb = Supabase.instance.client;
|
|
final data = await sb.from('children').select().order('full_name');
|
|
setState(() {
|
|
_children = data.map((d) => Child.fromMap(d)).toList();
|
|
_loadingChildren = false;
|
|
});
|
|
} catch (_) { setState(() => _loadingChildren = false); }
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
if (_childId == null) { _snack('Selecciona uma criança.'); return; }
|
|
if (_actCtrl.text.trim().isEmpty) { _snack('Descreve as actividades do dia.'); return; }
|
|
setState(() => _loading = true);
|
|
|
|
try {
|
|
final sb = Supabase.instance.client;
|
|
final profile = await ref.read(currentProfileProvider.future);
|
|
if (profile == null) throw Exception('Perfil não encontrado');
|
|
final today = DateTime.now().toIso8601String().split('T')[0];
|
|
|
|
// 1. Criar/actualizar diário
|
|
final existing = await sb.from('daily_diaries').select('id')
|
|
.eq('child_id', _childId!).eq('date', today).maybeSingle();
|
|
|
|
String diaryId;
|
|
if (existing != null) {
|
|
diaryId = existing['id'] as String;
|
|
await sb.from('daily_diaries').update({
|
|
'activities': _actCtrl.text.trim(),
|
|
'notes': _notesCtrl.text.trim(),
|
|
'institution_notes': _instNotesCtrl.text.trim(),
|
|
'teacher_id': profile.id,
|
|
}).eq('id', diaryId);
|
|
} else {
|
|
final res = await sb.from('daily_diaries').insert({
|
|
'child_id': _childId,
|
|
'teacher_id': profile.id,
|
|
'date': today,
|
|
'activities': _actCtrl.text.trim(),
|
|
'notes': _notesCtrl.text.trim(),
|
|
'institution_notes': _instNotesCtrl.text.trim(),
|
|
}).select('id').single();
|
|
diaryId = res['id'] as String;
|
|
}
|
|
|
|
// 2. Sono
|
|
await sb.from('sleep_records').upsert({
|
|
'child_id': _childId,
|
|
'diary_id': diaryId,
|
|
'date': today,
|
|
'morning': _sleepMorning,
|
|
'afternoon': _sleepAfternoon,
|
|
}, onConflict: 'child_id,date');
|
|
|
|
// 3. Alimentação
|
|
if (_breakfast.isNotEmpty || _lunch.isNotEmpty || _snackMeal.isNotEmpty) {
|
|
await sb.from('meal_records').upsert({
|
|
'child_id': _childId,
|
|
'diary_id': diaryId,
|
|
'date': today,
|
|
'breakfast': _breakfast,
|
|
'lunch': _lunch,
|
|
'snack': _snackMeal,
|
|
}, onConflict: 'child_id,date');
|
|
}
|
|
|
|
// 4. Higiene
|
|
if (_hygieneFreq > 0 || _hygieneState.isNotEmpty) {
|
|
await sb.from('hygiene_records').upsert({
|
|
'child_id': _childId,
|
|
'diary_id': diaryId,
|
|
'date': today,
|
|
'frequency': _hygieneFreq,
|
|
'state': _hygieneState,
|
|
}, onConflict: 'child_id,date');
|
|
}
|
|
|
|
if (mounted) {
|
|
_snack('Diário guardado! ✓', ok: true);
|
|
await Future.delayed(const Duration(milliseconds: 600));
|
|
if (mounted) context.pop();
|
|
}
|
|
} catch (e) {
|
|
_snack('Erro: $e');
|
|
} finally {
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
void _snack(String msg, {bool ok = false}) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text(msg, style: const TextStyle(color: Colors.white)),
|
|
backgroundColor: ok ? _green : _red, behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final today = DateFormat('d MMMM yyyy', 'pt_PT').format(DateTime.now());
|
|
|
|
return Scaffold(
|
|
backgroundColor: _bg,
|
|
appBar: AppBar(
|
|
backgroundColor: _card, elevation: 0,
|
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
const Text('Diário do Dia', style: TextStyle(color: _blue, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
Text(today, style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 11)),
|
|
]),
|
|
actions: [
|
|
TextButton.icon(
|
|
icon: _loading ? const SizedBox(width: 16, height: 16,
|
|
child: CircularProgressIndicator(color: _blue, strokeWidth: 2))
|
|
: const Icon(Icons.save_outlined, color: _blue, size: 18),
|
|
label: const Text('Guardar', style: TextStyle(color: _blue, fontWeight: FontWeight.bold)),
|
|
onPressed: _loading ? null : _save,
|
|
),
|
|
],
|
|
),
|
|
body: _loadingChildren
|
|
? const Center(child: CircularProgressIndicator(color: _blue))
|
|
: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(children: [
|
|
|
|
// ── Seleccionar criança ─────────────────────────
|
|
_Card(title: '👶 Criança', children: [
|
|
DropdownButtonFormField<String>(
|
|
value: _childId,
|
|
dropdownColor: _card,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: _dec('Selecciona a criança', Icons.child_care),
|
|
items: _children.map((c) => DropdownMenuItem(
|
|
value: c.id,
|
|
child: Text(c.fullName, style: const TextStyle(color: Colors.white)),
|
|
)).toList(),
|
|
onChanged: (v) => setState(() => _childId = v),
|
|
),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Actividades ─────────────────────────────────
|
|
_Card(title: '🎨 Actividades do Dia', children: [
|
|
TextField(
|
|
controller: _actCtrl, maxLines: 4,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
decoration: _dec('Descreve as actividades, brincadeiras, aprendizagens...', Icons.edit_note),
|
|
),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Controlo de Sono ────────────────────────────
|
|
_Card(title: '😴 Controlo de Sono', children: [
|
|
Row(children: [
|
|
Expanded(child: _CheckTile(
|
|
label: 'Manhã', value: _sleepMorning,
|
|
onChanged: (v) => setState(() => _sleepMorning = v),
|
|
)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _CheckTile(
|
|
label: 'Tarde', value: _sleepAfternoon,
|
|
onChanged: (v) => setState(() => _sleepAfternoon = v),
|
|
)),
|
|
]),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Alimentação ─────────────────────────────────
|
|
_Card(title: '🍽️ Alimentação', children: [
|
|
_MealRow(label: 'Pequeno Almoço', value: _breakfast,
|
|
onChanged: (v) => setState(() => _breakfast = v)),
|
|
const SizedBox(height: 10),
|
|
_MealRow(label: 'Almoço', value: _lunch,
|
|
onChanged: (v) => setState(() => _lunch = v)),
|
|
const SizedBox(height: 10),
|
|
_MealRow(label: 'Lanche', value: _snackMeal,
|
|
onChanged: (v) => setState(() => _snackMeal = v)),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Higiene/Evacuação ───────────────────────────
|
|
_Card(title: '🚿 Higiene & Evacuação', children: [
|
|
Row(children: [
|
|
const Text('Frequência:', style: TextStyle(color: Color(0xFF888888), fontSize: 13)),
|
|
const SizedBox(width: 12),
|
|
IconButton(
|
|
onPressed: () => setState(() => _hygieneFreq = (_hygieneFreq - 1).clamp(0, 20)),
|
|
icon: const Icon(Icons.remove_circle_outline, color: _red),
|
|
),
|
|
Text('$_hygieneFreq x',
|
|
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
|
IconButton(
|
|
onPressed: () => setState(() => _hygieneFreq++),
|
|
icon: const Icon(Icons.add_circle_outline, color: _green),
|
|
),
|
|
]),
|
|
const SizedBox(height: 10),
|
|
const Text('Estado:', style: TextStyle(color: Color(0xFF888888), fontSize: 13)),
|
|
const SizedBox(height: 8),
|
|
Wrap(spacing: 8, children: _hygieneOpts.map((opt) {
|
|
final sel = _hygieneState == opt;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _hygieneState = sel ? '' : opt),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: sel ? _blue.withOpacity(0.2) : Colors.white.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: sel ? _blue : Colors.white.withOpacity(0.1)),
|
|
),
|
|
child: Text(_hygieneLabels[opt]!,
|
|
style: TextStyle(color: sel ? _blue : Colors.white70, fontSize: 13)),
|
|
),
|
|
);
|
|
}).toList()),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Notas da Educadora ──────────────────────────
|
|
_Card(title: '📝 Notas da Educadora', children: [
|
|
TextField(
|
|
controller: _notesCtrl, maxLines: 3,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
decoration: _dec('Observações, comportamento, necessidades especiais...', Icons.notes),
|
|
),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Notas da Instituição ────────────────────────
|
|
_Card(title: '🏫 Notas da Instituição', children: [
|
|
TextField(
|
|
controller: _instNotesCtrl, maxLines: 3,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
decoration: _dec('Comunicado para o encarregado de educação...', Icons.business_outlined),
|
|
),
|
|
]),
|
|
const SizedBox(height: 80),
|
|
]),
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
backgroundColor: _blue,
|
|
onPressed: _loading ? null : _save,
|
|
icon: _loading ? const SizedBox(width: 18, height: 18,
|
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
: const Icon(Icons.save, color: Colors.white),
|
|
label: const Text('Guardar Diário', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
),
|
|
);
|
|
}
|
|
|
|
InputDecoration _dec(String hint, IconData icon) => InputDecoration(
|
|
hintText: hint, hintStyle: const TextStyle(color: Color(0xFF555555), fontSize: 13),
|
|
prefixIcon: Icon(icon, color: _blue.withOpacity(0.6), size: 18),
|
|
filled: true, fillColor: Colors.white.withOpacity(0.04),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.white.withOpacity(0.09))),
|
|
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: _blue, width: 1.5)),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
);
|
|
}
|
|
|
|
class _Card extends StatelessWidget {
|
|
final String title; final List<Widget> children;
|
|
const _Card({required this.title, required this.children});
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.white.withOpacity(0.07))),
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(title, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 12),
|
|
...children,
|
|
]),
|
|
);
|
|
}
|
|
|
|
class _CheckTile extends StatelessWidget {
|
|
final String label; final bool value; final ValueChanged<bool> onChanged;
|
|
const _CheckTile({required this.label, required this.value, required this.onChanged});
|
|
@override
|
|
Widget build(BuildContext context) => GestureDetector(
|
|
onTap: () => onChanged(!value),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: value ? _blue.withOpacity(0.12) : Colors.white.withOpacity(0.04),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: value ? _blue.withOpacity(0.4) : Colors.white.withOpacity(0.09)),
|
|
),
|
|
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
Icon(value ? Icons.check_circle : Icons.circle_outlined,
|
|
color: value ? _blue : Colors.white38, size: 18),
|
|
const SizedBox(width: 8),
|
|
Text(label, style: TextStyle(color: value ? _blue : Colors.white60, fontWeight: FontWeight.w500)),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _MealRow extends StatelessWidget {
|
|
final String label, value; final ValueChanged<String> onChanged;
|
|
const _MealRow({required this.label, required this.value, required this.onChanged});
|
|
@override
|
|
Widget build(BuildContext context) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(label, style: const TextStyle(color: Color(0xFF888888), fontSize: 12)),
|
|
const SizedBox(height: 6),
|
|
Row(children: _mealOpts.map((opt) {
|
|
final sel = value == opt;
|
|
Color c = opt == 'bem' ? const Color(0xFF2ECC71) : opt == 'pouco' ? const Color(0xFFFFB300) : const Color(0xFFE74C3C);
|
|
return Expanded(child: GestureDetector(
|
|
onTap: () => onChanged(sel ? '' : opt),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(right: 6),
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: sel ? c.withOpacity(0.15) : Colors.white.withOpacity(0.04),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: sel ? c.withOpacity(0.5) : Colors.white.withOpacity(0.08)),
|
|
),
|
|
child: Text(_mealLabels[opt]!, textAlign: TextAlign.center,
|
|
style: TextStyle(color: sel ? c : Colors.white54, fontSize: 11, fontWeight: FontWeight.w500)),
|
|
),
|
|
));
|
|
}).toList()),
|
|
]);
|
|
}
|