diff --git a/src/app/(dashboard)/[portSlug]/settings/profile/page.tsx b/src/app/(dashboard)/[portSlug]/settings/profile/page.tsx deleted file mode 100644 index 072f641d..00000000 --- a/src/app/(dashboard)/[portSlug]/settings/profile/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { UserProfile } from '@/components/settings/user-profile'; - -export default function ProfilePage() { - return ; -} diff --git a/src/components/layout/user-menu.tsx b/src/components/layout/user-menu.tsx index 173540d9..27b90c71 100644 --- a/src/components/layout/user-menu.tsx +++ b/src/components/layout/user-menu.tsx @@ -3,7 +3,7 @@ /** * Unified user menu - used by the topbar avatar AND the sidebar-footer * profile row. Encapsulates: - * - Profile / Settings / Notification preferences links + * - Settings / Notification preferences links * - Dark-mode toggle * - Sign out * - Inline port switcher (when the user has access to >1 port) @@ -15,7 +15,7 @@ import { useRouter } from 'next/navigation'; import { useQueryClient } from '@tanstack/react-query'; -import { LogOut, User, Settings, Bell, Check, Building2 } from 'lucide-react'; +import { LogOut, Settings, Bell, Check, Building2 } from 'lucide-react'; import { type ReactNode } from 'react'; import { useUIStore } from '@/stores/ui-store'; @@ -107,11 +107,6 @@ export function UserMenu({ trigger, align = 'end', user, ports }: UserMenuProps) > )} - {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - router.push(`${base}/settings/profile` as any)}> - - Profile - {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} router.push(`${base}/settings` as any)}> diff --git a/src/components/settings/user-profile.tsx b/src/components/settings/user-profile.tsx deleted file mode 100644 index 67e5d8db..00000000 --- a/src/components/settings/user-profile.tsx +++ /dev/null @@ -1,229 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { Save } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { PageHeader } from '@/components/shared/page-header'; -import { apiFetch } from '@/lib/api/client'; - -interface MeUser { - id?: string; - email?: string; - name?: string; -} - -export function UserProfile() { - const [me, setMe] = useState(null); - const [displayName, setDisplayName] = useState(''); - const [savingProfile, setSavingProfile] = useState(false); - const [profileMessage, setProfileMessage] = useState<{ kind: 'ok' | 'err'; text: string } | null>( - null, - ); - - const [currentPassword, setCurrentPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [revokeOthers, setRevokeOthers] = useState(true); - const [savingPassword, setSavingPassword] = useState(false); - const [passwordMessage, setPasswordMessage] = useState<{ - kind: 'ok' | 'err'; - text: string; - } | null>(null); - - useEffect(() => { - async function load() { - const res = await apiFetch<{ data: { user?: MeUser } }>('/api/v1/me'); - setMe(res.data.user ?? null); - setDisplayName(res.data.user?.name ?? ''); - } - void load(); - }, []); - - async function saveProfile() { - setSavingProfile(true); - setProfileMessage(null); - try { - await apiFetch('/api/v1/me', { - method: 'PATCH', - body: { displayName: displayName || undefined }, - }); - setProfileMessage({ kind: 'ok', text: 'Profile saved' }); - } catch (err) { - setProfileMessage({ - kind: 'err', - text: err instanceof Error ? err.message : 'Failed to save profile', - }); - } finally { - setSavingProfile(false); - } - } - - async function changePassword(e: React.FormEvent) { - e.preventDefault(); - setPasswordMessage(null); - if (newPassword.length < 9) { - setPasswordMessage({ kind: 'err', text: 'New password must be at least 9 characters' }); - return; - } - if (newPassword !== confirmPassword) { - setPasswordMessage({ kind: 'err', text: 'New password and confirmation do not match' }); - return; - } - setSavingPassword(true); - try { - await apiFetch('/api/v1/me/password', { - method: 'POST', - body: { currentPassword, newPassword, revokeOtherSessions: revokeOthers }, - }); - setCurrentPassword(''); - setNewPassword(''); - setConfirmPassword(''); - setPasswordMessage({ - kind: 'ok', - text: revokeOthers - ? 'Password changed. Other sessions have been signed out.' - : 'Password changed.', - }); - } catch (err) { - setPasswordMessage({ - kind: 'err', - text: err instanceof Error ? err.message : 'Failed to change password', - }); - } finally { - setSavingPassword(false); - } - } - - return ( - - - - - - - Account - Identity and display preferences for your CRM account - - - - Email - - - Email is your sign-in identifier and cannot be changed here. - - - - Display name - setDisplayName(e.target.value)} - placeholder="How your name appears in comments, audit log, and emails" - className="mt-1" - /> - - - - - Save profile - - {profileMessage ? ( - - {profileMessage.text} - - ) : null} - - - - - - - Change password - - Minimum 9 characters. You’ll be prompted to sign in again on your other devices - if you check the box below. - - - - - - Current password - setCurrentPassword(e.target.value)} - className="mt-1" - /> - - - New password - setNewPassword(e.target.value)} - className="mt-1" - /> - - - Confirm new password - setConfirmPassword(e.target.value)} - className="mt-1" - /> - - - setRevokeOthers(e.target.checked)} - className="h-4 w-4" - /> - - Sign out of other devices - - - - - Change password - - {passwordMessage ? ( - - {passwordMessage.text} - - ) : null} - - - - - - - ); -} diff --git a/src/components/settings/user-settings.tsx b/src/components/settings/user-settings.tsx index 017a8ae5..8e775a9f 100644 --- a/src/components/settings/user-settings.tsx +++ b/src/components/settings/user-settings.tsx @@ -435,6 +435,8 @@ export function UserSettings() { + + Notifications @@ -459,3 +461,134 @@ export function UserSettings() { ); } + +/** Direct change-password form (current + new). Lifted from the deprecated + * user-profile.tsx into the unified settings page so we keep both reset-via- + * email and change-in-place flows on a single screen. */ +function ChangePasswordCard() { + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [revokeOthers, setRevokeOthers] = useState(true); + const [savingPassword, setSavingPassword] = useState(false); + const [passwordMessage, setPasswordMessage] = useState<{ + kind: 'ok' | 'err'; + text: string; + } | null>(null); + + async function changePassword(e: React.FormEvent) { + e.preventDefault(); + setPasswordMessage(null); + if (newPassword.length < 9) { + setPasswordMessage({ kind: 'err', text: 'New password must be at least 9 characters' }); + return; + } + if (newPassword !== confirmPassword) { + setPasswordMessage({ kind: 'err', text: 'New password and confirmation do not match' }); + return; + } + setSavingPassword(true); + try { + await apiFetch('/api/v1/me/password', { + method: 'POST', + body: { currentPassword, newPassword, revokeOtherSessions: revokeOthers }, + }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setPasswordMessage({ + kind: 'ok', + text: revokeOthers + ? 'Password changed. Other sessions have been signed out.' + : 'Password changed.', + }); + } catch (err) { + setPasswordMessage({ + kind: 'err', + text: err instanceof Error ? err.message : 'Failed to change password', + }); + } finally { + setSavingPassword(false); + } + } + + return ( + + + Change password + + Minimum 9 characters. You’ll be prompted to sign in again on your other devices if + you check the box below. + + + + + + Current password + setCurrentPassword(e.target.value)} + className="mt-1" + /> + + + New password + setNewPassword(e.target.value)} + className="mt-1" + /> + + + Confirm new password + setConfirmPassword(e.target.value)} + className="mt-1" + /> + + + setRevokeOthers(e.target.checked)} + className="h-4 w-4" + /> + + Sign out of other devices + + + + + Change password + + {passwordMessage ? ( + + {passwordMessage.text} + + ) : null} + + + + + ); +}
- Email is your sign-in identifier and cannot be changed here. -