632 lines
21 KiB
TypeScript
632 lines
21 KiB
TypeScript
import React, { useState } from 'react';
|
||
import {
|
||
Container,
|
||
Grid,
|
||
Card,
|
||
CardContent,
|
||
Typography,
|
||
Button,
|
||
Box,
|
||
TextField,
|
||
Avatar,
|
||
Divider,
|
||
Switch,
|
||
FormControlLabel,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Tab,
|
||
Tabs,
|
||
Paper,
|
||
List,
|
||
ListItem,
|
||
ListItemText,
|
||
ListItemSecondaryAction,
|
||
IconButton,
|
||
Chip,
|
||
} from '@mui/material';
|
||
import {
|
||
Edit,
|
||
Save,
|
||
Cancel,
|
||
Person,
|
||
Security,
|
||
Notifications,
|
||
Delete,
|
||
Visibility,
|
||
VisibilityOff,
|
||
} from '@mui/icons-material';
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import { useForm, Controller } from 'react-hook-form';
|
||
import { yupResolver } from '@hookform/resolvers/yup';
|
||
import * as yup from 'yup';
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
import { usersAPI, notificationsAPI } from '../../services/api';
|
||
import { User, UserSettings } from '../../types';
|
||
import toast from 'react-hot-toast';
|
||
import { formatDistanceToNow } from 'date-fns';
|
||
|
||
const profileSchema = yup.object({
|
||
firstName: yup.string().required('Ad gereklidir').min(2, 'Ad en az 2 karakter olmalıdır'),
|
||
lastName: yup.string().required('Soyad gereklidir').min(2, 'Soyad en az 2 karakter olmalıdır'),
|
||
email: yup.string().required('E-posta gereklidir').email('Geçersiz e-posta formatı'),
|
||
});
|
||
|
||
const passwordSchema = yup.object({
|
||
currentPassword: yup.string().required('Mevcut şifre gereklidir'),
|
||
newPassword: yup.string().required('Yeni şifre gereklidir').min(6, 'Şifre en az 6 karakter olmalıdır'),
|
||
confirmPassword: yup.string()
|
||
.required('Şifre onayı gereklidir')
|
||
.oneOf([yup.ref('newPassword')], 'Şifreler eşleşmelidir'),
|
||
});
|
||
|
||
interface TabPanelProps {
|
||
children?: React.ReactNode;
|
||
index: number;
|
||
value: number;
|
||
}
|
||
|
||
function TabPanel(props: TabPanelProps) {
|
||
const { children, value, index, ...other } = props;
|
||
|
||
return (
|
||
<div
|
||
role="tabpanel"
|
||
hidden={value !== index}
|
||
id={`profile-tabpanel-${index}`}
|
||
aria-labelledby={`profile-tab-${index}`}
|
||
{...other}
|
||
>
|
||
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const ProfilePage: React.FC = () => {
|
||
const { user, updateProfile, changePassword } = useAuth();
|
||
const queryClient = useQueryClient();
|
||
|
||
const [tabValue, setTabValue] = useState(0);
|
||
const [editingProfile, setEditingProfile] = useState(false);
|
||
const [changePasswordDialogOpen, setChangePasswordDialogOpen] = useState(false);
|
||
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||
|
||
// Fetch user settings
|
||
const { data: settingsData } = useQuery({
|
||
queryKey: ['userSettings'],
|
||
queryFn: () => usersAPI.getUserSettings(),
|
||
});
|
||
|
||
// Fetch user activities - temporarily disabled
|
||
// const { data: activitiesData } = useQuery({
|
||
// queryKey: ['userActivities'],
|
||
// queryFn: () => usersAPI.getUserActivities({ limit: 10 }),
|
||
// });
|
||
const activitiesData = { data: [] };
|
||
|
||
// Update profile mutation
|
||
const updateProfileMutation = useMutation({
|
||
mutationFn: (data: Partial<User>) => updateProfile(data),
|
||
onSuccess: () => {
|
||
toast.success('Profil başarıyla güncellendi!');
|
||
setEditingProfile(false);
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error.response?.data?.message || 'Profil güncellenemedi');
|
||
},
|
||
});
|
||
|
||
// Change password mutation
|
||
const changePasswordMutation = useMutation({
|
||
mutationFn: (data: { currentPassword: string; newPassword: string }) =>
|
||
changePassword(data.currentPassword, data.newPassword),
|
||
onSuccess: () => {
|
||
toast.success('Şifre başarıyla değiştirildi!');
|
||
setChangePasswordDialogOpen(false);
|
||
passwordReset();
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error.response?.data?.message || 'Şifre değiştirilemedi');
|
||
},
|
||
});
|
||
|
||
// Update settings mutation with optimistic cache sync to avoid flicker
|
||
const updateSettingsMutation = useMutation({
|
||
mutationFn: (data: Partial<UserSettings>) => usersAPI.updateUserSettings(data),
|
||
onSuccess: (res) => {
|
||
const serverSettings = (res?.data?.data?.settings ?? {}) as Partial<UserSettings>;
|
||
// Update local state to the authoritative server value
|
||
setLocalSettings(serverSettings);
|
||
// Update the react-query cache so refetch won't revert UI
|
||
queryClient.setQueryData(['userSettings'], (prev: any) => {
|
||
if (!prev) return res;
|
||
try {
|
||
const next = {
|
||
...prev,
|
||
data: {
|
||
...prev.data,
|
||
data: {
|
||
...(prev?.data?.data || {}),
|
||
settings: serverSettings,
|
||
},
|
||
},
|
||
};
|
||
return next;
|
||
} catch {
|
||
return res;
|
||
}
|
||
});
|
||
toast.success('Ayarlar başarıyla güncellendi!');
|
||
},
|
||
onError: (error: any) => {
|
||
toast.error(error.response?.data?.message || 'Ayarlar güncellenemedi');
|
||
},
|
||
});
|
||
|
||
const {
|
||
control: profileControl,
|
||
handleSubmit: handleProfileSubmit,
|
||
reset: profileReset,
|
||
formState: { errors: profileErrors },
|
||
} = useForm({
|
||
resolver: yupResolver(profileSchema),
|
||
defaultValues: {
|
||
firstName: user?.firstName || '',
|
||
lastName: user?.lastName || '',
|
||
email: user?.email || '',
|
||
},
|
||
});
|
||
|
||
const {
|
||
control: passwordControl,
|
||
handleSubmit: handlePasswordSubmit,
|
||
reset: passwordReset,
|
||
formState: { errors: passwordErrors },
|
||
} = useForm({
|
||
resolver: yupResolver(passwordSchema),
|
||
defaultValues: {
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: '',
|
||
},
|
||
});
|
||
|
||
// Backend returns { success, data: { settings } }
|
||
const settings = (settingsData?.data?.data?.settings ?? {}) as Partial<UserSettings>;
|
||
const [localSettings, setLocalSettings] = useState<Partial<UserSettings>>({});
|
||
const activities = activitiesData?.data || [];
|
||
|
||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||
setTabValue(newValue);
|
||
};
|
||
|
||
const handleUpdateProfile = (data: any) => {
|
||
updateProfileMutation.mutate(data);
|
||
};
|
||
|
||
const handleChangePassword = (data: any) => {
|
||
changePasswordMutation.mutate({
|
||
currentPassword: data.currentPassword,
|
||
newPassword: data.newPassword,
|
||
});
|
||
};
|
||
|
||
const handleSettingChange = async (key: keyof UserSettings, value: boolean) => {
|
||
const previous = localSettings[key];
|
||
setLocalSettings((prev) => ({ ...prev, [key]: value }));
|
||
try {
|
||
await updateSettingsMutation.mutateAsync({ [key]: value } as Partial<UserSettings>);
|
||
} catch (error) {
|
||
// Revert optimistic update on error
|
||
setLocalSettings((prev) => ({ ...prev, [key]: previous as boolean }));
|
||
}
|
||
};
|
||
|
||
React.useEffect(() => {
|
||
// Avoid overriding optimistic state while a mutation is pending
|
||
if (!updateSettingsMutation.isPending) {
|
||
setLocalSettings(settings);
|
||
}
|
||
}, [settings, updateSettingsMutation.isPending]);
|
||
|
||
React.useEffect(() => {
|
||
if (user) {
|
||
profileReset({
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
email: user.email,
|
||
});
|
||
}
|
||
}, [user, profileReset]);
|
||
|
||
if (!user) {
|
||
return (
|
||
<Container maxWidth="lg" sx={{ mt: 4 }}>
|
||
<Typography>Yükleniyor...</Typography>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||
{/* Header */}
|
||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
|
||
<Avatar
|
||
sx={{
|
||
width: 80,
|
||
height: 80,
|
||
mr: 3,
|
||
bgcolor: 'primary.main',
|
||
fontSize: '2rem',
|
||
}}
|
||
>
|
||
{user.firstName?.[0] || ''}{user.lastName?.[0] || ''}
|
||
</Avatar>
|
||
<Box>
|
||
<Typography variant="h4" gutterBottom>
|
||
{user.firstName || ''} {user.lastName || ''}
|
||
</Typography>
|
||
<Typography variant="body1" color="text.secondary">
|
||
{user.email}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Üye olma tarihi: {new Date(user.createdAt).toLocaleDateString()}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Tabs */}
|
||
<Paper sx={{ mb: 3 }}>
|
||
<Tabs
|
||
value={tabValue}
|
||
onChange={handleTabChange}
|
||
aria-label="profile tabs"
|
||
variant="fullWidth"
|
||
>
|
||
<Tab icon={<Person />} label="Profil" />
|
||
<Tab icon={<Security />} label="Güvenlik" />
|
||
<Tab icon={<Notifications />} label="Bildirimler" />
|
||
</Tabs>
|
||
</Paper>
|
||
|
||
{/* Profile Tab */}
|
||
<TabPanel value={tabValue} index={0}>
|
||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '2fr 1fr' }, gap: 3 }}>
|
||
<Box>
|
||
<Card>
|
||
<CardContent>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||
<Typography variant="h6">Kişisel Bilgiler</Typography>
|
||
{!editingProfile ? (
|
||
<Button
|
||
startIcon={<Edit />}
|
||
onClick={() => setEditingProfile(true)}
|
||
>
|
||
Düzenle
|
||
</Button>
|
||
) : (
|
||
<Box>
|
||
<Button
|
||
startIcon={<Cancel />}
|
||
onClick={() => {
|
||
setEditingProfile(false);
|
||
profileReset();
|
||
}}
|
||
sx={{ mr: 1 }}
|
||
>
|
||
İptal
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
startIcon={<Save />}
|
||
onClick={handleProfileSubmit(handleUpdateProfile)}
|
||
disabled={updateProfileMutation.isPending}
|
||
>
|
||
Kaydet
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
|
||
<form onSubmit={handleProfileSubmit(handleUpdateProfile)}>
|
||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' }, gap: 2 }}>
|
||
<Box>
|
||
<Controller
|
||
name="firstName"
|
||
control={profileControl}
|
||
render={({ field }) => (
|
||
<TextField
|
||
{...field}
|
||
label="Ad"
|
||
fullWidth
|
||
disabled={!editingProfile}
|
||
error={!!profileErrors.firstName}
|
||
helperText={profileErrors.firstName?.message}
|
||
/>
|
||
)}
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Controller
|
||
name="lastName"
|
||
control={profileControl}
|
||
render={({ field }) => (
|
||
<TextField
|
||
{...field}
|
||
label="Soyad"
|
||
fullWidth
|
||
disabled={!editingProfile}
|
||
error={!!profileErrors.lastName}
|
||
helperText={profileErrors.lastName?.message}
|
||
/>
|
||
)}
|
||
/>
|
||
</Box>
|
||
<Box sx={{ gridColumn: '1 / -1' }}>
|
||
<Controller
|
||
name="email"
|
||
control={profileControl}
|
||
render={({ field }) => (
|
||
<TextField
|
||
{...field}
|
||
label="E-posta"
|
||
fullWidth
|
||
disabled={!editingProfile}
|
||
error={!!profileErrors.email}
|
||
helperText={profileErrors.email?.message}
|
||
/>
|
||
)}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
</Box>
|
||
|
||
<Box>
|
||
<Card>
|
||
<CardContent>
|
||
<Typography variant="h6" gutterBottom>
|
||
Son Aktiviteler
|
||
</Typography>
|
||
<List>
|
||
{activities.length === 0 ? (
|
||
<Typography variant="body2" color="text.secondary">
|
||
Son aktivite bulunmuyor
|
||
</Typography>
|
||
) : (
|
||
activities.map((activity: any, index: number) => (
|
||
<ListItem key={index} divider={index < activities.length - 1}>
|
||
<ListItemText
|
||
primary={activity.description}
|
||
secondary={formatDistanceToNow(new Date(activity.createdAt), { addSuffix: true })}
|
||
/>
|
||
</ListItem>
|
||
))
|
||
)}
|
||
</List>
|
||
</CardContent>
|
||
</Card>
|
||
</Box>
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* Security Tab */}
|
||
<TabPanel value={tabValue} index={1}>
|
||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
|
||
<Box>
|
||
<Card>
|
||
<CardContent>
|
||
<Typography variant="h6" gutterBottom>
|
||
Şifre
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||
Güçlü bir şifre kullanarak hesabınızı güvende tutun
|
||
</Typography>
|
||
<Button
|
||
variant="outlined"
|
||
onClick={() => setChangePasswordDialogOpen(true)}
|
||
>
|
||
Şifre Değiştir
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</Box>
|
||
|
||
<Box>
|
||
<Card>
|
||
<CardContent>
|
||
<Typography variant="h6" gutterBottom>
|
||
Hesap Durumu
|
||
</Typography>
|
||
|
||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||
<Typography variant="body2" sx={{ mr: 2 }}>
|
||
Hesap Durumu:
|
||
</Typography>
|
||
<Chip
|
||
label={user.isActive ? 'Aktif' : 'Pasif'}
|
||
color={user.isActive ? 'success' : 'error'}
|
||
size="small"
|
||
/>
|
||
</Box>
|
||
</CardContent>
|
||
</Card>
|
||
</Box>
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* Notifications Tab */}
|
||
<TabPanel value={tabValue} index={2}>
|
||
<Card>
|
||
<CardContent>
|
||
<Typography variant="h6" gutterBottom>
|
||
Bildirim Tercihleri
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||
Hangi bildirimleri almak istediğinizi seçin
|
||
</Typography>
|
||
|
||
<List>
|
||
<ListItem>
|
||
<ListItemText
|
||
primary="E-posta Bildirimleri"
|
||
secondary="E-posta yoluyla bildirim alın"
|
||
/>
|
||
<ListItemSecondaryAction>
|
||
<Switch
|
||
checked={Boolean(localSettings.emailNotifications)}
|
||
onChange={(e) => handleSettingChange('emailNotifications', e.target.checked)}
|
||
/>
|
||
</ListItemSecondaryAction>
|
||
</ListItem>
|
||
<Divider />
|
||
|
||
<ListItem>
|
||
<ListItemText
|
||
primary="Anlık Bildirimler"
|
||
secondary="Cihazınızda anlık bildirimler alın"
|
||
/>
|
||
<ListItemSecondaryAction>
|
||
<Switch
|
||
checked={Boolean(localSettings.pushNotifications)}
|
||
onChange={(e) => handleSettingChange('pushNotifications', e.target.checked)}
|
||
/>
|
||
</ListItemSecondaryAction>
|
||
</ListItem>
|
||
<Divider />
|
||
|
||
<ListItem>
|
||
<ListItemText
|
||
primary="Liste Güncellemeleri"
|
||
secondary="Paylaşılan listeler güncellendiğinde bildirim alın"
|
||
/>
|
||
<ListItemSecondaryAction>
|
||
<Switch
|
||
checked={Boolean(localSettings.itemUpdateNotifications)}
|
||
onChange={(e) => handleSettingChange('itemUpdateNotifications', e.target.checked)}
|
||
/>
|
||
</ListItemSecondaryAction>
|
||
</ListItem>
|
||
<Divider />
|
||
|
||
{/* Pazarlama E-postaları kaldırıldı */}
|
||
</List>
|
||
</CardContent>
|
||
</Card>
|
||
</TabPanel>
|
||
|
||
{/* Change Password Dialog */}
|
||
<Dialog
|
||
open={changePasswordDialogOpen}
|
||
onClose={() => setChangePasswordDialogOpen(false)}
|
||
maxWidth="sm"
|
||
fullWidth
|
||
>
|
||
<DialogTitle>Şifre Değiştir</DialogTitle>
|
||
<form onSubmit={handlePasswordSubmit(handleChangePassword)}>
|
||
<DialogContent>
|
||
<Controller
|
||
name="currentPassword"
|
||
control={passwordControl}
|
||
render={({ field }) => (
|
||
<TextField
|
||
{...field}
|
||
margin="dense"
|
||
label="Mevcut Şifre"
|
||
type={showCurrentPassword ? 'text' : 'password'}
|
||
fullWidth
|
||
variant="outlined"
|
||
error={!!passwordErrors.currentPassword}
|
||
helperText={passwordErrors.currentPassword?.message}
|
||
InputProps={{
|
||
endAdornment: (
|
||
<IconButton
|
||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||
edge="end"
|
||
>
|
||
{showCurrentPassword ? <VisibilityOff /> : <Visibility />}
|
||
</IconButton>
|
||
),
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
|
||
<Controller
|
||
name="newPassword"
|
||
control={passwordControl}
|
||
render={({ field }) => (
|
||
<TextField
|
||
{...field}
|
||
margin="dense"
|
||
label="Yeni Şifre"
|
||
type={showNewPassword ? 'text' : 'password'}
|
||
fullWidth
|
||
variant="outlined"
|
||
error={!!passwordErrors.newPassword}
|
||
helperText={passwordErrors.newPassword?.message}
|
||
InputProps={{
|
||
endAdornment: (
|
||
<IconButton
|
||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||
edge="end"
|
||
>
|
||
{showNewPassword ? <VisibilityOff /> : <Visibility />}
|
||
</IconButton>
|
||
),
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
|
||
<Controller
|
||
name="confirmPassword"
|
||
control={passwordControl}
|
||
render={({ field }) => (
|
||
<TextField
|
||
{...field}
|
||
margin="dense"
|
||
label="Yeni Şifre Tekrar"
|
||
type={showConfirmPassword ? 'text' : 'password'}
|
||
fullWidth
|
||
variant="outlined"
|
||
error={!!passwordErrors.confirmPassword}
|
||
helperText={passwordErrors.confirmPassword?.message}
|
||
InputProps={{
|
||
endAdornment: (
|
||
<IconButton
|
||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||
edge="end"
|
||
>
|
||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||
</IconButton>
|
||
),
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setChangePasswordDialogOpen(false)}>
|
||
İptal
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
variant="contained"
|
||
disabled={changePasswordMutation.isPending}
|
||
>
|
||
{changePasswordMutation.isPending ? 'Değiştiriliyor...' : 'Şifre Değiştir'}
|
||
</Button>
|
||
</DialogActions>
|
||
</form>
|
||
</Dialog>
|
||
</Container>
|
||
);
|
||
};
|
||
|
||
export default ProfilePage; |