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:
hamed
2026-06-11 13:36:54 +03:30
parent 04b51273e3
commit 82e1c264a1
18 changed files with 1095 additions and 274 deletions
+39 -11
View File
@@ -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>
);
}