Skip to content
Open
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
1 change: 0 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"recommendations": [
"biomejs.biome",
"prisma.prisma",
"GitHub.copilot",
"GitHub.copilot-chat",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "app"."Role" ADD VALUE 'Authenticated';
1 change: 1 addition & 0 deletions prisma/models/app/enums/role.prisma
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
enum Role {
Authenticated
CSR
SCSR
SDM
Expand Down
86 changes: 86 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,92 @@ async function main() {
data: { categories: { connect: { id: categoryIdentity.id } } },
})
}

// --- Mock test staff users for each role ---
const testLocation = await prisma.location.findUnique({
where: { legacyOfficeNumber: 999 },
})

if (testLocation) {
// Authenticated role
await prisma.staffUser.upsert({
where: { sub: "test-authenticated@idir" },
update: {},
create: {
guid: "11111111-1111-1111-1111-111111111111",
sub: "test-authenticated@idir",
username: "sarah.chen",
displayName: "Sarah Chen",
role: "Authenticated",
isActive: false,
locationCode: testLocation.code,
counterId: counter.id,
},
})

// CSR role
await prisma.staffUser.upsert({
where: { sub: "test-csr@idir" },
update: {},
create: {
guid: "22222222-2222-2222-2222-222222222222",
sub: "test-csr@idir",
username: "michael.rodriguez",
displayName: "Michael Rodriguez",
role: "CSR",
isActive: false,
locationCode: testLocation.code,
counterId: counter.id,
},
})

// SCSR role
await prisma.staffUser.upsert({
where: { sub: "test-scsr@idir" },
update: {},
create: {
guid: "33333333-3333-3333-3333-333333333333",
sub: "test-scsr@idir",
username: "jennifer.smith",
displayName: "Jennifer Smith",
role: "SCSR",
isActive: false,
locationCode: testLocation.code,
counterId: reception.id,
},
})

// SDM role
await prisma.staffUser.upsert({
where: { sub: "test-sdm@idir" },
update: {},
create: {
guid: "44444444-4444-4444-4444-444444444444",
sub: "test-sdm@idir",
username: "robert.taylor",
displayName: "Robert Taylor",
role: "SDM",
isActive: false,
locationCode: testLocation.code,
isOfficeManager: true,
},
})

// Administrator role
await prisma.staffUser.upsert({
where: { sub: "test-admin@idir" },
update: {},
create: {
guid: "55555555-5555-5555-5555-555555555555",
sub: "test-admin@idir",
username: "patricia.williams",
displayName: "Patricia Williams",
role: "Administrator",
isActive: false,
locationCode: testLocation.code,
},
})
}
} catch (error) {

console.error("❌ Seeding database failed:", error)
Expand Down
9 changes: 8 additions & 1 deletion src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export async function GET(_request: NextRequest) {
})

// Redirect the user to the SSO login page
return NextResponse.redirect(redirectURL)
const response = NextResponse.redirect(redirectURL)

// Prevent caching of redirect responses to avoid stale maintenance page redirects
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
response.headers.set("Pragma", "no-cache")
response.headers.set("Expires", "0")

return response
} catch (error) {
console.error("Error in login route:", error)
return NextResponse.json({ error: "Failed to generate login URL" }, { status: 500 })
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/protected/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getAuthContext } from "@/utils/auth/getAuthContext"

export async function GET(request: NextRequest) {
try {
const authContext = getAuthContext(request)
const authContext = getAuthContext(request.headers)

if (!authContext) {
return NextResponse.json(
Expand Down
19 changes: 14 additions & 5 deletions src/app/protected/settings/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { revalidatePath } from "next/cache"
import { headers } from "next/headers"
import { StaffUserTable } from "@/components/settings/users/StaffUserTable"
import { getAllLocations } from "@/lib/prisma/location/getAllLocations"
import { getAllStaffUsers } from "@/lib/prisma/staff_user/getAllStaffUsers"
import { getStaffUserBySub } from "@/lib/prisma/staff_user/getStaffUserBySub"
import { updateStaffUser } from "@/lib/prisma/staff_user/updateStaffUser"
import { getAuthContext } from "@/utils/auth/getAuthContext"

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

const revalidateTable = async () => {
"use server"
revalidatePath("/protected/settings/users")
}

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

const currentUser = await getStaffUserBySub(user?.sub ?? "")
const users = await getAllStaffUsers()
const locations = await getAllLocations()

const revalidateTable = async () => {
"use server"
revalidatePath("/protected/settings/users")
}

return (
<div className="space-y-sm">
<h2>Users</h2>
<StaffUserTable
currentUser={currentUser}
users={users}
locations={locations}
updateStaffUser={updateStaffUser}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const ConfirmArchiveLocationModal = ({
updateLocation,
revalidateTable,
}: ConfirmArchiveLocationModalProps) => {
const [error, setError] = useState<string | null>(null)
const [formData, setFormData] = useState<LocationWithRelations | null>(null)
const [previousLocation, setPreviousLocation] = useState<LocationWithRelations | null>(null)
const [archiveConfirmation, setArchiveConfirmation] = useState("")
Expand All @@ -45,14 +46,20 @@ export const ConfirmArchiveLocationModal = ({
if (!location || !formData || !previousLocation) return null

const handleSave = async () => {
if (formData) {
await updateLocation(
{ ...formData, deletedAt: isArchived ? null : new Date() },
previousLocation
)
await revalidateTable()
setArchiveConfirmation("")
onClose()
try {
if (formData) {
await updateLocation({ deletedAt: isArchived ? null : new Date() }, previousLocation)
await revalidateTable()
setArchiveConfirmation("")
onClose()
window.location.href = "/protected/settings/locations"
}
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message)
} else {
setError("An unknown error occurred")
}
}
}

Expand All @@ -66,6 +73,12 @@ export const ConfirmArchiveLocationModal = ({

<DialogBody>
<form className="space-y-5">
{error && (
<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">{error}</p>
</div>
)}

<div>
<label
htmlFor="archive-location"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const CreateLocationModal = ({
await revalidateTable()
onClose()
setIsSaving(false)
window.location.href = "/protected/settings/locations"
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const EditLocationModal = ({
await revalidateTable()
onClose()
setIsSaving(false)
window.location.href = "/protected/settings/locations"
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const ConfirmArchiveServiceCategoryModal = ({
updateServiceCategory,
revalidateTable,
}: ConfirmArchiveServiceCategoryModalProps) => {
const [error, setError] = useState<string | null>(null)
const [formData, setFormData] = useState<ServiceCategoryWithRelations | null>(null)
const [previousServiceCategory, setPreviousServiceCategory] =
useState<ServiceCategoryWithRelations | null>(null)
Expand All @@ -54,37 +55,53 @@ export const ConfirmArchiveServiceCategoryModal = ({
if (!serviceCategory || !formData || !previousServiceCategory) return null

const handleSave = async () => {
if (formData) {
if (!isArchived && hasServices) {
if (serviceAction === "remove") {
// Detach services
await updateServiceCategory({ ...formData, deletedAt: new Date(), services: [] })
} else if (serviceAction === "reassign" && newCategoryId) {
// Reassign services to the new category
const newCategory = serviceCategories.find((c) => c.id === newCategoryId)
if (newCategory) {
const mergedServices = [
...(newCategory.services || []),
...(serviceCategory.services || []),
]
// Deduplicate services by code
const uniqueServices = Array.from(
new Map(mergedServices.map((s) => [s.code, s])).values()
)

// Update the new category to include the transferred services
await updateServiceCategory({ id: newCategoryId, services: uniqueServices })
try {
if (formData) {
if (!isArchived && hasServices) {
if (serviceAction === "remove") {
// Detach services
await updateServiceCategory({ id: formData?.id, deletedAt: new Date(), services: [] })
} else if (serviceAction === "reassign" && newCategoryId) {
// Reassign services to the new category
const newCategory = serviceCategories.find((c) => c.id === newCategoryId)
if (newCategory) {
const mergedServices = [
...(newCategory.services || []),
...(serviceCategory.services || []),
]
// Deduplicate services by code
const uniqueServices = Array.from(
new Map(mergedServices.map((s) => [s.code, s])).values()
)

// Update the new category to include the transferred services
await updateServiceCategory({ id: newCategoryId, services: uniqueServices })
}
// Now archive the current category and detach its services
await updateServiceCategory({
id: formData?.id,
deletedAt: new Date(),
services: [],
})
}
// Now archive the current category and detach its services
await updateServiceCategory({ ...formData, deletedAt: new Date(), services: [] })
} else {
await updateServiceCategory({
id: formData?.id,
deletedAt: isArchived ? null : new Date(),
})
}

await revalidateTable()
setArchiveConfirmation("")
onClose()
window.location.href = "/protected/settings/service-categories"
}
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message)
} else {
await updateServiceCategory({ ...formData, deletedAt: isArchived ? null : new Date() })
setError("An unknown error occurred")
}

await revalidateTable()
setArchiveConfirmation("")
onClose()
}
}

Expand All @@ -103,6 +120,12 @@ export const ConfirmArchiveServiceCategoryModal = ({

<DialogBody>
<form className="space-y-5">
{error && (
<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">{error}</p>
</div>
)}

{!isArchived && hasServices && (
<div className="space-y-4">
<p className="text-sm font-medium text-typography-primary">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const CreateServiceCategoryModal = ({
await revalidateTable()
onClose()
setIsSaving(false)
window.location.href = "/protected/settings/service-categories"
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const EditServiceCategoryModal = ({
await revalidateTable()
onClose()
setIsSaving(false)
window.location.href = "/protected/settings/service-categories"
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message)
Expand Down
Loading
Loading