import React, { useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; import { useAuthStore } from '../stores/authStore'; import PwaLoginCard from '../components/ui/PwaLoginCard'; type Mode = 'password' | 'sms' | 'forgot'; type SmsStep = 1 | 2; type ForgotStep = 1 | 2 | 3; export default function LoginPage() { const login = useAuthStore((s) => s.login); const [searchParams] = useSearchParams(); const [mode, setMode] = useState('password'); const [pwMobile, setPwMobile] = useState(''); const [pwPass, setPwPass] = useState(''); const [showPass, setShowPass] = useState(false); const [pwLoading, setPwLoading] = useState(false); const [smsStep, setSmsStep] = useState(1); const [smsMobile, setSmsMobile] = useState(''); const [smsCode, setSmsCode] = useState(''); const [smsUuid, setSmsUuid] = useState(''); const [smsLoading, setSmsLoading] = useState(false); const [forgotStep, setForgotStep] = useState(1); const [forgotMobile, setForgotMobile] = useState(''); const [forgotCode, setForgotCode] = useState(''); const [forgotUuid, setForgotUuid] = useState(''); const [forgotPass, setForgotPass] = useState(''); const [forgotPass2, setForgotPass2] = useState(''); const [showNewPass, setShowNewPass] = useState(false); const [forgotLoading, setForgotLoading] = useState(false); const [cooldown, setCooldown] = useState(0); const timerRef = useRef | null>(null); useEffect(() => () => { if (timerRef.current) clearInterval(timerRef.current); }, []); const startCooldown = () => { setCooldown(60); timerRef.current = setInterval(() => { setCooldown((v) => { if (v <= 1) { clearInterval(timerRef.current!); return 0; } return v - 1; }); }, 1000); }; const switchMode = (m: Mode) => { setMode(m); setSmsStep(1); setSmsMobile(''); setSmsCode(''); setSmsUuid(''); setForgotStep(1); setForgotMobile(''); setForgotCode(''); setForgotUuid(''); setForgotPass(''); setForgotPass2(''); setCooldown(0); if (timerRef.current) { clearInterval(timerRef.current); } }; const handlePasswordLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!pwMobile || !pwPass) { toast.error('شماره موبایل و رمز عبور الزامی است'); return; } setPwLoading(true); try { const res = await fetch('/api/v1/user/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mobile_number: pwMobile, password: pwPass }), }); const json = await res.json(); if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'خطا در ورود'); return; } login(json.access_token, json.refresh_token ?? ''); toast.success('خوش آمدید'); } catch { toast.error('خطا در اتصال به سرور'); } finally { setPwLoading(false); } }; const sendCode = async (mobile: string, onSuccess: (uuid: string) => void, setLoading: (v: boolean) => void) => { setLoading(true); try { const res = await fetch('/api/v1/user/send-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mobile }), }); const json = await res.json(); if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'خطا در ارسال کد'); return; } onSuccess(json.uuid); startCooldown(); toast.success('کد تأیید ارسال شد'); } catch { toast.error('خطا در اتصال به سرور'); } finally { setLoading(false); } }; const handleSmsSend = () => { if (!/^09[0-9]{9}$/.test(smsMobile)) { toast.error('شماره موبایل معتبر نیست'); return; } sendCode(smsMobile, (uuid) => { setSmsUuid(uuid); setSmsStep(2); }, setSmsLoading); }; const handleSmsVerify = async () => { if (!smsCode) { toast.error('کد تأیید را وارد کنید'); return; } setSmsLoading(true); try { const vRes = await fetch('/api/v1/user/verify-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: smsUuid, code: smsCode }), }); const vJson = await vRes.json(); if (!vRes.ok) { toast.error(vJson?.errors?.[0]?.message ?? 'کد نادرست است'); return; } const lRes = await fetch('/api/v1/user/otp-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: smsUuid }), }); const lJson = await lRes.json(); if (!lRes.ok) { toast.error(lJson?.errors?.[0]?.message ?? 'کاربری با این شماره یافت نشد'); return; } login(lJson.access_token, lJson.refresh_token ?? ''); toast.success('خوش آمدید'); } catch { toast.error('خطا در اتصال به سرور'); } finally { setSmsLoading(false); } }; const handleForgotSend = () => { if (!/^09[0-9]{9}$/.test(forgotMobile)) { toast.error('شماره موبایل معتبر نیست'); return; } sendCode(forgotMobile, (uuid) => { setForgotUuid(uuid); setForgotStep(2); }, setForgotLoading); }; const handleForgotVerify = async () => { if (!forgotCode) { toast.error('کد تأیید را وارد کنید'); return; } setForgotLoading(true); try { const res = await fetch('/api/v1/user/verify-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: forgotUuid, code: forgotCode }), }); const json = await res.json(); if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'کد نادرست است'); return; } setForgotStep(3); } catch { toast.error('خطا در اتصال به سرور'); } finally { setForgotLoading(false); } }; const handleForgotReset = async () => { if (forgotPass.length < 6) { toast.error('رمز عبور باید حداقل ۶ کاراکتر باشد'); return; } if (forgotPass !== forgotPass2) { toast.error('رمزهای عبور یکسان نیستند'); return; } setForgotLoading(true); try { const res = await fetch('/api/v1/user/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: forgotUuid, new_password: forgotPass }), }); const json = await res.json(); if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'خطا در تغییر رمز'); return; } toast.success('رمز عبور با موفقیت تغییر یافت'); switchMode('password'); } catch { toast.error('خطا در اتصال به سرور'); } finally { setForgotLoading(false); } }; const handleResend = () => { setCooldown(0); if (timerRef.current) clearInterval(timerRef.current); if (mode === 'sms') { setSmsStep(1); setSmsCode(''); } else { setForgotStep(1); setForgotCode(''); } }; const ResendControl = () => cooldown > 0 ? ارسال مجدد تا {cooldown} ثانیه دیگر : ; return (

ورود به پنل

{mode === 'forgot' ? 'بازیابی رمز عبور' : 'اطلاعات حساب خود را وارد کنید'}

{searchParams.get('error') === 'access_denied' && (
حساب شما دسترسی به پنل مدیریت را ندارد
)} {mode !== 'forgot' && (
{(['password', 'sms'] as const).map((m) => ( ))}
)} {mode === 'password' && (
setPwMobile(e.target.value)} />
setPwPass(e.target.value)} />
)} {mode === 'sms' && (
{smsStep === 1 && ( <>
setSmsMobile(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSmsSend()} />
)} {smsStep === 2 && ( <>

کد پیامک‌شده به {smsMobile} را وارد کنید

setSmsCode(e.target.value.replace(/\D/g, ''))} onKeyDown={(e) => e.key === 'Enter' && handleSmsVerify()} autoFocus />
)}
)} {mode === 'forgot' && (
{([1, 2, 3] as const).map((s) => (
= s ? 'var(--primary)' : 'var(--border)', }} /> ))}
{forgotStep === 1 && ( <>
setForgotMobile(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleForgotSend()} />
)} {forgotStep === 2 && ( <>

کد پیامک‌شده به {forgotMobile} را وارد کنید

setForgotCode(e.target.value.replace(/\D/g, ''))} onKeyDown={(e) => e.key === 'Enter' && handleForgotVerify()} autoFocus />
)} {forgotStep === 3 && ( <>

رمز عبور جدید خود را وارد کنید

setForgotPass(e.target.value)} autoFocus />
setForgotPass2(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleForgotReset()} /> {forgotPass2 && forgotPass !== forgotPass2 && (
رمزهای عبور یکسان نیستند
)}
)}
)}

ClinicPro — نسخه ۱.۰.۰

); }