diff --git a/src/app/(protected)/pmschedules/_components/pm-schedule-unavailable.tsx b/src/app/(protected)/pmschedules/_components/pm-schedule-unavailable.tsx
new file mode 100644
index 00000000..4e77c2ff
--- /dev/null
+++ b/src/app/(protected)/pmschedules/_components/pm-schedule-unavailable.tsx
@@ -0,0 +1,33 @@
+import { Alert, Box, Button, Typography } from "@mui/material";
+import { useNavigate } from "react-router-dom";
+
+export function PmScheduleEditUnavailablePanel() {
+ return (
+
+
+ PM Schedule editing is unavailable — the production API does not expose{" "}
+ PMSchedule/GetById.
+
+
+ List and create remain available via GetList and{" "}
+ Save. Enable editing after the backend Dev branch exposes
+ GetById.
+
+
+ );
+}
+
+export default function PmScheduleEditUnavailablePage() {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/pmschedules/edit/[id].tsx b/src/app/(protected)/pmschedules/edit/[id].tsx
index d6a04793..24b9b509 100644
--- a/src/app/(protected)/pmschedules/edit/[id].tsx
+++ b/src/app/(protected)/pmschedules/edit/[id].tsx
@@ -1 +1,5 @@
-export { default } from "../_components/pm-schedule-form";
+import { pmScheduleCapabilities } from "@/config/pm-schedule-capabilities";
+import PmScheduleFormPage from "../_components/pm-schedule-form";
+import PmScheduleEditUnavailablePage from "../_components/pm-schedule-unavailable";
+
+export default pmScheduleCapabilities.canEdit ? PmScheduleFormPage : PmScheduleEditUnavailablePage;
diff --git a/src/app/(protected)/pmschedules/index.tsx b/src/app/(protected)/pmschedules/index.tsx
index f4056917..be8d453b 100644
--- a/src/app/(protected)/pmschedules/index.tsx
+++ b/src/app/(protected)/pmschedules/index.tsx
@@ -27,6 +27,7 @@ import {
Tooltip,
Typography,
} from "@mui/material";
+import { pmScheduleCapabilities } from "@/config/pm-schedule-capabilities";
import type { PmScheduleListItem } from "@/domain/pm-schedules/types/pm-schedule";
import { useDeletePmSchedule } from "@/domain/pm-schedules/use-cases/use-delete-pm-schedule";
import { usePmSchedulesList } from "@/domain/pm-schedules/use-cases/use-pm-schedules-list";
@@ -42,6 +43,9 @@ export default function PmSchedulesListPage() {
const debouncedSearch = useDebounce(search, 300);
const deletePmSchedule = useDeletePmSchedule();
+ const showActionsColumn = pmScheduleCapabilities.canEdit || pmScheduleCapabilities.canDelete;
+ const columnCount = showActionsColumn ? 7 : 6;
+
const listParams = useMemo(
() => ({
search: debouncedSearch,
@@ -57,7 +61,7 @@ export default function PmSchedulesListPage() {
const totalCount = data?.totalCount ?? 0;
const handleDeleteConfirm = () => {
- if (!deleteTarget) return;
+ if (!deleteTarget || !pmScheduleCapabilities.canDelete) return;
deletePmSchedule.mutate(deleteTarget.id, {
onSuccess: () => setDeleteTarget(null),
});
@@ -92,9 +96,11 @@ export default function PmSchedulesListPage() {
placeholder="Search PM schedules..."
className="min-w-[220px]"
/>
-
+ {pmScheduleCapabilities.canCreate && (
+
+ )}
@@ -114,21 +120,23 @@ export default function PmSchedulesListPage() {
Status
Asset
Location
-
- Actions
-
+ {showActionsColumn && (
+
+ Actions
+
+ )}
{isLoading ? (
-
+
) : rows.length === 0 ? (
-
+
No PM schedules found
@@ -136,14 +144,20 @@ export default function PmSchedulesListPage() {
rows.map((row) => (
-
+ {pmScheduleCapabilities.canEdit ? (
+
+ ) : (
+
+ {row.name || "—"}
+
+ )}
{row.frequency || "—"}
{row.startDate || "—"}
@@ -157,26 +171,32 @@ export default function PmSchedulesListPage() {
{row.asset || "—"}
{row.location || "—"}
-
-
- navigate(`/pmschedules/edit/${row.id}`)}
- >
-
-
-
-
- setDeleteTarget(row)}
- >
-
-
-
-
+ {showActionsColumn && (
+
+ {pmScheduleCapabilities.canEdit && (
+
+ navigate(`/pmschedules/edit/${row.id}`)}
+ >
+
+
+
+ )}
+ {pmScheduleCapabilities.canDelete && (
+
+ setDeleteTarget(row)}
+ >
+
+
+
+ )}
+
+ )}
))
)}
@@ -197,28 +217,30 @@ export default function PmSchedulesListPage() {
rowsPerPageOptions={[12, 24, 48]}
/>
-
+ {pmScheduleCapabilities.canDelete && (
+
+ )}
);
}
diff --git a/src/config/pm-schedule-capabilities.ts b/src/config/pm-schedule-capabilities.ts
new file mode 100644
index 00000000..653575e3
--- /dev/null
+++ b/src/config/pm-schedule-capabilities.ts
@@ -0,0 +1,20 @@
+/**
+ * PM Schedule API capabilities aligned with production backend (`origin/Dev`).
+ *
+ * Exposed controller endpoints on Dev:
+ * - GET PMSchedule/GetList
+ * - POST PMSchedule/Save (Id=0 create, Id>0 update)
+ *
+ * Not exposed on Dev (present on `main` after 59385cf):
+ * - GET PMSchedule/GetById
+ * - POST PMSchedule/Delete
+ *
+ * Flip flags to true only after Dev exposes the matching endpoints.
+ */
+export const pmScheduleCapabilities = {
+ canCreate: true,
+ canEdit: false,
+ canDelete: false,
+} as const;
+
+export type PmScheduleCapabilities = typeof pmScheduleCapabilities;
diff --git a/src/domain/pm-schedules/use-cases/use-delete-pm-schedule.ts b/src/domain/pm-schedules/use-cases/use-delete-pm-schedule.ts
index 66594fce..92a55fdc 100644
--- a/src/domain/pm-schedules/use-cases/use-delete-pm-schedule.ts
+++ b/src/domain/pm-schedules/use-cases/use-delete-pm-schedule.ts
@@ -1,5 +1,6 @@
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
import { toast } from "react-toastify";
+import { pmScheduleCapabilities } from "@/config/pm-schedule-capabilities";
import { pmSchedulesApi } from "@/domain/pm-schedules/api/pm-schedules-api";
import { queryKeys } from "@/infra/query-key/query-key";
@@ -7,7 +8,13 @@ export function useDeletePmSchedule(): UseMutationResult {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (id: number) => pmSchedulesApi.delete(id),
+ mutationFn: (id: number) => {
+ if (!pmScheduleCapabilities.canDelete) {
+ return Promise.reject(new Error("PM Schedule delete is not supported by the current API"));
+ }
+
+ return pmSchedulesApi.delete(id);
+ },
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.pmSchedules.all });
toast.success("PM schedule deleted successfully!");
diff --git a/src/domain/pm-schedules/use-cases/use-pm-schedule-detail.ts b/src/domain/pm-schedules/use-cases/use-pm-schedule-detail.ts
index 1f559234..c0bfc643 100644
--- a/src/domain/pm-schedules/use-cases/use-pm-schedule-detail.ts
+++ b/src/domain/pm-schedules/use-cases/use-pm-schedule-detail.ts
@@ -1,4 +1,5 @@
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
+import { pmScheduleCapabilities } from "@/config/pm-schedule-capabilities";
import { pmSchedulesApi } from "@/domain/pm-schedules/api/pm-schedules-api";
import type { PmSchedule } from "@/domain/pm-schedules/types/pm-schedule";
import { queryKeys } from "@/infra/query-key/query-key";
@@ -8,6 +9,6 @@ export function usePmScheduleDetail(id?: string): UseQueryResult pmSchedulesApi.getById(requireQueryParam(id, "PM schedule id")),
- enabled: Boolean(id),
+ enabled: Boolean(id) && pmScheduleCapabilities.canEdit,
});
}
diff --git a/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx b/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx
new file mode 100644
index 00000000..f26b9ea5
--- /dev/null
+++ b/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx
@@ -0,0 +1,92 @@
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import PmSchedulesListPage from "@/app/(protected)/pmschedules/index";
+import { renderWithProviders } from "@/test/test-utils";
+
+const usePmSchedulesList = vi.fn();
+const useDeletePmSchedule = vi.fn();
+
+vi.mock("@/domain/pm-schedules/use-cases/use-pm-schedules-list", () => ({
+ usePmSchedulesList: (...args: unknown[]) => usePmSchedulesList(...args),
+}));
+
+vi.mock("@/domain/pm-schedules/use-cases/use-delete-pm-schedule", () => ({
+ useDeletePmSchedule: (...args: unknown[]) => useDeletePmSchedule(...args),
+}));
+
+vi.mock("@/hooks/use-debounce", () => ({
+ useDebounce: (value: string) => value,
+}));
+
+const mockRow = {
+ id: 1,
+ name: "Quarterly HVAC",
+ frequency: "Quarterly",
+ startDate: "2026-01-01",
+ isActive: true,
+ asset: "Unit A",
+ location: "Building 1",
+};
+
+describe("PmSchedulesListPage actions", () => {
+ it("does not render edit or delete actions when capabilities are disabled", () => {
+ useDeletePmSchedule.mockReturnValue({
+ mutate: vi.fn(),
+ isPending: false,
+ });
+ usePmSchedulesList.mockReturnValue({
+ data: {
+ items: [mockRow],
+ totalCount: 1,
+ page: 1,
+ pageSize: 12,
+ },
+ isLoading: false,
+ isFetching: false,
+ error: null,
+ });
+
+ renderWithProviders(, { route: "/pmschedules", withAuth: false });
+
+ expect(screen.queryByLabelText("Edit PM schedule")).not.toBeInTheDocument();
+ expect(screen.queryByLabelText("Delete PM schedule")).not.toBeInTheDocument();
+ expect(screen.queryByRole("columnheader", { name: "Actions" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Quarterly HVAC" })).not.toBeInTheDocument();
+ expect(screen.getByText("Quarterly HVAC")).toBeInTheDocument();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+
+ it("still renders create when canCreate is enabled", () => {
+ useDeletePmSchedule.mockReturnValue({
+ mutate: vi.fn(),
+ isPending: false,
+ });
+ usePmSchedulesList.mockReturnValue({
+ data: { items: [], totalCount: 0, page: 1, pageSize: 12 },
+ isLoading: false,
+ isFetching: false,
+ error: null,
+ });
+
+ renderWithProviders(, { route: "/pmschedules", withAuth: false });
+
+ expect(screen.getByRole("button", { name: "+ New PM Schedule" })).toBeInTheDocument();
+ });
+});
+
+describe("PmScheduleEditUnavailablePage", () => {
+ it("shows unavailable message instead of edit form when canEdit is false", async () => {
+ const EditPage = (await import("@/app/(protected)/pmschedules/edit/[id]")).default;
+
+ renderWithProviders(, {
+ route: "/pmschedules/edit/1",
+ routerProps: { initialEntries: ["/pmschedules/edit/1"] },
+ withAuth: false,
+ });
+
+ expect(screen.getByText(/PM Schedule editing is unavailable/i)).toBeInTheDocument();
+ expect(screen.getByText(/PMSchedule\/GetById/i)).toBeInTheDocument();
+ expect(screen.queryByRole("heading", { name: "Edit PM Schedule" })).not.toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Back to PM Schedules" })).toBeInTheDocument();
+ });
+});
diff --git a/src/test/config/pm-schedule-capabilities.test.ts b/src/test/config/pm-schedule-capabilities.test.ts
new file mode 100644
index 00000000..8ae57e27
--- /dev/null
+++ b/src/test/config/pm-schedule-capabilities.test.ts
@@ -0,0 +1,10 @@
+import { pmScheduleCapabilities } from "@/config/pm-schedule-capabilities";
+import { describe, expect, it } from "vitest";
+
+describe("pmScheduleCapabilities", () => {
+ it("reflects production Dev API contract (GetList + Save only)", () => {
+ expect(pmScheduleCapabilities.canCreate).toBe(true);
+ expect(pmScheduleCapabilities.canEdit).toBe(false);
+ expect(pmScheduleCapabilities.canDelete).toBe(false);
+ });
+});