Skip to content

Commit dd87eee

Browse files
authored
SBCQ-112: Category policy (#94)
2 parents 912f355 + cd51d63 commit dd87eee

5 files changed

Lines changed: 99 additions & 12 deletions

File tree

src/app/protected/settings/service-categories/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { revalidatePath } from "next/cache"
2+
import { headers } from "next/headers"
23
import { ServiceCategoryTable } from "@/components/settings/service_categories/ServiceCategoryTable"
34
import { getAllServices } from "@/lib/prisma/service/getAllServices"
45
import { getAllServiceCategories } from "@/lib/prisma/service_category/getAllServiceCategories"
6+
import { getStaffUserBySub } from "@/lib/prisma/staff_user/getStaffUserBySub"
7+
import { getAuthContext } from "@/utils/auth/getAuthContext"
58

69
// This page should always be rendered dynamically to ensure fresh data
710
export const dynamic = "force-dynamic"
811

912
export default async function Page() {
13+
const headersList = await headers()
14+
const authContext = getAuthContext(headersList)
15+
const user = authContext?.user
16+
17+
const currentUser = await getStaffUserBySub(user?.sub ?? "")
1018
const serviceCategories = await getAllServiceCategories()
1119
const services = await getAllServices()
1220

@@ -19,6 +27,7 @@ export default async function Page() {
1927
<div className="space-y-sm">
2028
<h2>Service Categories</h2>
2129
<ServiceCategoryTable
30+
currentUser={currentUser}
2231
serviceCategories={serviceCategories}
2332
services={services}
2433
revalidateTable={revalidateTable}

src/components/settings/service_categories/EditServiceCategoryModal/EditServiceCategoryModal.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type EditServiceCategoryModalProps = {
1919
onClose: () => void
2020
serviceCategory: ServiceCategoryWithRelations | null
2121
services: Service[]
22+
canEdit: boolean
23+
canArchive: boolean
2224
updateServiceCategory: (
2325
serviceCategory: Partial<ServiceCategoryWithRelations>
2426
) => Promise<ServiceCategoryWithRelations | null>
@@ -31,6 +33,8 @@ export const EditServiceCategoryModal = ({
3133
onClose,
3234
serviceCategory,
3335
services,
36+
canEdit,
37+
canArchive,
3438
updateServiceCategory,
3539
revalidateTable,
3640
openConfirmArchiveServiceCategoryModal,
@@ -86,7 +90,7 @@ export const EditServiceCategoryModal = ({
8690
if (!serviceCategory || !formData) return null
8791

8892
const isArchived = serviceCategory.deletedAt !== null
89-
const isReadonly = isArchived
93+
const isReadonly = isArchived || !canEdit
9094

9195
const hasMadeChanges = JSON.stringify(formData) !== JSON.stringify(serviceCategory)
9296

@@ -123,13 +127,19 @@ export const EditServiceCategoryModal = ({
123127

124128
<DialogBody>
125129
<form className="space-y-5">
126-
{isReadonly && (
130+
{!canEdit && (
127131
<div className="flex flex-col gap-1 rounded-md border-l-4 border-l-red-600 bg-red-50 p-4">
128-
{isArchived && (
129-
<p className="text-sm font-medium text-red-800">
130-
This service category is archived and cannot be edited.
131-
</p>
132-
)}
132+
<p className="text-sm font-medium text-red-800">
133+
You do not have permission to edit this service category.
134+
</p>
135+
</div>
136+
)}
137+
138+
{isArchived && (
139+
<div className="flex flex-col gap-1 rounded-md border-l-4 border-l-red-600 bg-red-50 p-4">
140+
<p className="text-sm font-medium text-red-800">
141+
This service category is archived and cannot be edited.
142+
</p>
133143
</div>
134144
)}
135145

@@ -152,9 +162,11 @@ export const EditServiceCategoryModal = ({
152162
<button type="button" className="tertiary" onClick={onClose}>
153163
Cancel
154164
</button>
155-
<button type="button" className="secondary danger" onClick={handleOpenArchive}>
156-
{isArchived ? "Unarchive" : "Archive"}
157-
</button>
165+
{canArchive && (
166+
<button type="button" className="secondary danger" onClick={handleOpenArchive}>
167+
{isArchived ? "Unarchive" : "Archive"}
168+
</button>
169+
)}
158170
<button
159171
type="button"
160172
className="primary"

src/components/settings/service_categories/ServiceCategoryTable/ServiceCategoryTable.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
"use client"
22

3-
import { useState } from "react"
3+
import { useEffect, useMemo, useState } from "react"
44
import { DataTable } from "@/components/common/datatable"
55
import { Switch } from "@/components/common/switch"
66
import type { Service } from "@/generated/prisma/client"
7+
import { useAuth } from "@/hooks/useAuth"
78
import { useDialog } from "@/hooks/useDialog"
89
import { insertServiceCategory } from "@/lib/prisma/service_category/insertServiceCategory"
910
import type { ServiceCategoryWithRelations } from "@/lib/prisma/service_category/types"
1011
import { updateServiceCategory } from "@/lib/prisma/service_category/updateServiceCategory"
12+
import type { StaffUserWithRelations } from "@/lib/prisma/staff_user/types"
13+
import { resolvePolicy } from "@/utils/policies/resolvePolicy"
14+
import type { UserContext } from "@/utils/policies/types"
1115
import { ConfirmArchiveServiceCategoryModal } from "../ConfirmArchiveServiceCategoryModal"
1216
import { CreateServiceCategoryModal } from "../CreateServiceCategoryModal"
1317
import { EditServiceCategoryModal } from "../EditServiceCategoryModal"
1418
import { columns } from "./columns"
1519

1620
export type ServiceCategoryTableProps = {
21+
currentUser: StaffUserWithRelations | null
1722
serviceCategories: ServiceCategoryWithRelations[]
1823
services: Service[]
1924
revalidateTable: () => Promise<void>
2025
}
2126

2227
export const ServiceCategoryTable = ({
28+
currentUser,
2329
serviceCategories,
2430
services,
2531
revalidateTable,
2632
}: ServiceCategoryTableProps) => {
33+
const { role, idir_user_guid } = useAuth()
2734
const {
2835
open: editServiceCategoryModalOpen,
2936
openDialog: openEditServiceCategoryModal,
@@ -40,9 +47,36 @@ export const ServiceCategoryTable = ({
4047
closeDialog: closeConfirmArchiveServiceCategoryModal,
4148
} = useDialog()
4249

50+
const userContext = useMemo<UserContext>(
51+
() => ({
52+
staff_user_id: idir_user_guid ?? null,
53+
role,
54+
location_code: currentUser?.locationCode ?? null,
55+
}),
56+
[idir_user_guid, role, currentUser?.locationCode]
57+
)
58+
59+
const actions = resolvePolicy("service_category", userContext)
60+
4361
const [showArchived, setShowArchived] = useState<boolean>(false)
4462
const [selectedServiceCategory, setSelectedServiceCategory] =
4563
useState<ServiceCategoryWithRelations | null>(null)
64+
const [canEditSelectedServiceCategory, setCanEditSelectedServiceCategory] =
65+
useState<boolean>(false)
66+
const [canArchiveSelectedServiceCategory, setCanArchiveSelectedServiceCategory] =
67+
useState<boolean>(false)
68+
69+
// Determine if the current user can edit/archive the selected service category whenever either changes
70+
useEffect(() => {
71+
if (selectedServiceCategory) {
72+
const actions = resolvePolicy("service_category", userContext, selectedServiceCategory)
73+
setCanEditSelectedServiceCategory(actions.includes("edit"))
74+
setCanArchiveSelectedServiceCategory(actions.includes("archive"))
75+
} else {
76+
setCanEditSelectedServiceCategory(false)
77+
setCanArchiveSelectedServiceCategory(false)
78+
}
79+
}, [selectedServiceCategory, userContext])
4680

4781
const handleRowClick = (serviceCategory: ServiceCategoryWithRelations) => {
4882
setSelectedServiceCategory(serviceCategory)
@@ -52,12 +86,20 @@ export const ServiceCategoryTable = ({
5286
const serviceCategoriesToShow = showArchived
5387
? serviceCategories
5488
: serviceCategories.filter((serviceCategory) => serviceCategory.deletedAt === null)
89+
90+
const canCreate = actions.includes("create")
91+
5592
return (
5693
<>
5794
<div className="flex items-center justify-end mb-3 gap-4">
5895
<h3 className="self-center text-sm font-medium text-gray-700">Show Archived</h3>
5996
<Switch checked={showArchived} onChange={setShowArchived} />
60-
<button type="button" onClick={openCreateServiceCategoryModal} className="primary">
97+
<button
98+
type="button"
99+
onClick={openCreateServiceCategoryModal}
100+
disabled={!canCreate}
101+
className="primary"
102+
>
61103
+ Create
62104
</button>
63105
</div>
@@ -84,6 +126,8 @@ export const ServiceCategoryTable = ({
84126
updateServiceCategory={updateServiceCategory}
85127
revalidateTable={revalidateTable}
86128
openConfirmArchiveServiceCategoryModal={openConfirmArchiveServiceCategoryModal}
129+
canEdit={canEditSelectedServiceCategory}
130+
canArchive={canArchiveSelectedServiceCategory}
87131
/>
88132
<CreateServiceCategoryModal
89133
open={createServiceCategoryModalOpen}

src/utils/policies/policies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LocationPolicy } from "./resources/location"
2+
import { ServiceCategoryPolicy } from "./resources/service_category"
23
import { StaffUserPolicy } from "./resources/staff_user"
34
import type { Policies } from "./types"
45

@@ -20,4 +21,5 @@ import type { Policies } from "./types"
2021
export const policies: Policies = {
2122
staff_user: StaffUserPolicy,
2223
location: LocationPolicy,
24+
service_category: ServiceCategoryPolicy,
2325
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Policy } from "../types"
2+
3+
export const ServiceCategoryPolicy: Policy = (user_context, _data) => {
4+
const { role } = user_context
5+
const actions = new Set<string>()
6+
7+
// View permissions
8+
actions.add("view") // Anyone can view service categories
9+
10+
// Create permissions
11+
if (role === "Administrator") actions.add("create") // Administrators can create service categories
12+
13+
// Edit permissions
14+
if (role === "Administrator") actions.add("edit") // Administrators can edit all records
15+
16+
// Archive permissions
17+
if (role === "Administrator") actions.add("archive") // Administrators can archive all records
18+
19+
return Array.from(actions)
20+
}

0 commit comments

Comments
 (0)