feat(resources): one tabbed page per resource

In the resource-first model a resource is the unit of capacity, so its
working hours, holidays, services, skills and categories belong to it — not
scattered across a list page's modals plus a separate calendar page.

/admin/resources/{uuid} now carries six tabs and the active tab lives in the
query string, so back and refresh land on the same view. The old
/calendar URL redirects to ?tab=hours instead of 404ing.

The skills and services modal bodies became panels the tab renders directly;
the modals are now thin wrappers, so the list page keeps working unchanged
and there is still one implementation of each editor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-02 13:06:06 +03:30
co-authored by Claude Opus 5
parent 03d8d7d68a
commit e08876f7b4
15 changed files with 1126 additions and 705 deletions
+11 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { Routes, Route, Navigate, useLocation, useParams } from 'react-router-dom';
import { useAuthStore } from './stores/authStore';
import { usePermissions } from './hooks/usePermissions';
import AdminLayout from './components/layout/AdminLayout';
@@ -84,7 +84,7 @@ import ResourceTypesPage from './pages/ResourceTypesPage';
import CatalogCategoriesPage from './pages/CatalogCategoriesPage';
import SkillsPage from './pages/SkillsPage';
import ResourcePoolsPage from './pages/ResourcePoolsPage';
import ResourceCalendarPage from './pages/ResourceCalendarPage';
import ResourceDetailPage from './pages/ResourceDetailPage';
import HolidaysSettingsPage from './pages/HolidaysSettingsPage';
import PatientRecordFormPage from './pages/PatientRecordFormPage';
import PatientDetailPage from './pages/PatientDetailPage';
@@ -132,6 +132,12 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
/** `/resources/{uuid}/calendar` قدیمی → تب «ساعات کاری» صفحهٔ منبع. */
function ResourceCalendarRedirect() {
const { resourceUuid } = useParams<{ resourceUuid: string }>();
return <Navigate to={`/admin/resources/${resourceUuid}?tab=hours`} replace />;
}
function PublicRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
return isAuthenticated ? <Navigate to="/admin/dashboard" replace /> : <>{children}</>;
@@ -297,7 +303,9 @@ export default function App() {
<Route path="resources/types" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['appointment_settings', 'view']}><ResourceTypesPage /></RoleRoute>} />
<Route path="resources/skills" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['appointment_settings', 'view']}><SkillsPage /></RoleRoute>} />
<Route path="resources/pools" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['appointment_settings', 'view']}><ResourcePoolsPage /></RoleRoute>} />
<Route path="resources/:resourceUuid/calendar" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['appointment_settings', 'view']}><ResourceCalendarPage /></RoleRoute>} />
<Route path="resources/:resourceUuid" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['appointment_settings', 'view']}><ResourceDetailPage /></RoleRoute>} />
{/* تقویم منبع در تب «ساعات کاری» همان صفحه حل شده؛ لینک‌های قدیمی نباید بشکنند. */}
<Route path="resources/:resourceUuid/calendar" element={<ResourceCalendarRedirect />} />
<Route path="holidays" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['appointment_settings', 'view']}><HolidaysSettingsPage /></RoleRoute>} />
<Route path="sms-wallet" element={<RoleRoute roles={['doctor', 'clinic', 'secretary']} blockClinicScope permission={['sms', 'view']}><SmsWalletPage /></RoleRoute>} />
<Route path="my-secretaries" element={<RoleRoute roles={['doctor', 'clinic']} blockClinicScope><MySecretariesPage /></RoleRoute>} />