syncra_addons/creche_app/lib/features/home/home_dashboard.dart

742 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fl_chart/fl_chart.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/profile.dart';
import '/models/child.dart';
import '/models/daily_access_approval.dart';
class HomeDashboard extends ConsumerWidget {
const HomeDashboard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileAsync = ref.watch(currentProfileProvider);
return profileAsync.when(
data: (profile) {
if (profile == null) {
return const Scaffold(
body: Center(child: Text('Perfil não encontrado')),
);
}
switch (profile.role) {
case 'principal':
case 'admin':
return _AdminDashboard(profile: profile);
case 'teacher':
return _TeacherDashboard(profile: profile);
case 'parent':
return _ParentDashboard(profile: profile);
default:
return const Scaffold(
body: Center(child: Text('Role desconhecido')));
}
},
loading: () =>
const Scaffold(body: Center(child: CircularProgressIndicator())),
error: (e, _) => Scaffold(body: Center(child: Text('Erro: $e'))),
);
}
}
// ─────────────── ADMIN DASHBOARD ───────────────
class _AdminDashboard extends ConsumerWidget {
final Profile profile;
const _AdminDashboard({required this.profile});
@override
Widget build(BuildContext context, WidgetRef ref) {
final supabase = Supabase.instance.client;
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
appBar: AppBar(
backgroundColor: const Color(0xFF161B22),
title: Row(
children: [
Image.asset('assets/logo.png', height: 36,
errorBuilder: (_, __, ___) =>
const Icon(Icons.child_care, color: Color(0xFF4FC3F7))),
const SizedBox(width: 10),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Sementes do Futuro',
style: TextStyle(
color: Color(0xFF4FC3F7),
fontSize: 14,
fontWeight: FontWeight.bold)),
Text('Dashboard Admin',
style:
TextStyle(color: Color(0xFF888888), fontSize: 11)),
],
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined,
color: Color(0xFF4FC3F7)),
onPressed: () => context.go('/announcements'),
),
IconButton(
icon: const Icon(Icons.settings_outlined,
color: Color(0xFF4FC3F7)),
onPressed: () => context.go('/settings'),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Olá, ${profile.fullName.split(' ').first}! 👋',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(DateFormat('EEEE, d MMMM yyyy', 'pt_PT').format(DateTime.now()),
style: const TextStyle(color: Color(0xFF888888), fontSize: 13)),
const SizedBox(height: 24),
// Quick actions
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_QuickAction(
icon: Icons.add_circle,
label: 'Diário',
onTap: () => context.go('/new-diary')),
_QuickAction(
icon: Icons.check_circle,
label: 'Presença',
onTap: () => context.go('/attendance')),
_QuickAction(
icon: Icons.attach_money,
label: 'Pagamentos',
onTap: () => context.go('/payments')),
_QuickAction(
icon: Icons.campaign,
label: 'Avisos',
onTap: () => context.go('/announcements')),
],
),
const SizedBox(height: 24),
// Cards de estatísticas
const Text('Visão Geral',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.5,
children: const [
_StatCard(title: 'Crianças Hoje', value: '', icon: Icons.child_care, color: Color(0xFF4FC3F7)),
_StatCard(title: 'Presença', value: '%', icon: Icons.check_circle, color: Color(0xFFA5D6A7)),
_StatCard(title: 'Pendentes', value: '', icon: Icons.payment, color: Color(0xFFFFCC02)),
_StatCard(title: 'Avisos', value: '', icon: Icons.campaign, color: Color(0xFFFF7043)),
],
),
const SizedBox(height: 24),
// Aprovações pendentes
const Text('Pedidos de Acesso Hoje',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
StreamBuilder<List<Map<String, dynamic>>>(
stream: supabase
.from('daily_access_approvals')
.stream(primaryKey: ['id']).eq('status', 'pending'),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xFF4FC3F7)));
}
final approvals = snapshot.data!;
if (approvals.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.check_circle, color: Color(0xFFA5D6A7)),
SizedBox(width: 8),
Text('Nenhum pedido pendente',
style: TextStyle(color: Color(0xFF888888))),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: approvals.length,
itemBuilder: (context, index) {
final approval =
DailyAccessApproval.fromMap(approvals[index]);
return _ApprovalCard(
approval: approval,
onApprove: () => _approve(supabase, approval.id),
onReject: () => _reject(supabase, approval.id),
);
},
);
},
),
const SizedBox(height: 80),
],
),
),
bottomNavigationBar: _AdminBottomNav(),
floatingActionButton: FloatingActionButton.extended(
backgroundColor: const Color(0xFF4FC3F7),
icon: const Icon(Icons.person_add, color: Colors.white),
label:
const Text('Nova Criança', style: TextStyle(color: Colors.white)),
onPressed: () => context.go('/child/new'),
),
);
}
Future<void> _approve(SupabaseClient supabase, String id) async {
await supabase.from('daily_access_approvals').update({
'status': 'approved',
'approved_at': DateTime.now().toIso8601String(),
'approved_by': supabase.auth.currentUser!.id,
}).eq('id', id);
}
Future<void> _reject(SupabaseClient supabase, String id) async {
await supabase
.from('daily_access_approvals')
.update({'status': 'rejected'}).eq('id', id);
}
}
// ─────────────── TEACHER DASHBOARD ───────────────
class _TeacherDashboard extends ConsumerWidget {
final Profile profile;
const _TeacherDashboard({required this.profile});
@override
Widget build(BuildContext context, WidgetRef ref) {
final supabase = Supabase.instance.client;
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
appBar: AppBar(
backgroundColor: const Color(0xFF161B22),
title: const Text('Minha Turma',
style: TextStyle(color: Color(0xFF4FC3F7))),
actions: [
IconButton(
icon: const Icon(Icons.chat_outlined, color: Color(0xFF4FC3F7)),
onPressed: () => context.go('/chat'),
),
IconButton(
icon:
const Icon(Icons.person_outline, color: Color(0xFF4FC3F7)),
onPressed: () => context.go('/profile'),
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Olá, ${profile.fullName.split(' ').first}! 👋',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('Crianças da tua turma hoje:',
style: TextStyle(color: Color(0xFF888888), fontSize: 14)),
),
const SizedBox(height: 12),
Expanded(
child: StreamBuilder<List<Map<String, dynamic>>>(
stream: supabase
.from('children')
.stream(primaryKey: ['id']).eq('teacher_id', profile.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child:
CircularProgressIndicator(color: Color(0xFF4FC3F7)));
}
final children =
snapshot.data!.map(Child.fromMap).toList();
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: children.length,
itemBuilder: (context, index) {
final child = children[index];
return _ChildCard(
child: child,
onTap: () => context.go('/child/${child.id}'),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
backgroundColor: const Color(0xFF4FC3F7),
icon: const Icon(Icons.add, color: Colors.white),
label:
const Text('Novo Diário', style: TextStyle(color: Colors.white)),
onPressed: () => context.go('/new-diary'),
),
bottomNavigationBar: _TeacherBottomNav(),
);
}
}
// ─────────────── PARENT DASHBOARD ───────────────
class _ParentDashboard extends ConsumerWidget {
final Profile profile;
const _ParentDashboard({required this.profile});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
appBar: AppBar(
backgroundColor: const Color(0xFF161B22),
title: Image.asset('assets/logo.png', height: 36,
errorBuilder: (_, __, ___) =>
const Icon(Icons.child_care, color: Color(0xFF4FC3F7))),
centerTitle: true,
actions: [
IconButton(
icon:
const Icon(Icons.notifications_outlined, color: Color(0xFF4FC3F7)),
onPressed: () => context.go('/announcements'),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundColor: const Color(0xFF4FC3F7),
child: Text(
profile.fullName.isNotEmpty
? profile.fullName[0].toUpperCase()
: 'P',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Bem-vindo(a)!',
style: TextStyle(
color: Color(0xFF888888), fontSize: 12)),
Text(profile.fullName,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold)),
],
),
],
),
),
const SizedBox(height: 24),
const Text('Ações Rápidas',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _ActionButton(
icon: Icons.book_outlined,
label: 'Ver Diário',
onTap: () => context.go('/children'),
),
),
const SizedBox(width: 12),
Expanded(
child: _ActionButton(
icon: Icons.restaurant_menu,
label: 'Cardápio',
onTap: () => context.go('/menu'),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _ActionButton(
icon: Icons.medication,
label: 'Medicação',
onTap: () => context.go('/medication'),
),
),
const SizedBox(width: 12),
Expanded(
child: _ActionButton(
icon: Icons.chat_outlined,
label: 'Falar c/ Educadora',
onTap: () => context.go('/chat'),
),
),
],
),
],
),
),
bottomNavigationBar: _ParentBottomNav(),
);
}
}
// ─────────────── WIDGETS AUXILIARES ───────────────
class _QuickAction extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _QuickAction(
{required this.icon, required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF333366)),
),
child: Icon(icon, color: const Color(0xFF4FC3F7), size: 26),
),
const SizedBox(height: 6),
Text(label,
style: const TextStyle(color: Color(0xFF888888), fontSize: 12)),
],
),
);
}
}
class _StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
const _StatCard(
{required this.title,
required this.value,
required this.icon,
required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 22),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(value,
style: TextStyle(
color: color,
fontSize: 20,
fontWeight: FontWeight.bold)),
),
Text(title,
style: const TextStyle(
color: Color(0xFF888888), fontSize: 10),
overflow: TextOverflow.ellipsis),
],
),
);
}
}
class _ApprovalCard extends StatelessWidget {
final DailyAccessApproval approval;
final VoidCallback onApprove;
final VoidCallback onReject;
const _ApprovalCard(
{required this.approval,
required this.onApprove,
required this.onReject});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFCC02).withOpacity(0.3)),
),
child: Row(
children: [
const Icon(Icons.person_outline, color: Color(0xFF4FC3F7)),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Funcionário: ${approval.userId.substring(0, 8)}...',
style: const TextStyle(color: Colors.white, fontSize: 13)),
Text(
'IP: ${approval.ipAddress ?? 'N/A'}${DateFormat('HH:mm').format(approval.approvalDate)}',
style: const TextStyle(
color: Color(0xFF888888), fontSize: 11)),
],
),
),
IconButton(
icon: const Icon(Icons.check_circle, color: Color(0xFFA5D6A7)),
onPressed: onApprove,
tooltip: 'Aprovar',
),
IconButton(
icon: const Icon(Icons.cancel, color: Colors.red),
onPressed: onReject,
tooltip: 'Rejeitar',
),
],
),
);
}
}
class _ChildCard extends StatelessWidget {
final Child child;
final VoidCallback onTap;
const _ChildCard({required this.child, required this.onTap});
String get _moodEmoji {
switch (child.mood) {
case 'happy': return '😊';
case 'sad': return '😟';
case 'sick': return '🤒';
case 'excited': return '😃';
default: return '😐';
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF333366)),
),
child: Row(
children: [
CircleAvatar(
radius: 26,
backgroundImage: child.photoUrl != null
? NetworkImage(child.photoUrl!)
: null,
backgroundColor: const Color(0xFF4FC3F7).withOpacity(0.2),
child: child.photoUrl == null
? const Icon(Icons.child_care,
color: Color(0xFF4FC3F7), size: 28)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(child.fullName,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15)),
Text('${child.age} anos',
style: const TextStyle(
color: Color(0xFF888888), fontSize: 12)),
],
),
),
Text(_moodEmoji, style: const TextStyle(fontSize: 28)),
const SizedBox(width: 8),
const Icon(Icons.chevron_right, color: Color(0xFF888888)),
],
),
),
);
}
}
class _ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _ActionButton(
{required this.icon, required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF333366)),
),
child: Column(
children: [
Icon(icon, color: const Color(0xFF4FC3F7), size: 28),
const SizedBox(height: 8),
Text(label,
style:
const TextStyle(color: Colors.white, fontSize: 13),
textAlign: TextAlign.center),
],
),
),
);
}
}
// Bottom Navs
class _AdminBottomNav extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
backgroundColor: const Color(0xFF161B22),
selectedItemColor: const Color(0xFF4FC3F7),
unselectedItemColor: const Color(0xFF888888),
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'),
BottomNavigationBarItem(icon: Icon(Icons.child_care), label: 'Crianças'),
BottomNavigationBarItem(icon: Icon(Icons.check_box), label: 'Presença'),
BottomNavigationBarItem(icon: Icon(Icons.people), label: 'Utilizadores'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Perfil'),
],
onTap: (i) {
final routes = ['/home', '/children', '/attendance', '/users', '/profile'];
context.go(routes[i]);
},
);
}
}
class _TeacherBottomNav extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
backgroundColor: const Color(0xFF161B22),
selectedItemColor: const Color(0xFF4FC3F7),
unselectedItemColor: const Color(0xFF888888),
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'),
BottomNavigationBarItem(icon: Icon(Icons.child_care), label: 'Crianças'),
BottomNavigationBarItem(icon: Icon(Icons.book), label: 'Diários'),
BottomNavigationBarItem(icon: Icon(Icons.chat), label: 'Chat'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Perfil'),
],
onTap: (i) {
final routes = ['/home', '/children', '/new-diary', '/chat', '/profile'];
context.go(routes[i]);
},
);
}
}
class _ParentBottomNav extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
backgroundColor: const Color(0xFF161B22),
selectedItemColor: const Color(0xFF4FC3F7),
unselectedItemColor: const Color(0xFF888888),
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'),
BottomNavigationBarItem(icon: Icon(Icons.child_care), label: 'Filhos'),
BottomNavigationBarItem(icon: Icon(Icons.restaurant_menu), label: 'Cardápio'),
BottomNavigationBarItem(icon: Icon(Icons.chat), label: 'Chat'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Perfil'),
],
onTap: (i) {
final routes = ['/home', '/children', '/menu', '/chat', '/profile'];
context.go(routes[i]);
},
);
}
}