SEction 4 completeed
This commit is contained in:
@@ -6,19 +6,22 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Save, Loader2, Settings, User, Users, UserPlus, Shield, Lock
|
||||
Save, Loader2, Settings, User, Users, UserPlus, Shield, Lock, X
|
||||
} from 'lucide-react';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import {
|
||||
getAccountSettings,
|
||||
updateAccountSettings,
|
||||
getTeamMembers,
|
||||
inviteTeamMember,
|
||||
removeTeamMember,
|
||||
getUserProfile,
|
||||
changePassword,
|
||||
type AccountSettings,
|
||||
type TeamMember,
|
||||
} from '../../services/billing.api';
|
||||
@@ -27,6 +30,7 @@ type TabType = 'account' | 'profile' | 'team';
|
||||
|
||||
export default function AccountSettingsPage() {
|
||||
const toast = useToast();
|
||||
const { user, refreshUser } = useAuthStore();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('account');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -59,6 +63,15 @@ export default function AccountSettingsPage() {
|
||||
marketingEmails: false,
|
||||
});
|
||||
|
||||
// Password change state
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const [changingPassword, setChangingPassword] = useState(false);
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
// Team state
|
||||
const [members, setMembers] = useState<TeamMember[]>([]);
|
||||
const [teamLoading, setTeamLoading] = useState(false);
|
||||
@@ -74,6 +87,23 @@ export default function AccountSettingsPage() {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Load profile from auth store user data
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
const [firstName = '', lastName = ''] = (user.username || '').split(' ');
|
||||
setProfileForm({
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: user.email || '',
|
||||
phone: '',
|
||||
timezone: 'America/New_York',
|
||||
language: 'en',
|
||||
emailNotifications: true,
|
||||
marketingEmails: false,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -138,9 +168,10 @@ export default function AccountSettingsPage() {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setSaving(true);
|
||||
// TODO: Connect to profile API when available
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// Profile data is stored in auth user - refresh after save
|
||||
// Note: Full profile API would go here when backend supports it
|
||||
toast.success('Profile settings saved');
|
||||
await refreshUser();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to save profile');
|
||||
} finally {
|
||||
@@ -148,6 +179,33 @@ export default function AccountSettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = async () => {
|
||||
if (!passwordForm.currentPassword || !passwordForm.newPassword) {
|
||||
toast.error('Please fill in all password fields');
|
||||
return;
|
||||
}
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
toast.error('New passwords do not match');
|
||||
return;
|
||||
}
|
||||
if (passwordForm.newPassword.length < 8) {
|
||||
toast.error('Password must be at least 8 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setChangingPassword(true);
|
||||
await changePassword(passwordForm.currentPassword, passwordForm.newPassword);
|
||||
toast.success('Password changed successfully');
|
||||
setShowPasswordModal(false);
|
||||
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to change password');
|
||||
} finally {
|
||||
setChangingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInvite = async () => {
|
||||
if (!inviteForm.email) {
|
||||
toast.error('Email is required');
|
||||
@@ -555,7 +613,11 @@ export default function AccountSettingsPage() {
|
||||
<Lock className="w-5 h-5" />
|
||||
Security
|
||||
</h2>
|
||||
<Button variant="outline" tone="neutral">
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
onClick={() => setShowPasswordModal(true)}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</Card>
|
||||
@@ -771,6 +833,91 @@ export default function AccountSettingsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Password Change Modal */}
|
||||
{showPasswordModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50"
|
||||
onClick={() => setShowPasswordModal(false)}
|
||||
/>
|
||||
<Card className="relative z-10 w-full max-w-md p-6 m-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Lock className="w-5 h-5" />
|
||||
Change Password
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowPasswordModal(false)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Current Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={(e) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={(e) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={(e) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
onClick={() => {
|
||||
setShowPasswordModal(false);
|
||||
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
}}
|
||||
disabled={changingPassword}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={handlePasswordChange}
|
||||
disabled={changingPassword}
|
||||
>
|
||||
{changingPassword ? 'Changing...' : 'Change Password'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user