Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/app/protected/settings/service-categories/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { revalidatePath } from "next/cache"
import { headers } from "next/headers"
import { ServiceCategoryTable } from "@/components/settings/service_categories/ServiceCategoryTable"
import { getAllServices } from "@/lib/prisma/service/getAllServices"
import { getAllServiceCategories } from "@/lib/prisma/service_category/getAllServiceCategories"
import { getStaffUserBySub } from "@/lib/prisma/staff_user/getStaffUserBySub"
import { getAuthContext } from "@/utils/auth/getAuthContext"

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

export default async function Page() {
const headersList = await headers()
const authContext = getAuthContext(headersList)
const user = authContext?.user

const currentUser = await getStaffUserBySub(user?.sub ?? "")
const serviceCategories = await getAllServiceCategories()
const services = await getAllServices()

Expand All @@ -19,6 +27,7 @@ export default async function Page() {
<div className="space-y-sm">
<h2>Service Categories</h2>
<ServiceCategoryTable
currentUser={currentUser}
serviceCategories={serviceCategories}
services={services}
revalidateTable={revalidateTable}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type EditServiceCategoryModalProps = {
onClose: () => void
serviceCategory: ServiceCategoryWithRelations | null
services: Service[]
canEdit: boolean
canArchive: boolean
updateServiceCategory: (
serviceCategory: Partial<ServiceCategoryWithRelations>
) => Promise<ServiceCategoryWithRelations | null>
Expand All @@ -31,6 +33,8 @@ export const EditServiceCategoryModal = ({
onClose,
serviceCategory,
services,
canEdit,
canArchive,
updateServiceCategory,
revalidateTable,
openConfirmArchiveServiceCategoryModal,
Expand Down Expand Up @@ -86,7 +90,7 @@ export const EditServiceCategoryModal = ({
if (!serviceCategory || !formData) return null

const isArchived = serviceCategory.deletedAt !== null
const isReadonly = isArchived
const isReadonly = isArchived || !canEdit

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

Expand Down Expand Up @@ -123,13 +127,19 @@ export const EditServiceCategoryModal = ({

<DialogBody>
<form className="space-y-5">
{isReadonly && (
{!canEdit && (
<div className="flex flex-col gap-1 rounded-md border-l-4 border-l-red-600 bg-red-50 p-4">
{isArchived && (
<p className="text-sm font-medium text-red-800">
This service category is archived and cannot be edited.
</p>
)}
<p className="text-sm font-medium text-red-800">
You do not have permission to edit this service category.
</p>
</div>
)}

{isArchived && (
<div className="flex flex-col gap-1 rounded-md border-l-4 border-l-red-600 bg-red-50 p-4">
<p className="text-sm font-medium text-red-800">
This service category is archived and cannot be edited.
</p>
</div>
)}

Expand All @@ -152,9 +162,11 @@ export const EditServiceCategoryModal = ({
<button type="button" className="tertiary" onClick={onClose}>
Cancel
</button>
<button type="button" className="secondary danger" onClick={handleOpenArchive}>
{isArchived ? "Unarchive" : "Archive"}
</button>
{canArchive && (
<button type="button" className="secondary danger" onClick={handleOpenArchive}>
{isArchived ? "Unarchive" : "Archive"}
</button>
)}
<button
type="button"
className="primary"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
"use client"

import { useState } from "react"
import { useEffect, useMemo, useState } from "react"
import { DataTable } from "@/components/common/datatable"
import { Switch } from "@/components/common/switch"
import type { Service } from "@/generated/prisma/client"
import { useAuth } from "@/hooks/useAuth"
import { useDialog } from "@/hooks/useDialog"
import { insertServiceCategory } from "@/lib/prisma/service_category/insertServiceCategory"
import type { ServiceCategoryWithRelations } from "@/lib/prisma/service_category/types"
import { updateServiceCategory } from "@/lib/prisma/service_category/updateServiceCategory"
import type { StaffUserWithRelations } from "@/lib/prisma/staff_user/types"
import { resolvePolicy } from "@/utils/policies/resolvePolicy"
import type { UserContext } from "@/utils/policies/types"
import { ConfirmArchiveServiceCategoryModal } from "../ConfirmArchiveServiceCategoryModal"
import { CreateServiceCategoryModal } from "../CreateServiceCategoryModal"
import { EditServiceCategoryModal } from "../EditServiceCategoryModal"
import { columns } from "./columns"

export type ServiceCategoryTableProps = {
currentUser: StaffUserWithRelations | null
serviceCategories: ServiceCategoryWithRelations[]
services: Service[]
revalidateTable: () => Promise<void>
}

export const ServiceCategoryTable = ({
currentUser,
serviceCategories,
services,
revalidateTable,
}: ServiceCategoryTableProps) => {
const { role, idir_user_guid } = useAuth()
const {
open: editServiceCategoryModalOpen,
openDialog: openEditServiceCategoryModal,
Expand All @@ -40,9 +47,36 @@ export const ServiceCategoryTable = ({
closeDialog: closeConfirmArchiveServiceCategoryModal,
} = useDialog()

const userContext = useMemo<UserContext>(
() => ({
staff_user_id: idir_user_guid ?? null,
role,
location_code: currentUser?.locationCode ?? null,
}),
[idir_user_guid, role, currentUser?.locationCode]
)

const actions = resolvePolicy("service_category", userContext)

const [showArchived, setShowArchived] = useState<boolean>(false)
const [selectedServiceCategory, setSelectedServiceCategory] =
useState<ServiceCategoryWithRelations | null>(null)
const [canEditSelectedServiceCategory, setCanEditSelectedServiceCategory] =
useState<boolean>(false)
const [canArchiveSelectedServiceCategory, setCanArchiveSelectedServiceCategory] =
useState<boolean>(false)

// Determine if the current user can edit/archive the selected service category whenever either changes
useEffect(() => {
if (selectedServiceCategory) {
const actions = resolvePolicy("service_category", userContext, selectedServiceCategory)
setCanEditSelectedServiceCategory(actions.includes("edit"))
setCanArchiveSelectedServiceCategory(actions.includes("archive"))
} else {
setCanEditSelectedServiceCategory(false)
setCanArchiveSelectedServiceCategory(false)
}
}, [selectedServiceCategory, userContext])

const handleRowClick = (serviceCategory: ServiceCategoryWithRelations) => {
setSelectedServiceCategory(serviceCategory)
Expand All @@ -52,12 +86,20 @@ export const ServiceCategoryTable = ({
const serviceCategoriesToShow = showArchived
? serviceCategories
: serviceCategories.filter((serviceCategory) => serviceCategory.deletedAt === null)

const canCreate = actions.includes("create")

return (
<>
<div className="flex items-center justify-end mb-3 gap-4">
<h3 className="self-center text-sm font-medium text-gray-700">Show Archived</h3>
<Switch checked={showArchived} onChange={setShowArchived} />
<button type="button" onClick={openCreateServiceCategoryModal} className="primary">
<button
type="button"
onClick={openCreateServiceCategoryModal}
disabled={!canCreate}
className="primary"
>
+ Create
</button>
</div>
Expand All @@ -84,6 +126,8 @@ export const ServiceCategoryTable = ({
updateServiceCategory={updateServiceCategory}
revalidateTable={revalidateTable}
openConfirmArchiveServiceCategoryModal={openConfirmArchiveServiceCategoryModal}
canEdit={canEditSelectedServiceCategory}
canArchive={canArchiveSelectedServiceCategory}
/>
<CreateServiceCategoryModal
open={createServiceCategoryModalOpen}
Expand Down
2 changes: 2 additions & 0 deletions src/utils/policies/policies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LocationPolicy } from "./resources/location"
import { ServiceCategoryPolicy } from "./resources/service_category"
import { StaffUserPolicy } from "./resources/staff_user"
import type { Policies } from "./types"

Expand All @@ -20,4 +21,5 @@ import type { Policies } from "./types"
export const policies: Policies = {
staff_user: StaffUserPolicy,
location: LocationPolicy,
service_category: ServiceCategoryPolicy,
}
20 changes: 20 additions & 0 deletions src/utils/policies/resources/service_category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Policy } from "../types"

export const ServiceCategoryPolicy: Policy = (user_context, _data) => {
const { role } = user_context
const actions = new Set<string>()

// View permissions
actions.add("view") // Anyone can view service categories

// Create permissions
if (role === "Administrator") actions.add("create") // Administrators can create service categories

// Edit permissions
if (role === "Administrator") actions.add("edit") // Administrators can edit all records

// Archive permissions
if (role === "Administrator") actions.add("archive") // Administrators can archive all records

return Array.from(actions)
}
Loading