Eliminar creche_app/lib/features/profile/profile_screen.dart

This commit is contained in:
Alberto 2026-03-11 19:25:53 +00:00
parent acf18cfdc9
commit 5cd496cd43
1 changed files with 0 additions and 335 deletions

View File

@ -1,335 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:uuid/uuid.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '/core/auth_provider.dart';
import '/core/supabase_client.dart';
import '/shared/widgets/custom_button.dart';
const _bg = Color(0xFF0D1117);
const _card = Color(0xFF161B22);
const _blue = Color(0xFF4FC3F7);
String _roleLabel(String r) {
switch (r) {
case 'principal': return 'Diretora';
case 'admin': return 'Administrador';
case 'teacher': return 'Educadora';
case 'staff': return 'Auxiliar';
case 'parent': return 'Encarregado';
default: return r;
}
}
Color _roleColor(String r) {
switch (r) {
case 'principal': return const Color(0xFFFFD700);
case 'admin': return const Color(0xFFFF7043);
case 'teacher': return _blue;
case 'staff': return const Color(0xFFA5D6A7);
case 'parent': return const Color(0xFFFFB300);
default: return Colors.grey;
}
}
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
final _nameCtrl = TextEditingController();
final _phoneCtrl = TextEditingController();
final _newPassCtrl = TextEditingController();
final _confPassCtrl = TextEditingController();
bool _isSaving = false;
bool _changingPw = false;
bool _showPwForm = false;
bool _obscureNew = true;
@override
void dispose() {
_nameCtrl.dispose(); _phoneCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final profileAsync = ref.watch(currentProfileProvider);
return profileAsync.when(
data: (profile) {
if (profile == null) {
return const Scaffold(backgroundColor: _bg,
body: Center(child: Text('Perfil não encontrado', style: TextStyle(color: Colors.white))));
}
// preenche se vazio (evita reset ao rebuild)
if (_nameCtrl.text.isEmpty) _nameCtrl.text = profile.fullName;
if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = profile.phone ?? '';
final roleColor = _roleColor(profile.role);
return Scaffold(
backgroundColor: _bg,
appBar: AppBar(
backgroundColor: _card,
title: const Text('O meu perfil', style: TextStyle(color: _blue, fontWeight: FontWeight.bold)),
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(children: [
// Avatar
Center(child: Stack(children: [
Container(
width: 100, height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: roleColor.withOpacity(0.5), width: 2.5),
color: roleColor.withOpacity(0.1),
),
child: profile.avatarUrl != null
? ClipOval(child: Image.network(profile.avatarUrl!, fit: BoxFit.cover))
: Center(child: Text(
profile.fullName.isNotEmpty ? profile.fullName[0].toUpperCase() : 'U',
style: TextStyle(color: roleColor, fontSize: 38, fontWeight: FontWeight.bold))),
),
Positioned(bottom: 0, right: 0, child: GestureDetector(
onTap: () => _pickAvatar(profile.id),
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: 16),
),
)),
])),
const SizedBox(height: 12),
// Role badge ( visualização não pode mudar)
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: roleColor.withOpacity(0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: roleColor.withOpacity(0.3)),
),
child: Text(_roleLabel(profile.role).toUpperCase(),
style: TextStyle(color: roleColor, fontSize: 11, fontWeight: FontWeight.bold, letterSpacing: 1.2)),
),
const SizedBox(height: 6),
Text('A tua função é atribuída pela Diretora',
style: TextStyle(color: Colors.white.withOpacity(0.3), fontSize: 11)),
const SizedBox(height: 28),
// Dados pessoais
_Section(title: 'Dados Pessoais', children: [
_Field(ctrl: _nameCtrl, label: 'Nome completo', icon: Icons.person_outline),
const SizedBox(height: 14),
_Field(ctrl: _phoneCtrl, label: 'Telefone', icon: Icons.phone_outlined,
type: TextInputType.phone),
const SizedBox(height: 14),
// Email leitura
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.03),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.06)),
),
child: Row(children: [
Icon(Icons.alternate_email, color: _blue.withOpacity(0.5), size: 19),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('Email', style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 11)),
const SizedBox(height: 2),
Text(Supabase.instance.client.auth.currentUser?.email ?? '',
style: const TextStyle(color: Colors.white70, fontSize: 14)),
])),
const Icon(Icons.lock_outline, color: Colors.white24, size: 14),
]),
),
const SizedBox(height: 20),
CustomButton(text: 'Guardar Alterações', isLoading: _isSaving,
onPressed: () => _save(profile.id), icon: Icons.save_outlined),
]),
const SizedBox(height: 16),
// Alterar Senha
_Section(
title: 'Segurança',
trailing: TextButton(
onPressed: () => setState(() => _showPwForm = !_showPwForm),
child: Text(_showPwForm ? 'Cancelar' : 'Alterar senha',
style: const TextStyle(color: _blue, fontSize: 12)),
),
children: [
if (!_showPwForm)
Text('Podes alterar a tua senha a qualquer momento.',
style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 13))
else ...[
_Field(ctrl: _newPassCtrl, label: 'Nova senha', icon: Icons.lock_outline,
obscure: _obscureNew,
suffix: IconButton(
icon: Icon(_obscureNew ? Icons.visibility_off : Icons.visibility,
color: Colors.white38, size: 18),
onPressed: () => setState(() => _obscureNew = !_obscureNew),
)),
const SizedBox(height: 12),
_Field(ctrl: _confPassCtrl, label: 'Confirmar nova senha', icon: Icons.lock_outline,
obscure: _obscureNew),
const SizedBox(height: 16),
CustomButton(text: 'Actualizar Senha', isLoading: _changingPw,
onPressed: _changePassword, icon: Icons.security),
],
],
),
const SizedBox(height: 16),
// Sair
GestureDetector(
onTap: () async {
await ref.read(authNotifierProvider.notifier).signOut();
if (context.mounted) context.go('/login');
},
child: Container(
height: 50, width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.withOpacity(0.4)),
color: Colors.red.withOpacity(0.06),
),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.logout, color: Colors.red, size: 18),
SizedBox(width: 8),
Text('Terminar Sessão', style: TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.w500)),
]),
),
),
const SizedBox(height: 32),
]),
),
);
},
loading: () => const Scaffold(backgroundColor: _bg,
body: Center(child: CircularProgressIndicator(color: _blue))),
error: (e, _) => Scaffold(backgroundColor: _bg,
body: Center(child: Text('Erro: $e', style: const TextStyle(color: Colors.red)))),
);
}
Future<void> _pickAvatar(String profileId) async {
final picker = ImagePicker();
final file = await picker.pickImage(source: ImageSource.gallery, imageQuality: 70);
if (file == null) return;
final supabase = ref.read(supabaseProvider);
final bytes = await file.readAsBytes();
final path = 'avatars/${const Uuid().v4()}.jpg';
await supabase.storage.from('photos').uploadBinary(path, bytes);
final url = supabase.storage.from('photos').getPublicUrl(path);
await supabase.from('profiles').update({'avatar_url': url}).eq('id', profileId);
ref.invalidate(currentProfileProvider);
}
Future<void> _save(String profileId) async {
setState(() => _isSaving = true);
try {
final supabase = ref.read(supabaseProvider);
await supabase.from('profiles').update({
'full_name': _nameCtrl.text.trim(),
'phone': _phoneCtrl.text.trim(),
// NÃO inclui 'role' utilizador não pode mudar o próprio role
}).eq('id', profileId);
ref.invalidate(currentProfileProvider);
if (mounted) _snack('Perfil actualizado! ✓', ok: true);
} catch (e) {
if (mounted) _snack('Erro: $e');
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
Future<void> _changePassword() async {
final newPass = _newPassCtrl.text;
final confPass = _confPassCtrl.text;
if (newPass.length < 6) { _snack('A senha deve ter pelo menos 6 caracteres.'); return; }
if (newPass != confPass) { _snack('As senhas não coincidem.'); return; }
setState(() => _changingPw = true);
try {
await Supabase.instance.client.auth.updateUser(UserAttributes(password: newPass));
_newPassCtrl.clear();
_confPassCtrl.clear();
setState(() => _showPwForm = false);
_snack('Senha alterada com sucesso! ✓', ok: true);
} catch (e) {
_snack('Erro ao alterar senha: $e');
} finally {
if (mounted) setState(() => _changingPw = false);
}
}
void _snack(String msg, {bool ok = false}) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(msg, style: const TextStyle(color: Colors.white)),
backgroundColor: ok ? const Color(0xFF2ECC71) : Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
));
}
}
class _Section extends StatelessWidget {
final String title;
final Widget? trailing;
final List<Widget> children;
const _Section({required this.title, required this.children, this.trailing});
@override
Widget build(BuildContext context) => Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: _card, borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.07)),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Text(title, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)),
const Spacer(),
if (trailing != null) trailing!,
]),
const SizedBox(height: 14),
...children,
]),
);
}
class _Field extends StatelessWidget {
final TextEditingController ctrl;
final String label;
final IconData icon;
final bool obscure;
final TextInputType type;
final Widget? suffix;
const _Field({required this.ctrl, required this.label, required this.icon,
this.obscure = false, this.type = TextInputType.text, this.suffix});
@override
Widget build(BuildContext context) => TextField(
controller: ctrl, obscureText: obscure, keyboardType: type,
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 13),
prefixIcon: Icon(icon, color: _blue.withOpacity(0.7), size: 19),
suffixIcon: suffix,
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: 16, vertical: 14),
),
);
}