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:
@@ -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 */}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user