206 lines
7.9 KiB
TypeScript
206 lines
7.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { DevicePhoneMobileIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
import { api } from '../../lib/api';
|
|
import type { ApiResponse } from '../../lib/api';
|
|
import { sanitizeMobileInput } from '../../lib/utils';
|
|
|
|
interface NotificationMobileData {
|
|
notification_mobile: string | null;
|
|
}
|
|
|
|
interface Props {
|
|
target: 'doctor' | 'clinic';
|
|
}
|
|
|
|
export default function NotificationMobileCard({ target }: Props) {
|
|
const qc = useQueryClient();
|
|
const [step, setStep] = useState<'idle' | 'enter_mobile' | 'enter_otp'>('idle');
|
|
const [newMobile, setNewMobile] = useState('');
|
|
const [otpCode, setOtpCode] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['notification-mobile', target],
|
|
queryFn: () => api.get<ApiResponse<NotificationMobileData>>(`/api/v1/notification-mobile/${target}`),
|
|
staleTime: 60_000,
|
|
});
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const current: string | null = ((data?.data as any)?.data ?? data?.data)?.notification_mobile ?? null;
|
|
|
|
const requestOtp = useMutation({
|
|
mutationFn: (mobile: string) =>
|
|
api.post<ApiResponse<{ message: string }>>('/api/v1/notification-mobile/request-otp', {
|
|
target, new_mobile: mobile,
|
|
}),
|
|
onSuccess: () => {
|
|
setError(null);
|
|
setStep('enter_otp');
|
|
},
|
|
onError: (e: unknown) => setError(String(e)),
|
|
});
|
|
|
|
const verify = useMutation({
|
|
mutationFn: (code: string) =>
|
|
api.post<ApiResponse<{ notification_mobile: string }>>('/api/v1/notification-mobile/verify', {
|
|
target, otp_code: code,
|
|
}),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['notification-mobile', target] });
|
|
setStep('idle');
|
|
setNewMobile('');
|
|
setOtpCode('');
|
|
setError(null);
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
onError: (e: any) => setError(e?.message ?? 'خطا در تأیید کد'),
|
|
});
|
|
|
|
const remove = useMutation({
|
|
mutationFn: () => api.delete<ApiResponse<unknown>>(`/api/v1/notification-mobile/${target}`),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['notification-mobile', target] }),
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <div className="skeleton" style={{ height: 100, borderRadius: 'var(--r)' }} />;
|
|
}
|
|
|
|
return (
|
|
<div className="card card-pad" style={{ marginTop: 'var(--gap)' }}>
|
|
<div className="card-title-row" style={{ marginBottom: '1rem' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<div className="ico" style={{ background: 'var(--info-bg)', color: 'var(--info)', width: 36, height: 36, borderRadius: 10 }}>
|
|
<DevicePhoneMobileIcon style={{ width: 18, height: 18 }} />
|
|
</div>
|
|
<div>
|
|
<h3 style={{ fontSize: 14.5, fontWeight: 600 }}>شماره اعلان نوبت</h3>
|
|
<p className="muted" style={{ fontSize: 12, marginTop: 2 }}>
|
|
پیامک نوبت جدید به این شماره ارسال میشود
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* نمایش شماره فعلی */}
|
|
{current && step === 'idle' && (
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px',
|
|
background: 'var(--success-bg)', borderRadius: 'var(--r-sm)',
|
|
marginBottom: '1rem',
|
|
}}>
|
|
<CheckCircleIcon style={{ width: 18, height: 18, color: 'var(--success)', flexShrink: 0 }} />
|
|
<span style={{ fontWeight: 600, fontSize: 14, direction: 'ltr', flex: 1 }}>{current}</span>
|
|
<button
|
|
className="mini-btn"
|
|
onClick={() => remove.mutate()}
|
|
disabled={remove.isPending}
|
|
title="حذف"
|
|
>
|
|
<XMarkIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!current && step === 'idle' && (
|
|
<p className="muted" style={{ fontSize: 13, marginBottom: '1rem' }}>
|
|
شمارهای تنظیم نشده است.
|
|
</p>
|
|
)}
|
|
|
|
{/* مرحله ۱: ورود شماره جدید */}
|
|
{step === 'idle' && (
|
|
<button
|
|
className="btn ghost sm"
|
|
onClick={() => { setStep('enter_mobile'); setError(null); }}
|
|
>
|
|
{current ? 'تغییر شماره' : 'تنظیم شماره اعلان'}
|
|
</button>
|
|
)}
|
|
|
|
{step === 'enter_mobile' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 5 }}>
|
|
شماره موبایل جدید
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={newMobile}
|
|
dir="ltr"
|
|
inputMode="numeric"
|
|
maxLength={11}
|
|
onChange={e => setNewMobile(sanitizeMobileInput(e.target.value))}
|
|
placeholder="09XXXXXXXXX"
|
|
style={{
|
|
width: '100%', maxWidth: 220, height: 38, padding: '0 12px',
|
|
borderRadius: 'var(--r-sm)', border: '1px solid var(--border)',
|
|
background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5,
|
|
boxSizing: 'border-box',
|
|
}}
|
|
/>
|
|
</div>
|
|
{error && <p style={{ color: 'var(--danger)', fontSize: 12.5 }}>{error}</p>}
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
className="btn primary sm"
|
|
onClick={() => requestOtp.mutate(newMobile)}
|
|
disabled={requestOtp.isPending || !/^09\d{9}$/.test(newMobile)}
|
|
>
|
|
{requestOtp.isPending ? 'در حال ارسال...' : 'ارسال کد تأیید'}
|
|
</button>
|
|
<button className="btn ghost sm" onClick={() => { setStep('idle'); setError(null); }}>
|
|
انصراف
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* مرحله ۲: ورود کد OTP */}
|
|
{step === 'enter_otp' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<p style={{ fontSize: 13, color: 'var(--text-2)' }}>
|
|
کد ۶ رقمی ارسالشده به <b style={{ direction: 'ltr', display: 'inline-block' }}>{newMobile}</b> را وارد کنید.
|
|
</p>
|
|
<div>
|
|
<input
|
|
type="text"
|
|
value={otpCode}
|
|
onChange={e => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
placeholder="_ _ _ _ _ _"
|
|
dir="ltr"
|
|
maxLength={6}
|
|
style={{
|
|
width: 140, height: 44, padding: '0 12px', textAlign: 'center',
|
|
letterSpacing: 8, fontSize: 22, fontWeight: 700,
|
|
borderRadius: 'var(--r-sm)', border: '1.5px solid var(--border)',
|
|
background: 'var(--surface)', color: 'var(--text)', boxSizing: 'border-box',
|
|
}}
|
|
/>
|
|
</div>
|
|
{error && <p style={{ color: 'var(--danger)', fontSize: 12.5 }}>{error}</p>}
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
className="btn primary sm"
|
|
onClick={() => verify.mutate(otpCode)}
|
|
disabled={verify.isPending || otpCode.length !== 6}
|
|
>
|
|
{verify.isPending ? 'در حال تأیید...' : 'تأیید'}
|
|
</button>
|
|
<button
|
|
className="btn ghost sm"
|
|
onClick={() => requestOtp.mutate(newMobile)}
|
|
disabled={requestOtp.isPending}
|
|
>
|
|
ارسال مجدد
|
|
</button>
|
|
<button className="btn ghost sm" onClick={() => { setStep('idle'); setError(null); }}>
|
|
انصراف
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|