Files
clinicpro/assets/admin/pages/AccountSettingsPage.tsx
T
hamed ecdefa3c24 feat(tour): add onboarding tours for various admin pages
- Integrated TourButton component into SettingsMenuPage, SkillsPage, SmsWalletPage, StaffPage, StaffSessionDetailPage, StaffTreatmentSessionsPage, SubscriptionPage, TagsSettingsPage, TreatmentCasesPage to enhance user onboarding experience.
- Created new tour definitions for appointments, clinics, staff management, financial management, and patient management, ensuring comprehensive guidance for users navigating the admin panel.
- Updated documentation to reflect the addition of tours and their implementation details.
2026-08-10 10:10:20 +03:30

121 lines
6.3 KiB
TypeScript

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useMutation } from '@tanstack/react-query';
import { UserCircleIcon, EyeIcon, EyeSlashIcon, LockClosedIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import { useAuthStore } from '../stores/authStore';
import SettingsLayout from '../components/layout/SettingsLayout';
import TourButton from '../components/ui/TourButton';
const ROLE_LABELS: Record<string, string> = {
admin: 'مدیر', clinic: 'کلینیک', doctor: 'پزشک',
secretary: 'منشی', representation: 'نماینده', user: 'کاربر',
};
const schema = z.object({
current_password: z.string().min(1, 'رمز فعلی الزامی است'),
new_password: z.string().min(8, 'رمز جدید باید حداقل ۸ کاراکتر باشد'),
confirm: z.string().min(1, 'تکرار رمز الزامی است'),
}).refine((d) => d.new_password === d.confirm, { path: ['confirm'], message: 'تکرار رمز مطابقت ندارد' })
.refine((d) => d.new_password !== d.current_password, { path: ['new_password'], message: 'رمز جدید نباید با رمز فعلی یکسان باشد' });
type Form = z.infer<typeof schema>;
/** حساب کاربری — profile summary + change-password form, inside the settings shell. */
export default function AccountSettingsPage() {
const { userName, primaryRole, context } = useAuthStore();
const [show, setShow] = useState<{ cur: boolean; next: boolean }>({ cur: false, next: false });
const form = useForm<Form>({ resolver: zodResolver(schema) });
const changePassword = useMutation({
mutationFn: (d: Form) => api.post('/api/v1/user/change-password', {
current_password: d.current_password,
new_password: d.new_password,
}),
onSuccess: () => { toast.success('رمز عبور با موفقیت تغییر یافت'); form.reset(); },
onError: (e: any) => toast.error(e.message),
});
const rows: [string, string][] = [
['نام', userName || '—'],
['نقش', primaryRole ? (ROLE_LABELS[primaryRole] ?? primaryRole) : '—'],
['محیط فعلی', context?.name || '—'],
];
return (
<SettingsLayout active="account">
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 20, maxWidth: 620 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">حساب کاربری</h1><TourButton tourId="account-settings" ready /></div>
{/* Profile summary */}
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 20 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<div style={{ width: 48, height: 48, borderRadius: '50%', background: 'var(--primary-soft)', display: 'grid', placeItems: 'center' }}>
<UserCircleIcon style={{ width: 28, color: 'var(--primary)' }} />
</div>
<div style={{ fontWeight: 700, fontSize: 16 }}>{userName || 'کاربر'}</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{rows.map(([label, value]) => (
<div key={label} style={{ display: 'flex', justifyContent: 'space-between', padding: '9px 0', borderTop: '1px solid var(--border)' }}>
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>{label}</span>
<span style={{ fontSize: 13.5, color: 'var(--text)', fontWeight: 600 }}>{value}</span>
</div>
))}
</div>
</div>
{/* Change password */}
<form
onSubmit={form.handleSubmit((d) => changePassword.mutate(d))}
style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 20, display: 'flex', flexDirection: 'column', gap: 16 }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<LockClosedIcon style={{ width: 18, color: 'var(--text-2)' }} />
<b style={{ fontSize: 15 }}>تغییر رمز عبور</b>
</div>
<PasswordField
label="رمز فعلی" show={show.cur} onToggle={() => setShow((s) => ({ ...s, cur: !s.cur }))}
register={form.register('current_password')} error={form.formState.errors.current_password?.message}
/>
<PasswordField
label="رمز جدید" show={show.next} onToggle={() => setShow((s) => ({ ...s, next: !s.next }))}
register={form.register('new_password')} error={form.formState.errors.new_password?.message}
/>
<PasswordField
label="تکرار رمز جدید" show={show.next} onToggle={() => setShow((s) => ({ ...s, next: !s.next }))}
register={form.register('confirm')} error={form.formState.errors.confirm?.message}
/>
<button type="submit" className="btn primary" style={{ alignSelf: 'flex-start', height: 42 }} disabled={changePassword.isPending}>
{changePassword.isPending ? 'در حال ذخیره...' : 'تغییر رمز عبور'}
</button>
</form>
</div>
</SettingsLayout>
);
}
function PasswordField({ label, show, onToggle, register, error }: {
label: string; show: boolean; onToggle: () => void;
register: ReturnType<ReturnType<typeof useForm>['register']>; error?: string;
}) {
return (
<div>
<label className="field-label">{label}</label>
<div className="field" style={{ display: 'flex', alignItems: 'center', ...(error ? { borderColor: 'var(--danger)' } : {}) }}>
<input type={show ? 'text' : 'password'} aria-label={label} autoComplete="off" {...register} style={{ flex: 1 }} />
<button type="button" aria-label={show ? 'پنهان‌کردن رمز' : 'نمایش رمز'} onClick={onToggle} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--text-3)', display: 'grid', placeItems: 'center' }}>
{show ? <EyeSlashIcon style={{ width: 17 }} /> : <EyeIcon style={{ width: 17 }} />}
</button>
</div>
{error && <span className="field-error">{error}</span>}
</div>
);
}