fix(secretary): gate CRUD action buttons across all panel pages by permission
Backend already returned 403 for ungranted secretary actions, but the UI still showed the add/edit/delete buttons (e.g. clinic-services showed «بخش جدید» to a secretary without services.create). Sweep every secretary-reachable page so each create/edit/delete/manage control renders only when the matching usePermissions().can(resource, action) is true. Owner/doctor/clinic are unaffected — can() returns true when there is no permission context — so this restricts only secretaries and mirrors the server checks. Pages/components gated (resource): - services: ClinicServicesPage, ServiceDetailPage (+ its tabs) - inventory: InventoryPage, InventoryItemsTable, InventoryActionsMenu, PackagesView - tags: TagsSettingsPage · staff: StaffPage · discounts: DiscountTab - sms: SmsWalletPage · insurances: TenantInsuranceContracts - clinic_doctors: ClinicDoctorsPage + ClinicDoctorsManager (props, default true) - patients: PatientsListPage, MyPatientsPage, PatientDetailPage (records/notes/ sessions/attachments/calls/wallet — create/update/delete split) - appointments: AppointmentsPage (add + empty-slot booking gated by create), TurnsTable (status dropdown → read-only badge without update_status; actions menu hidden without manage/cancel) - appointment_settings: AppointmentSettingsPage + ClinicAppointmentSettingsPage pass readOnly to ScheduleSection + FreeVisitPrice (new readOnly prop) Not gated: view/read, search, filter, tabs, navigation, export, and modal submit buttons reachable only via an already-gated trigger. tsc clean; full frontend suite 501/501 passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import type { TurnsViewMode } from '../components/appointments/TurnsViewToggle';
|
||||
import DoctorTabs from '../components/appointments/DoctorTabs';
|
||||
import TurnsTimeline from '../components/appointments/TurnsTimeline';
|
||||
import TurnsTable from '../components/appointments/TurnsTable';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
|
||||
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
|
||||
import { CANCELLED_STATUSES } from '../components/appointments/turnStatus';
|
||||
@@ -497,6 +498,11 @@ export default function AppointmentsPage() {
|
||||
// منشیِ محیطِ کلینیک باید مثل کلینیک چندپزشکه رفتار کند: تب پزشکان + تایملاین.
|
||||
// dbUuid در این محیط uuid کلینیک است (نه پزشک) — همان مبنای clinic/doctor-list.
|
||||
const isClinicScopedSecretary = primaryRole === 'secretary' && scope === 'clinic';
|
||||
// مجوزهای منشی روی نوبتها؛ برای owner/پزشک همیشه true.
|
||||
const { can } = usePermissions();
|
||||
const canCreateAppt = can('appointments', 'create');
|
||||
const canManageAppt = can('appointments', 'update_status');
|
||||
const canCancelAppt = can('appointments', 'cancel');
|
||||
|
||||
const [params] = useSearchParams();
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
@@ -715,7 +721,7 @@ export default function AppointmentsPage() {
|
||||
|
||||
// ── Slot click → quick booking modal
|
||||
function handleSlotClick(slot: TimelineSlot) {
|
||||
if (isRepresentation) return;
|
||||
if (isRepresentation || !canCreateAppt) return;
|
||||
const doctorName = doctors.find(d => d.uuid === selectedDoctorUuid)?.name ?? '';
|
||||
setBookingSlot({
|
||||
start: slot.start,
|
||||
@@ -786,7 +792,7 @@ export default function AppointmentsPage() {
|
||||
<AdjustmentsHorizontalIcon style={{ width: 16 }} />
|
||||
</button>
|
||||
|
||||
{!isRepresentation && (
|
||||
{!isRepresentation && canCreateAppt && (
|
||||
<button
|
||||
className="btn primary sm"
|
||||
onClick={() => {
|
||||
@@ -817,6 +823,8 @@ export default function AppointmentsPage() {
|
||||
loading={apptQuery.isLoading}
|
||||
queryKey={apptQueryKey}
|
||||
showDoctor={showDoctorCol}
|
||||
canManage={canManageAppt}
|
||||
canCancel={canCancelAppt}
|
||||
/>
|
||||
{filteredAppointments.length > TABLE_PAGE_SIZE && (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
|
||||
Reference in New Issue
Block a user