pwa setting
This commit is contained in:
@@ -2014,6 +2014,116 @@ const editSchema = z.object({
|
||||
});
|
||||
type EditForm = z.infer<typeof editSchema>;
|
||||
|
||||
// ── Clinic Invitations Section ─────────────────────────────────────────────
|
||||
|
||||
interface InvitationItem {
|
||||
uuid: string;
|
||||
status: string;
|
||||
invited_name: string | null;
|
||||
invited_specialty: string | null;
|
||||
invited_at: number;
|
||||
expires_at: number;
|
||||
clinic: { uuid: string; name: string; logo: string | null };
|
||||
}
|
||||
|
||||
function ClinicInvitationsSection() {
|
||||
const qc = useQueryClient();
|
||||
const [respondingUuid, setRespondingUuid] = useState<string | null>(null);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['doctor-my-invitations'],
|
||||
queryFn: () => api.get<ApiResponse<InvitationItem[]>>('/api/v1/doctor/invitations'),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const invitations: InvitationItem[] = useMemo(() => {
|
||||
const raw = data?.data;
|
||||
return (raw as any)?.data ?? raw ?? [];
|
||||
}, [data]);
|
||||
|
||||
const respondMut = useMutation({
|
||||
mutationFn: ({ uuid, action }: { uuid: string; action: 'accept' | 'reject' }) =>
|
||||
api.post<ApiResponse<any>>(`/api/v1/doctor/invitation/${uuid}/respond`, { action }),
|
||||
onSuccess: (_, vars) => {
|
||||
toast.success(vars.action === 'accept' ? 'دعوتنامه پذیرفته شد' : 'دعوتنامه رد شد');
|
||||
setRespondingUuid(null);
|
||||
qc.invalidateQueries({ queryKey: ['doctor-my-invitations'] });
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
if (!isLoading && invitations.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="cp-card p-6">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 8, background: 'var(--primary-soft)', color: 'var(--primary-700)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<BuildingOfficeIcon style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>دعوتنامههای کلینیک</span>
|
||||
{invitations.length > 0 && (
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--primary-700)', background: 'var(--primary-soft)', padding: '2px 10px', borderRadius: 999 }}>
|
||||
{invitations.length} دعوت جدید
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{[1, 2].map(i => <div key={i} className="skeleton" style={{ height: 72, borderRadius: 'var(--r)' }} />)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{invitations.map(inv => {
|
||||
const isExpired = Date.now() / 1000 > inv.expires_at;
|
||||
const busy = respondMut.isPending && respondingUuid === inv.uuid;
|
||||
return (
|
||||
<div key={inv.uuid} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', borderRadius: 'var(--r)', border: '1px solid var(--border)', background: 'var(--surface-2)' }}>
|
||||
{/* Logo */}
|
||||
<div style={{ width: 44, height: 44, borderRadius: 10, overflow: 'hidden', flexShrink: 0, background: 'var(--surface)', border: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{inv.clinic.logo
|
||||
? <img src={inv.clinic.logo} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
: <BuildingOfficeIcon style={{ width: 20, height: 20, color: 'var(--text-3)' }} />
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontWeight: 700, fontSize: 14, color: 'var(--text)', marginBottom: 3 }}>{inv.clinic.name}</p>
|
||||
{inv.invited_specialty && (
|
||||
<p style={{ fontSize: 12, color: 'var(--text-2)' }}>تخصص: {inv.invited_specialty}</p>
|
||||
)}
|
||||
<p style={{ fontSize: 11, color: isExpired ? 'var(--danger)' : 'var(--text-3)', marginTop: 2 }}>
|
||||
{isExpired ? 'منقضی شده' : `انقضا: ${new Date(inv.expires_at * 1000).toLocaleDateString('fa-IR')}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{!isExpired && (
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<button
|
||||
disabled={busy}
|
||||
onClick={() => { setRespondingUuid(inv.uuid); respondMut.mutate({ uuid: inv.uuid, action: 'reject' }); }}
|
||||
style={{ height: 34, padding: '0 14px', borderRadius: 'var(--r-sm)', border: '1.5px solid var(--border)', background: 'var(--surface)', color: 'var(--text-2)', fontSize: 13, fontWeight: 600, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.5 : 1 }}>
|
||||
رد
|
||||
</button>
|
||||
<button
|
||||
disabled={busy}
|
||||
onClick={() => { setRespondingUuid(inv.uuid); respondMut.mutate({ uuid: inv.uuid, action: 'accept' }); }}
|
||||
style={{ height: 34, padding: '0 14px', borderRadius: 'var(--r-sm)', border: '1.5px solid var(--primary)', background: 'var(--primary)', color: 'var(--on-primary)', fontSize: 13, fontWeight: 600, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.5 : 1 }}>
|
||||
{busy ? '...' : 'پذیرفتن'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ──────────────────────────────────────────────────────────────
|
||||
|
||||
export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfile?: boolean }) {
|
||||
@@ -2380,6 +2490,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
|
||||
{uuid && <ScheduleSection doctorUuid={uuid} />}
|
||||
|
||||
{isOwnProfile && <ClinicInvitationsSection />}
|
||||
|
||||
{doctor.clinics && doctor.clinics.length > 0 && (
|
||||
<div className="cp-card p-6">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
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;
|
||||
@@ -367,6 +368,8 @@ export default function LoginPage() {
|
||||
ClinicPro — نسخه ۱.۰.۰
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PwaLoginCard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user