import { useMemo, useState, type FormEvent } from 'react' import { Navigate, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { AlertCircle, ArrowLeft, ChevronDown, Eye, EyeOff, FlaskConical, Loader2, Lock, Mail, Phone, Send, ShieldCheck, Truck, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { InitialsAvatar } from '@/components/ui/avatar' import { statusLabel } from '@/components/ui/status-badge' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useAuthStore } from '@/zustand/authStore' import { demoAccounts } from '@/config/permissions' import type { AuthProvider, StaffRole } from '@/types' import { cn } from '@/lib/utils' const REMEMBER_KEY = 'gishen_remember_email' /** Demo OTP accepted for any phone (mock SMS provider). */ const MOCK_OTP = '123456' const ROLE_ORDER: StaffRole[] = [ 'super_admin', 'pharmacist', 'stock_manager', 'procurement', 'finance', 'marketing_manager', 'operations', ] const VALUE_POINTS = [ { icon: ShieldCheck, text: 'Prescription review with a full controlled-substance audit trail' }, { icon: Truck, text: 'Live dispatch across every Gishen branch and rider' }, { icon: Lock, text: 'Role-scoped access so each team only sees its own work' }, ] type CredentialMode = 'email' | 'phone' type PhoneStep = 'enter' | 'verify' function GoogleMark({ className }: { className?: string }) { return ( ) } function TelegramMark({ className }: { className?: string }) { return ( ) } function normalizePhone(value: string) { return value.replace(/[\s\-()]/g, '') } function isLikelyPhone(value: string) { const digits = normalizePhone(value).replace(/^\+/, '') return /^\d{9,15}$/.test(digits) } export function LoginPage() { const { t } = useTranslation() const navigate = useNavigate() const login = useAuthStore((s) => s.login) const loginWithProvider = useAuthStore((s) => s.loginWithProvider) const token = useAuthStore((s) => s.token) || localStorage.getItem('access_token') const [credentialMode, setCredentialMode] = useState('email') const [email, setEmail] = useState(() => localStorage.getItem(REMEMBER_KEY) ?? '') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [remember, setRemember] = useState(() => Boolean(localStorage.getItem(REMEMBER_KEY))) const [fieldErrors, setFieldErrors] = useState<{ email?: string password?: string phone?: string otp?: string }>({}) const [formError, setFormError] = useState(null) const [loading, setLoading] = useState(false) const [providerBusy, setProviderBusy] = useState | null>(null) const [demoEmail, setDemoEmail] = useState(null) const [phone, setPhone] = useState('') const [phoneStep, setPhoneStep] = useState('enter') const [otp, setOtp] = useState('') const [otpSending, setOtpSending] = useState(false) const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID as string | undefined const accountsByRole = useMemo( () => ROLE_ORDER.map((role) => ({ role, label: statusLabel('role', role), accounts: demoAccounts.filter((a) => a.role === role), })).filter((group) => group.accounts.length > 0), [], ) if (token) return const finishSignIn = (nextEmail: string) => { if (remember) localStorage.setItem(REMEMBER_KEY, nextEmail) else localStorage.removeItem(REMEMBER_KEY) toast.success(t('auth.signedIn')) navigate('/') } const signInEmail = (nextEmail: string, nextPassword: string) => { const result = login(nextEmail, nextPassword) if (!result.ok) { setFormError(result.error ?? t('auth.error.invalidCredentials')) toast.error(result.error ?? t('auth.error.invalidCredentials')) return false } finishSignIn(nextEmail) return true } const signInProvider = (provider: Exclude) => { const result = loginWithProvider(provider) if (!result.ok) { setFormError(result.error ?? t('auth.error.invalidCredentials')) toast.error(result.error ?? t('auth.error.invalidCredentials')) return false } toast.success(t('auth.signedIn'), { description: t('auth.providerSignedIn', { provider: t(`auth.providers.${provider}`) }), }) navigate('/') return true } const onSubmitEmail = (e: FormEvent) => { e.preventDefault() const errors: { email?: string; password?: string } = {} if (!email.trim()) errors.email = t('auth.error.emailRequired') if (!password) errors.password = t('auth.error.passwordRequired') setFieldErrors(errors) if (Object.keys(errors).length > 0) return setFormError(null) setLoading(true) window.setTimeout(() => { const ok = signInEmail(email.trim(), password) if (!ok) setLoading(false) }, 450) } const onSendOtp = () => { if (!isLikelyPhone(phone)) { setFieldErrors((prev) => ({ ...prev, phone: t('auth.error.phoneInvalid') })) return } setFieldErrors((prev) => ({ ...prev, phone: undefined, otp: undefined })) setFormError(null) setOtpSending(true) window.setTimeout(() => { setOtpSending(false) setPhoneStep('verify') setOtp('') toast.message(t('auth.otpSent'), { description: t('auth.otpSentHint', { code: MOCK_OTP }), }) }, 600) } const onVerifyOtp = (e: FormEvent) => { e.preventDefault() if (otp.trim() !== MOCK_OTP) { setFieldErrors((prev) => ({ ...prev, otp: t('auth.error.otpInvalid') })) return } setFieldErrors((prev) => ({ ...prev, otp: undefined })) setFormError(null) setLoading(true) window.setTimeout(() => { const ok = signInProvider('phone') if (!ok) setLoading(false) }, 450) } const onProviderClick = (provider: 'google' | 'telegram') => { setFormError(null) setProviderBusy(provider) window.setTimeout(() => { const ok = signInProvider(provider) if (!ok) setProviderBusy(null) }, 700) } const onPickDemoAccount = (account: (typeof demoAccounts)[number]) => { setEmail(account.email) setPassword(account.password) setCredentialMode('email') setFieldErrors({}) setFormError(null) setDemoEmail(account.email) window.setTimeout(() => { const ok = signInEmail(account.email, account.password) if (!ok) setDemoEmail(null) }, 450) } const busy = loading || demoEmail !== null || providerBusy !== null || otpSending return (
G
{t('app.name')}

Admin console

{t('auth.welcome')}

{t('auth.subtitle')}

{formError ? (
) : null}
{!googleClientId ? (

{t('auth.googleMockHint')}

) : null}

{botUsername ? t('auth.telegramWidgetHint', { bot: botUsername }) : t('auth.telegramMockHint')}

{t('auth.orContinueWith')}
{( [ { id: 'email' as const, icon: Mail, label: t('auth.email') }, { id: 'phone' as const, icon: Phone, label: t('auth.phone') }, ] as const ).map(({ id, icon: Icon, label }) => ( ))}
{credentialMode === 'email' ? (
{ setEmail(e.target.value) if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: undefined })) }} className={fieldErrors.email ? 'border-red-400 focus-visible:ring-red-400' : undefined} /> {fieldErrors.email ? (

{fieldErrors.email}

) : null}
{ setPassword(e.target.value) if (fieldErrors.password) setFieldErrors((prev) => ({ ...prev, password: undefined })) }} className={`pr-11 ${fieldErrors.password ? 'border-red-400 focus-visible:ring-red-400' : ''}`} />
{fieldErrors.password ? (

{fieldErrors.password}

) : null}
) : (
{phoneStep === 'enter' ? ( <>
{ setPhone(e.target.value) if (fieldErrors.phone) setFieldErrors((prev) => ({ ...prev, phone: undefined })) }} className={fieldErrors.phone ? 'border-red-400 focus-visible:ring-red-400' : undefined} /> {fieldErrors.phone ? (

{fieldErrors.phone}

) : (

{t('auth.phoneHint')}

)}
) : (
{ setOtp(e.target.value.replace(/\D/g, '').slice(0, 6)) if (fieldErrors.otp) setFieldErrors((prev) => ({ ...prev, otp: undefined })) }} className={cn( 'tracking-[0.35em] font-mono text-center text-lg', fieldErrors.otp ? 'border-red-400 focus-visible:ring-red-400' : undefined, )} /> {fieldErrors.otp ? (

{fieldErrors.otp}

) : (

{t('auth.otpHint', { phone: normalizePhone(phone), code: MOCK_OTP })}

)}
)}
)}

{t('auth.demoAccounts')}

Demo only

{t('auth.demoPickHint')} {t('auth.demoHint')}

{accountsByRole.map((group) => (

{group.label}

{group.accounts.map((account) => ( onPickDemoAccount(account)} > {account.name} {account.branchName ?? 'All branches'} · {account.email} ))}
))}

{t('app.poweredBy')} · © {new Date().getFullYear()}

) }