- 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.
121 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|