feat: add doctor invitation modal and appointment creation API
- Implemented InviteDoctorModal component for inviting doctors to clinics. - Updated ClinicDashboard to include a button for inviting doctors and handle modal state. - Added createAppointment API endpoint in AdminApiController for scheduling appointments. - Enhanced ClinicInvitationController to check user access when inviting doctors. - Updated MyAppointmentsController to ensure unique appointment records. - Added seed_test_data.php for populating test data including doctors, clinics, and appointments. - Refactored styles to include new appointment status badges and updated font imports.
This commit is contained in:
@@ -10,6 +10,7 @@ import { api } from '../lib/api';
|
||||
import type { ApiResponse } from '../lib/api';
|
||||
import { formatNumber, formatRial, formatDateTime } from '../lib/utils';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import InviteDoctorModal from '../components/ui/InviteDoctorModal';
|
||||
|
||||
// ── Shared Status Maps ────────────────────────────────────────────────────
|
||||
|
||||
@@ -514,7 +515,8 @@ interface ClinicDashboardData {
|
||||
}
|
||||
|
||||
function ClinicDashboard() {
|
||||
const { context } = useAuthStore();
|
||||
const { context, dbUuid } = useAuthStore();
|
||||
const [inviteOpen, setInviteOpen] = useState(false);
|
||||
const q = useQuery({
|
||||
queryKey: ['dashboard-clinic'],
|
||||
queryFn: () => api.get<ApiResponse<ClinicDashboardData>>('/api/v1/dashboard/clinic'),
|
||||
@@ -524,6 +526,7 @@ function ClinicDashboard() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = useMemo<ClinicDashboardData | undefined>(() => (q.data?.data as any)?.data ?? q.data?.data, [q.data]);
|
||||
const today = new Date().toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const clinicUuid = d?.clinic.uuid ?? dbUuid ?? '';
|
||||
|
||||
if (q.isLoading) return <LoadingSkeleton />;
|
||||
|
||||
@@ -541,10 +544,16 @@ function ClinicDashboard() {
|
||||
<h1 className="section-title">داشبورد کلینیک</h1>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>{today} · {d?.clinic.name ?? context?.name ?? ''}</div>
|
||||
</div>
|
||||
<button className="btn ghost sm" onClick={() => q.refetch()}>
|
||||
<ArrowPathIcon style={{ width: 14, height: 14 }} />
|
||||
بهروزرسانی
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn primary sm" onClick={() => setInviteOpen(true)}>
|
||||
<UserIcon style={{ width: 14, height: 14 }} />
|
||||
دعوت پزشک
|
||||
</button>
|
||||
<button className="btn ghost sm" onClick={() => q.refetch()}>
|
||||
<ArrowPathIcon style={{ width: 14, height: 14 }} />
|
||||
بهروزرسانی
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-grid">
|
||||
@@ -571,29 +580,48 @@ function ClinicDashboard() {
|
||||
<div className="card card-pad">
|
||||
<div className="card-title-row">
|
||||
<h3 style={{ fontSize: 16 }}>پزشکان کلینیک</h3>
|
||||
<Link to="/admin/doctors" className="link">همه</Link>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button className="btn ghost sm" style={{ fontSize: 11, padding: '3px 10px' }} onClick={() => setInviteOpen(true)}>
|
||||
+ دعوت جدید
|
||||
</button>
|
||||
{clinicUuid && (
|
||||
<Link to={`/admin/clinics/${clinicUuid}`} className="link">مدیریت</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!d?.doctors.length ? (
|
||||
<p className="muted" style={{ textAlign: 'center', padding: '32px 0', fontSize: 13.5 }}>پزشکی ثبت نشده</p>
|
||||
<div style={{ textAlign: 'center', padding: '24px 0' }}>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginBottom: 12 }}>هنوز پزشکی دعوت نشده</p>
|
||||
<button className="btn primary sm" onClick={() => setInviteOpen(true)}>
|
||||
دعوت اولین پزشک
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{d.doctors.map((doc, i) => (
|
||||
<Link
|
||||
<div
|
||||
key={doc.uuid}
|
||||
to={`/admin/doctors/${doc.uuid}`}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 0', borderBottom: i < d.doctors.length - 1 ? '1px solid var(--border)' : 'none', textDecoration: 'none', color: 'inherit' }}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 0', borderBottom: i < d.doctors.length - 1 ? '1px solid var(--border)' : 'none' }}
|
||||
>
|
||||
<AvatarEl initials={doc.name.slice(0, 1)} hue={162} size="sm" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<b style={{ fontSize: 13.5 }}>دکتر {doc.name}</b>
|
||||
</div>
|
||||
<span className="badge blue"><span className="bdot" />{formatNumber(doc.today_count)} امروز</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{inviteOpen && clinicUuid && (
|
||||
<InviteDoctorModal
|
||||
clinicUuid={clinicUuid}
|
||||
onClose={() => setInviteOpen(false)}
|
||||
onInvited={() => q.refetch()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user