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