pwa setting

This commit is contained in:
hamed
2026-06-13 13:28:39 +03:30
parent 0b5d017b14
commit d50fb96542
15 changed files with 782 additions and 0 deletions
+112
View File
@@ -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">
+3
View File
@@ -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>
);
}