From 38defe1060cd58615d88c06c3591ea2ca6c480fe Mon Sep 17 00:00:00 2001 From: marcoitaloesp-ai Date: Wed, 17 Dec 2025 10:22:04 +0000 Subject: [PATCH] =?UTF-8?q?v1.47.0=20-=20P=C3=A1gina=20Meu=20Perfil:=20edi?= =?UTF-8?q?tar=20nome,=20email=20e=20senha?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 16 + VERSION | 2 +- .../Http/Controllers/Api/AuthController.php | 81 +++++ backend/routes/api.php | 1 + frontend/src/App.jsx | 11 + frontend/src/components/Layout.jsx | 1 + frontend/src/i18n/locales/en.json | 20 ++ frontend/src/i18n/locales/es.json | 20 ++ frontend/src/i18n/locales/pt-BR.json | 20 ++ frontend/src/pages/Profile.jsx | 335 ++++++++++++++++++ frontend/src/services/api.js | 21 ++ 11 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/Profile.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ffeb41..8a7a07c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/). Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/). +## [1.47.0] - 2025-12-17 + +### Added +- 👤 **Página Meu Perfil** - Gerenciamento de dados pessoais do usuário + - Editar nome e email + - Alterar senha (validação de senha atual obrigatória) + - Validação em tempo real dos campos + - Feedback visual de erros e sucesso + - Traduções completas: PT-BR, EN, ES + - Link no menu Configurações > Perfil + +### Technical Details +- Backend: `PUT /api/profile` no AuthController +- Frontend: `Profile.jsx` com formulários separados para dados e senha +- Service: `profileService` em api.js com atualização automática do localStorage + ## [1.44.6] - 2025-12-17 ### Changed diff --git a/VERSION b/VERSION index 437e025..21998d3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.44.6 +1.47.0 diff --git a/backend/app/Http/Controllers/Api/AuthController.php b/backend/app/Http/Controllers/Api/AuthController.php index 1a153fd..b9f81bb 100644 --- a/backend/app/Http/Controllers/Api/AuthController.php +++ b/backend/app/Http/Controllers/Api/AuthController.php @@ -179,5 +179,86 @@ public function me(Request $request): JsonResponse ], 500); } } + + /** + * Update user profile (name, email, password) + */ + public function updateProfile(Request $request): JsonResponse + { + try { + $user = $request->user(); + + $rules = [ + 'name' => 'sometimes|required|string|max:255', + 'email' => 'sometimes|required|string|email|max:255|unique:users,email,' . $user->id, + 'current_password' => 'required_with:new_password', + 'new_password' => 'nullable|string|min:8|confirmed', + ]; + + $messages = [ + 'name.required' => 'O nome é obrigatório', + 'name.max' => 'O nome deve ter no máximo 255 caracteres', + 'email.required' => 'O email é obrigatório', + 'email.email' => 'O email deve ser válido', + 'email.unique' => 'Este email já está em uso', + 'current_password.required_with' => 'A senha atual é obrigatória para alterar a senha', + 'new_password.min' => 'A nova senha deve ter pelo menos 8 caracteres', + 'new_password.confirmed' => 'As senhas não coincidem', + ]; + + $validator = Validator::make($request->all(), $rules, $messages); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Erro de validação', + 'errors' => $validator->errors() + ], 422); + } + + // Verificar senha atual se estiver alterando senha + if ($request->filled('new_password')) { + if (!Hash::check($request->current_password, $user->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Senha atual incorreta', + 'errors' => ['current_password' => ['A senha atual está incorreta']] + ], 422); + } + $user->password = Hash::make($request->new_password); + } + + // Atualizar nome se fornecido + if ($request->has('name')) { + $user->name = $request->name; + } + + // Atualizar email se fornecido + if ($request->has('email')) { + $user->email = $request->email; + } + + $user->save(); + + return response()->json([ + 'success' => true, + 'message' => 'Perfil atualizado com sucesso', + 'data' => [ + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + ] + ] + ], 200); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Erro ao atualizar perfil', + 'error' => $e->getMessage() + ], 500); + } + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index d5395f1..eebc20f 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -36,6 +36,7 @@ // Auth routes Route::post('/logout', [AuthController::class, 'logout']); Route::get('/me', [AuthController::class, 'me']); + Route::put('/profile', [AuthController::class, 'updateProfile']); Route::get('/user', function (Request $request) { return $request->user(); }); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2b494c6..66cca8b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,7 @@ import Goals from './pages/Goals'; import Budgets from './pages/Budgets'; import Reports from './pages/Reports'; import Preferences from './pages/Preferences'; +import Profile from './pages/Profile'; function App() { return ( @@ -190,6 +191,16 @@ function App() { } /> + + + + + + } + /> } /> diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 913b484..3c9e48a 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -104,6 +104,7 @@ const Layout = ({ children }) => { { path: '/categories', icon: 'bi-tags', label: t('nav.categories') }, { path: '/cost-centers', icon: 'bi-building', label: t('nav.costCenters') }, { path: '/preferences', icon: 'bi-sliders', label: t('nav.preferences') }, + { path: '/profile', icon: 'bi-person-circle', label: t('nav.profile') }, ] }, ]; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 08f7d36..f59c323 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2005,5 +2005,25 @@ "timezone": "Timezone", "note": "Some changes may require page refresh." } + }, + "profile": { + "title": "My Profile", + "subtitle": "Manage your personal data and password", + "personalData": "Personal Data", + "name": "Name", + "email": "Email", + "saveProfile": "Save Profile", + "changePassword": "Change Password", + "currentPassword": "Current Password", + "newPassword": "New Password", + "confirmPassword": "Confirm New Password", + "passwordHint": "Minimum 8 characters", + "loadError": "Error loading profile", + "saveSuccess": "Profile updated successfully!", + "saveError": "Error saving profile", + "passwordChanged": "Password changed successfully!", + "passwordError": "Error changing password", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 8 characters" } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 8849c97..6681a72 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -1993,5 +1993,25 @@ "timezone": "Zona horaria", "note": "Algunos cambios pueden requerir actualizar la página." } + }, + "profile": { + "title": "Mi Perfil", + "subtitle": "Gestiona tus datos personales y contraseña", + "personalData": "Datos Personales", + "name": "Nombre", + "email": "Email", + "saveProfile": "Guardar Perfil", + "changePassword": "Cambiar Contraseña", + "currentPassword": "Contraseña Actual", + "newPassword": "Nueva Contraseña", + "confirmPassword": "Confirmar Nueva Contraseña", + "passwordHint": "Mínimo 8 caracteres", + "loadError": "Error al cargar perfil", + "saveSuccess": "¡Perfil actualizado con éxito!", + "saveError": "Error al guardar perfil", + "passwordChanged": "¡Contraseña cambiada con éxito!", + "passwordError": "Error al cambiar contraseña", + "passwordMismatch": "Las contraseñas no coinciden", + "passwordTooShort": "La contraseña debe tener al menos 8 caracteres" } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/pt-BR.json b/frontend/src/i18n/locales/pt-BR.json index 994da65..b580134 100644 --- a/frontend/src/i18n/locales/pt-BR.json +++ b/frontend/src/i18n/locales/pt-BR.json @@ -2011,5 +2011,25 @@ "timezone": "Fuso horário", "note": "Algumas alterações podem exigir atualização da página." } + }, + "profile": { + "title": "Meu Perfil", + "subtitle": "Gerencie seus dados pessoais e senha", + "personalData": "Dados Pessoais", + "name": "Nome", + "email": "Email", + "saveProfile": "Salvar Perfil", + "changePassword": "Alterar Senha", + "currentPassword": "Senha Atual", + "newPassword": "Nova Senha", + "confirmPassword": "Confirmar Nova Senha", + "passwordHint": "Mínimo de 8 caracteres", + "loadError": "Erro ao carregar perfil", + "saveSuccess": "Perfil atualizado com sucesso!", + "saveError": "Erro ao salvar perfil", + "passwordChanged": "Senha alterada com sucesso!", + "passwordError": "Erro ao alterar senha", + "passwordMismatch": "As senhas não coincidem", + "passwordTooShort": "A senha deve ter pelo menos 8 caracteres" } } \ No newline at end of file diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx new file mode 100644 index 0000000..ce6ce97 --- /dev/null +++ b/frontend/src/pages/Profile.jsx @@ -0,0 +1,335 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useToast } from '../components/Toast'; +import { profileService } from '../services/api'; + +export default function Profile() { + const { t } = useTranslation(); + const { showToast } = useToast(); + + const [loading, setLoading] = useState(true); + const [savingProfile, setSavingProfile] = useState(false); + const [savingPassword, setSavingPassword] = useState(false); + + // Dados do perfil + const [profile, setProfile] = useState({ + name: '', + email: '', + }); + + // Dados de alteração de senha + const [passwordData, setPasswordData] = useState({ + current_password: '', + new_password: '', + new_password_confirmation: '', + }); + + // Erros de validação + const [profileErrors, setProfileErrors] = useState({}); + const [passwordErrors, setPasswordErrors] = useState({}); + + useEffect(() => { + loadProfile(); + }, []); + + const loadProfile = async () => { + try { + setLoading(true); + const response = await profileService.get(); + if (response.success) { + setProfile({ + name: response.data.user.name || '', + email: response.data.user.email || '', + }); + } + } catch (error) { + console.error('Error loading profile:', error); + showToast(t('profile.loadError'), 'error'); + } finally { + setLoading(false); + } + }; + + const handleProfileChange = (field, value) => { + setProfile(prev => ({ + ...prev, + [field]: value, + })); + // Limpar erro do campo + if (profileErrors[field]) { + setProfileErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const handlePasswordChange = (field, value) => { + setPasswordData(prev => ({ + ...prev, + [field]: value, + })); + // Limpar erro do campo + if (passwordErrors[field]) { + setPasswordErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const handleSaveProfile = async (e) => { + e.preventDefault(); + setProfileErrors({}); + + try { + setSavingProfile(true); + const response = await profileService.update({ + name: profile.name, + email: profile.email, + }); + + if (response.success) { + showToast(t('profile.saveSuccess'), 'success'); + } + } catch (error) { + console.error('Error saving profile:', error); + if (error.response?.data?.errors) { + setProfileErrors(error.response.data.errors); + } + showToast(error.response?.data?.message || t('profile.saveError'), 'error'); + } finally { + setSavingProfile(false); + } + }; + + const handleChangePassword = async (e) => { + e.preventDefault(); + setPasswordErrors({}); + + // Validação básica no frontend + if (passwordData.new_password !== passwordData.new_password_confirmation) { + setPasswordErrors({ new_password_confirmation: [t('profile.passwordMismatch')] }); + return; + } + + if (passwordData.new_password.length < 8) { + setPasswordErrors({ new_password: [t('profile.passwordTooShort')] }); + return; + } + + try { + setSavingPassword(true); + const response = await profileService.update({ + current_password: passwordData.current_password, + new_password: passwordData.new_password, + new_password_confirmation: passwordData.new_password_confirmation, + }); + + if (response.success) { + showToast(t('profile.passwordChanged'), 'success'); + // Limpar campos de senha + setPasswordData({ + current_password: '', + new_password: '', + new_password_confirmation: '', + }); + } + } catch (error) { + console.error('Error changing password:', error); + if (error.response?.data?.errors) { + setPasswordErrors(error.response.data.errors); + } + showToast(error.response?.data?.message || t('profile.passwordError'), 'error'); + } finally { + setSavingPassword(false); + } + }; + + if (loading) { + return ( +
+
+
+ {t('common.loading')} +
+
+
+ ); + } + + return ( +
+
+
+ {/* Header */} +
+ +
+

{t('profile.title')}

+

{t('profile.subtitle')}

+
+
+ + {/* Dados Pessoais */} +
+
+
+ + {t('profile.personalData')} +
+
+
+
+ {/* Nome */} +
+ + handleProfileChange('name', e.target.value)} + required + /> + {profileErrors.name && ( +
{profileErrors.name[0]}
+ )} +
+ + {/* Email */} +
+ + handleProfileChange('email', e.target.value)} + required + /> + {profileErrors.email && ( +
{profileErrors.email[0]}
+ )} +
+ + {/* Botão Salvar Perfil */} +
+ +
+
+
+
+ + {/* Alterar Senha */} +
+
+
+ + {t('profile.changePassword')} +
+
+
+
+ {/* Senha Atual */} +
+ + handlePasswordChange('current_password', e.target.value)} + required + autoComplete="current-password" + /> + {passwordErrors.current_password && ( +
{passwordErrors.current_password[0]}
+ )} +
+ + {/* Nova Senha */} +
+ + handlePasswordChange('new_password', e.target.value)} + required + minLength={8} + autoComplete="new-password" + /> + {passwordErrors.new_password && ( +
{passwordErrors.new_password[0]}
+ )} +
{t('profile.passwordHint')}
+
+ + {/* Confirmar Nova Senha */} +
+ + handlePasswordChange('new_password_confirmation', e.target.value)} + required + minLength={8} + autoComplete="new-password" + /> + {passwordErrors.new_password_confirmation && ( +
{passwordErrors.new_password_confirmation[0]}
+ )} +
+ + {/* Botão Alterar Senha */} +
+ +
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index f7ad7b0..cdce16b 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1517,4 +1517,25 @@ export const preferencesService = { }, }; +// ============================================ +// Profile Service (Perfil do Usuário) +// ============================================ +export const profileService = { + // Obter dados do usuário + get: async () => { + const response = await api.get('/me'); + return response.data; + }, + + // Atualizar perfil (nome, email, senha) + update: async (data) => { + const response = await api.put('/profile', data); + // Atualizar localStorage se sucesso + if (response.data.success && response.data.data?.user) { + localStorage.setItem('user', JSON.stringify(response.data.data.user)); + } + return response.data; + }, +}; + export default api;