Eliminar creche_app/lib/features/profile/profile_screen.dart
This commit is contained in:
parent
acf18cfdc9
commit
5cd496cd43
|
|
@ -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))));
|
||||
}
|
||||
// só 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 (só 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 — só 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue