feat: enhance ClinicDetailPage with dynamic tab management in EditModal

- Added initialTab prop to EditModal for setting the active tab on open.
- Updated state management in ClinicDetailPage to handle initial tab for editing.
- Refactored openEdit function to set the initial tab before opening the edit modal.
- Combined specialties, insurances, and services sections in the sidebar for better organization.
- Improved modal rendering using createPortal for better context handling.

style: increase z-index for modal overlay

- Updated the z-index of the overlay class in styles.css to ensure modals appear above other elements.

feat: implement multi-role dashboard functionality

- Created a new prompt for multi-role dashboard implementation.
- Defined roles and their access levels in the admin panel.
- Updated backend to support user role identification and context retrieval.
- Enhanced frontend to dynamically render components based on user roles.
- Added new routes and components for role-specific dashboards.

chore: add skills for admin endpoint and page creation

- Created SKILL.md files for adding admin endpoints and pages.
- Provided templates and guidelines for implementing new admin features.

chore: sync database after entity changes

- Added a new skill for syncing the database after any entity modifications.
This commit is contained in:
hamed
2026-06-11 09:28:51 +03:30
parent af0ae51987
commit e88ae9bf9c
7 changed files with 1475 additions and 955 deletions
+74 -52
View File
@@ -258,8 +258,9 @@ async function geocodeCity(name: string): Promise<[number, number] | null> {
// ── Edit Modal ─────────────────────────────────────────────────────────────
function EditModal({ clinic, onClose, onSaved }: {
function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
clinic: ClinicDetail; onClose: () => void; onSaved: () => void;
initialTab?: 'basic' | 'location' | 'tags';
}) {
const [mapFlyTarget, setMapFlyTarget] = useState<[number, number] | null>(null);
@@ -347,7 +348,7 @@ function EditModal({ clinic, onClose, onSaved }: {
onError: (e: Error) => toast.error(e.message),
});
const [activeTab, setActiveTab] = useState<'basic' | 'location' | 'tags'>('basic');
const [activeTab, setActiveTab] = useState<'basic' | 'location' | 'tags'>(initialTab);
return (
<div className="overlay" onClick={onClose}>
@@ -567,9 +568,10 @@ export default function ClinicDetailPage() {
const { uuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
const qc = useQueryClient();
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [editInitialTab, setEditInitialTab] = useState<'basic' | 'location' | 'tags'>('basic');
const [deleteOpen, setDeleteOpen] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false);
const [doctorsTab, setDoctorsTab] = useState<'doctors' | 'invitations'>('doctors');
const logoInputRef = useRef<HTMLInputElement>(null);
const galleryInputRef = useRef<HTMLInputElement>(null);
@@ -606,6 +608,11 @@ export default function ClinicDetailPage() {
const invitationList: ClinicInvitation[] = invitationsQ.data?.data ?? [];
const openEdit = (tab: 'basic' | 'location' | 'tags' = 'basic') => {
setEditInitialTab(tab);
setEditOpen(true);
};
const toggleMut = useMutation({
mutationFn: () => api.patch<ApiResponse<any>>(`/api/v1/admin/clinic/${uuid}/status`, {}),
onSuccess: () => {
@@ -679,8 +686,8 @@ export default function ClinicDetailPage() {
const json = await res.json();
const url = json?.data?.url;
if (url && clinic) {
const existing = (clinic.images_clinic ?? []).filter(img => img?.url).map(img => img.url);
await api.patch(`/api/v1/clinic/${uuid}`, { image_clinic: [...existing, url] });
const existing = (clinic.images_clinic ?? []).filter(img => img?.url);
await api.patch(`/api/v1/clinic/${uuid}`, { image_clinic: [...existing, { url }] });
toast.success('تصویر اضافه شد');
qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] });
}
@@ -735,7 +742,7 @@ export default function ClinicDetailPage() {
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn ghost sm" onClick={() => setEditOpen(true)}>
<button className="btn ghost sm" onClick={() => openEdit('basic')}>
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش
</button>
<button className={`btn sm ${clinic.is_active ? 'soft' : 'primary'}`}
@@ -970,67 +977,82 @@ export default function ClinicDetailPage() {
{/* Right sidebar */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
{/* Specialties */}
{/* Specialties + Insurances + Services combined */}
<div className="card card-pad">
<b style={{ fontSize: 13, display: 'block', marginBottom: 10 }}>
تخصصها ({formatNumber((clinic.specialties ?? []).length)})
</b>
{(clinic.specialties ?? []).length === 0
? <p className="muted" style={{ fontSize: 12 }}>تخصصی ثبت نشده</p>
: <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{clinic.specialties.map(s => (
<span key={s.id} className="badge violet"><span className="bdot" />{s.name}</span>
))}
</div>
}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<b style={{ fontSize: 13 }}>تخصصها، بیمه و خدمات</b>
<button className="btn ghost sm" style={{ fontSize: 12 }} onClick={() => openEdit('tags')}>
<PencilIcon style={{ width: 13, height: 13 }} /> ویرایش
</button>
</div>
{/* Insurances */}
<div className="card card-pad">
<b style={{ fontSize: 13, display: 'block', marginBottom: 10 }}>
بیمهها ({formatNumber((clinic.list_bime ?? []).length)})
</b>
{(clinic.list_bime ?? []).length === 0
? <p className="muted" style={{ fontSize: 12 }}>بیمهای ثبت نشده</p>
: <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{clinic.list_bime.map(ins => (
<span key={ins.id} className="badge blue"><span className="bdot" />{ins.name}</span>
))}
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<div className="muted" style={{ fontSize: 11, marginBottom: 6 }}>
تخصصها ({formatNumber((clinic.specialties ?? []).length)})
</div>
}
</div>
{(clinic.specialties ?? []).length === 0
? <p className="muted" style={{ fontSize: 12 }}>ثبت نشده</p>
: <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{clinic.specialties.map(s => (
<span key={s.id} className="badge violet"><span className="bdot" />{s.name}</span>
))}
</div>
}
</div>
{/* Services */}
<div className="card card-pad">
<b style={{ fontSize: 13, display: 'block', marginBottom: 10 }}>
خدمات ({formatNumber((clinic.services ?? []).length)})
</b>
{(clinic.services ?? []).length === 0
? <p className="muted" style={{ fontSize: 12 }}>خدمتی ثبت نشده</p>
: <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{clinic.services.map(s => (
<span key={s.id} className="badge green"><span className="bdot" />{s.name}</span>
))}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 12 }}>
<div className="muted" style={{ fontSize: 11, marginBottom: 6 }}>
بیمهها ({formatNumber((clinic.list_bime ?? []).length)})
</div>
}
{(clinic.list_bime ?? []).length === 0
? <p className="muted" style={{ fontSize: 12 }}>ثبت نشده</p>
: <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{clinic.list_bime.map(ins => (
<span key={ins.id} className="badge blue"><span className="bdot" />{ins.name}</span>
))}
</div>
}
</div>
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 12 }}>
<div className="muted" style={{ fontSize: 11, marginBottom: 6 }}>
خدمات ({formatNumber((clinic.services ?? []).length)})
</div>
{(clinic.services ?? []).length === 0
? <p className="muted" style={{ fontSize: 12 }}>ثبت نشده</p>
: <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{clinic.services.map(s => (
<span key={s.id} className="badge green"><span className="bdot" />{s.name}</span>
))}
</div>
}
</div>
</div>
</div>
</div>
</div>
{/* Edit modal */}
{editOpen && (
<EditModal clinic={clinic} onClose={() => setEditOpen(false)}
onSaved={() => { qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] }); qc.invalidateQueries({ queryKey: ['admin-clinics'] }); }} />
{/* Edit modal — portal to escape Leaflet transform context */}
{editOpen && createPortal(
<EditModal
clinic={clinic}
initialTab={editInitialTab}
onClose={() => setEditOpen(false)}
onSaved={() => { qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] }); qc.invalidateQueries({ queryKey: ['admin-clinics'] }); }}
/>,
document.body,
)}
{/* Invite doctor modal */}
{inviteOpen && uuid && (
{inviteOpen && uuid && createPortal(
<InviteModal
clinicUuid={uuid}
onClose={() => setInviteOpen(false)}
onInvited={() => { qc.invalidateQueries({ queryKey: ['clinic-invitations', uuid] }); setDoctorsTab('invitations'); }}
/>
/>,
document.body,
)}
{/* Delete confirm */}
+1 -1
View File
@@ -523,7 +523,7 @@ table.t tbody tr:hover .row-actions { opacity: 1; }
/* ── Modal ───────────────────────────────────────────────────── */
.overlay {
position: fixed; inset: 0; z-index: 80; display: grid; place-items: center; padding: 20px;
position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 20px;
background: rgba(8,13,22,.5); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
animation: fade-in .2s var(--ease);
}