mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 01:52:04 +00:00
feat: add PM schedules CRUD module
This commit is contained in:
parent
66c8c7cb4a
commit
77e128af5a
12 changed files with 704 additions and 0 deletions
185
src/app/(protected)/pmschedules/_components/pm-schedule-form.tsx
Normal file
185
src/app/(protected)/pmschedules/_components/pm-schedule-form.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
pmScheduleSchema,
|
||||
type PmScheduleFormValues,
|
||||
} from "@/domain/pm-schedules/schemas/pm-schedule-schema";
|
||||
import { usePmScheduleDetail } from "@/domain/pm-schedules/use-cases/use-pm-schedule-detail";
|
||||
import { useSavePmSchedule } from "@/domain/pm-schedules/use-cases/use-save-pm-schedule";
|
||||
import { toDateInputValue } from "@/lib/time-utils";
|
||||
|
||||
const defaultValues: PmScheduleFormValues = {
|
||||
asset: "",
|
||||
assetName: "",
|
||||
frequency: "Quarterly",
|
||||
lastServiceDate: "",
|
||||
nextServiceDate: "",
|
||||
status: "Active",
|
||||
assignedTo: "",
|
||||
notes: "",
|
||||
};
|
||||
|
||||
export default function PmScheduleFormPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const isEdit = Boolean(id);
|
||||
|
||||
const { data: schedule, isLoading, error } = usePmScheduleDetail(id);
|
||||
const savePmSchedule = useSavePmSchedule(id);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<PmScheduleFormValues>({
|
||||
resolver: zodResolver(pmScheduleSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule && isEdit) {
|
||||
reset({
|
||||
asset: schedule.asset,
|
||||
assetName: schedule.assetName,
|
||||
frequency: schedule.frequency as PmScheduleFormValues["frequency"],
|
||||
lastServiceDate: toDateInputValue(schedule.lastServiceDate),
|
||||
nextServiceDate: toDateInputValue(schedule.nextServiceDate),
|
||||
status: schedule.status as PmScheduleFormValues["status"],
|
||||
assignedTo: schedule.assignedTo,
|
||||
notes: schedule.notes,
|
||||
});
|
||||
}
|
||||
}, [schedule, isEdit, reset]);
|
||||
|
||||
if (isEdit && isLoading) {
|
||||
return (
|
||||
<Box className="flex justify-center p-10">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="mx-auto max-w-3xl p-4">
|
||||
<Paper variant="outlined" className="p-6">
|
||||
<Stack spacing={3}>
|
||||
<Typography variant="h5">{isEdit ? "Edit PM Schedule" : "Create PM Schedule"}</Typography>
|
||||
|
||||
{error && <Alert severity="error">Failed to load PM schedule</Alert>}
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit((values) => savePmSchedule.mutate(values))}>
|
||||
<Stack spacing={3}>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||||
<TextField
|
||||
label="Asset ID"
|
||||
required
|
||||
fullWidth
|
||||
error={Boolean(errors.asset)}
|
||||
helperText={errors.asset?.message}
|
||||
{...register("asset")}
|
||||
/>
|
||||
<TextField
|
||||
label="Asset Name"
|
||||
required
|
||||
fullWidth
|
||||
error={Boolean(errors.assetName)}
|
||||
helperText={errors.assetName?.message}
|
||||
{...register("assetName")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Controller
|
||||
name="frequency"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Frequency</InputLabel>
|
||||
<Select label="Frequency" value={field.value} onChange={field.onChange}>
|
||||
{["Daily", "Weekly", "Monthly", "Quarterly", "Semi-Annual", "Annual"].map(
|
||||
(value) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{value}
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||||
<TextField
|
||||
label="Last Service Date"
|
||||
type="date"
|
||||
required
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={Boolean(errors.lastServiceDate)}
|
||||
helperText={errors.lastServiceDate?.message}
|
||||
{...register("lastServiceDate")}
|
||||
/>
|
||||
<TextField
|
||||
label="Next Service Date"
|
||||
type="date"
|
||||
required
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={Boolean(errors.nextServiceDate)}
|
||||
helperText={errors.nextServiceDate?.message}
|
||||
{...register("nextServiceDate")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||||
<TextField label="Assigned To" fullWidth {...register("assignedTo")} />
|
||||
<Controller
|
||||
name="status"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select label="Status" value={field.value} onChange={field.onChange}>
|
||||
<MenuItem value="Active">Active</MenuItem>
|
||||
<MenuItem value="Inactive">Inactive</MenuItem>
|
||||
<MenuItem value="Suspended">Suspended</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<TextField label="Notes" fullWidth multiline rows={4} {...register("notes")} />
|
||||
|
||||
<Stack direction="row" spacing={2} justifyContent="flex-end">
|
||||
<Button variant="outlined" onClick={() => navigate("/pmschedules")}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="contained" disabled={savePmSchedule.isPending}>
|
||||
{savePmSchedule.isPending ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/(protected)/pmschedules/edit/[id].tsx
Normal file
1
src/app/(protected)/pmschedules/edit/[id].tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "../_components/pm-schedule-form";
|
||||
216
src/app/(protected)/pmschedules/index.tsx
Normal file
216
src/app/(protected)/pmschedules/index.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
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";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
|
||||
export default function PmSchedulesListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
const [deleteTarget, setDeleteTarget] = useState<PmScheduleListItem | null>(null);
|
||||
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
const deletePmSchedule = useDeletePmSchedule();
|
||||
|
||||
const listParams = useMemo(
|
||||
() => ({
|
||||
search: debouncedSearch,
|
||||
page: page + 1,
|
||||
pageSize,
|
||||
}),
|
||||
[debouncedSearch, page, pageSize],
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, error } = usePmSchedulesList(listParams);
|
||||
|
||||
const rows = data?.items ?? [];
|
||||
const totalCount = data?.totalCount ?? 0;
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (!deleteTarget) return;
|
||||
deletePmSchedule.mutate(deleteTarget.id, {
|
||||
onSuccess: () => setDeleteTarget(null),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
spacing={2}
|
||||
alignItems={{ xs: "stretch", md: "center" }}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack direction="row" spacing={2} alignItems="baseline">
|
||||
<Typography variant="h5" component="h1">
|
||||
PM Schedules
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{totalCount} total
|
||||
</Typography>
|
||||
{isFetching && !isLoading && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems="center">
|
||||
<TextField
|
||||
size="small"
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
placeholder="Search PM schedules..."
|
||||
className="min-w-[220px]"
|
||||
/>
|
||||
<Button variant="contained" onClick={() => navigate("/pmschedules/new")}>
|
||||
+ New PM Schedule
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error">
|
||||
{error instanceof Error ? error.message : "Failed to load PM schedules"}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className="font-semibold">Asset</TableCell>
|
||||
<TableCell className="font-semibold">Frequency</TableCell>
|
||||
<TableCell className="font-semibold">Last Date</TableCell>
|
||||
<TableCell className="font-semibold">Next Date</TableCell>
|
||||
<TableCell className="font-semibold">Status</TableCell>
|
||||
<TableCell className="font-semibold">Assigned To</TableCell>
|
||||
<TableCell align="right" className="font-semibold">
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} align="center" className="py-10">
|
||||
<CircularProgress size={28} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} align="center" className="py-10">
|
||||
<Typography color="text.secondary">No PM schedules found</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
<TableRow key={String(row.id)} hover>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
className="font-semibold"
|
||||
onClick={() => navigate(`/pmschedules/edit/${row.id}`)}
|
||||
>
|
||||
{row.asset || row.name || "—"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>{row.frequency || "—"}</TableCell>
|
||||
<TableCell>{row.lastDate || "—"}</TableCell>
|
||||
<TableCell>{row.nextDate || "—"}</TableCell>
|
||||
<TableCell>{row.status || "—"}</TableCell>
|
||||
<TableCell>{row.assignedTo || "—"}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Edit PM schedule"
|
||||
onClick={() => navigate(`/pmschedules/edit/${row.id}`)}
|
||||
>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Delete PM schedule"
|
||||
onClick={() => setDeleteTarget(row)}
|
||||
>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
onPageChange={(_event, nextPage) => setPage(nextPage)}
|
||||
rowsPerPage={pageSize}
|
||||
onRowsPerPageChange={(event) => {
|
||||
setPageSize(Number(event.target.value));
|
||||
setPage(0);
|
||||
}}
|
||||
rowsPerPageOptions={[12, 24, 48]}
|
||||
/>
|
||||
|
||||
<Dialog open={Boolean(deleteTarget)} onClose={() => setDeleteTarget(null)}>
|
||||
<DialogTitle>Delete PM Schedule</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete "{deleteTarget?.asset || deleteTarget?.name}"?
|
||||
This action cannot be undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteTarget(null)} disabled={deletePmSchedule.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={deletePmSchedule.isPending}
|
||||
>
|
||||
{deletePmSchedule.isPending ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/(protected)/pmschedules/new.tsx
Normal file
1
src/app/(protected)/pmschedules/new.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./_components/pm-schedule-form";
|
||||
57
src/domain/pm-schedules/api/pm-schedules-api.ts
Normal file
57
src/domain/pm-schedules/api/pm-schedules-api.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { API_PATHS } from "@/api/api-paths";
|
||||
import { apiGet, apiPost } from "@/api/api";
|
||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||
import {
|
||||
mapPmSchedule,
|
||||
mapPmScheduleListItem,
|
||||
mapPmScheduleToSavePayload,
|
||||
} from "@/domain/pm-schedules/mappers/pm-schedule-mapper";
|
||||
import type {
|
||||
PmSchedule,
|
||||
PmSchedulesListParams,
|
||||
PmSchedulesListResult,
|
||||
} from "@/domain/pm-schedules/types/pm-schedule";
|
||||
|
||||
export const pmSchedulesApi = {
|
||||
getList: async (params: PmSchedulesListParams = {}): Promise<PmSchedulesListResult> => {
|
||||
const data = await apiGet<unknown>(API_PATHS.pmSchedule.getList, {
|
||||
searchParams: {
|
||||
search: params.search ?? "",
|
||||
page: params.page ?? 1,
|
||||
pageSize: params.pageSize ?? 12,
|
||||
},
|
||||
});
|
||||
|
||||
const paginated = handlePaginatedResponse<unknown>(data, {
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
items: paginated.items.map(mapPmScheduleListItem),
|
||||
totalCount: paginated.totalCount,
|
||||
page: paginated.page,
|
||||
pageSize: paginated.pageSize,
|
||||
};
|
||||
},
|
||||
|
||||
getById: async (id: string | number): Promise<PmSchedule> => {
|
||||
const data = await apiGet<unknown>(API_PATHS.pmSchedule.getById, {
|
||||
searchParams: { id },
|
||||
});
|
||||
return mapPmSchedule(handleApiResponse(data));
|
||||
},
|
||||
|
||||
/** Create and update both use PMSchedule/Save — Id=0 for create. */
|
||||
save: async (schedule: Partial<PmSchedule> & { id?: number }): Promise<PmSchedule> => {
|
||||
const payload = mapPmScheduleToSavePayload(schedule);
|
||||
const data = await apiPost<unknown>(API_PATHS.pmSchedule.save, payload);
|
||||
return mapPmSchedule(handleApiResponse(data));
|
||||
},
|
||||
|
||||
delete: async (id: string | number): Promise<void> => {
|
||||
await apiPost<unknown>(API_PATHS.pmSchedule.delete, undefined, {
|
||||
searchParams: { id },
|
||||
});
|
||||
},
|
||||
};
|
||||
107
src/domain/pm-schedules/mappers/pm-schedule-mapper.ts
Normal file
107
src/domain/pm-schedules/mappers/pm-schedule-mapper.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import type { PmSchedule, PmScheduleListItem } from "@/domain/pm-schedules/types/pm-schedule";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readString(record: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function readNumber(record: Record<string, unknown>, ...keys: string[]): number | null {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mapPmScheduleListItem(raw: unknown): PmScheduleListItem {
|
||||
const record = isRecord(raw) ? raw : {};
|
||||
|
||||
return {
|
||||
id: readNumber(record, "id", "Id") ?? 0,
|
||||
asset: readString(record, "asset", "Asset", "assetCode", "AssetCode", "name", "Name"),
|
||||
frequency: readString(record, "frequency", "Frequency"),
|
||||
lastDate: readString(record, "lastDate", "LastDate", "lastServiceDate", "LastServiceDate"),
|
||||
nextDate: readString(
|
||||
record,
|
||||
"nextDate",
|
||||
"NextDate",
|
||||
"nextServiceDate",
|
||||
"NextServiceDate",
|
||||
"startDate",
|
||||
"StartDate",
|
||||
),
|
||||
status: readString(record, "status", "Status", "isActive", "IsActive") || "Active",
|
||||
assignedTo: readString(record, "assignedTo", "AssignedTo"),
|
||||
name: readString(record, "name", "Name", "assetName", "AssetName"),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapPmSchedule(raw: unknown): PmSchedule {
|
||||
const record = isRecord(raw) ? raw : {};
|
||||
const listItem = mapPmScheduleListItem(raw);
|
||||
const isActive = record.isActive ?? record.IsActive;
|
||||
|
||||
return {
|
||||
id: listItem.id,
|
||||
asset: readString(record, "asset", "Asset", "assetCode", "AssetCode") || listItem.asset,
|
||||
assetName:
|
||||
readString(record, "assetName", "AssetName", "name", "Name") ||
|
||||
listItem.name ||
|
||||
listItem.asset,
|
||||
assetId: readNumber(record, "assetId", "AssetId"),
|
||||
locationId: readNumber(record, "locationId", "LocationId"),
|
||||
frequency: listItem.frequency || "Quarterly",
|
||||
lastServiceDate: readString(
|
||||
record,
|
||||
"lastServiceDate",
|
||||
"LastServiceDate",
|
||||
"lastDate",
|
||||
"LastDate",
|
||||
),
|
||||
nextServiceDate:
|
||||
readString(
|
||||
record,
|
||||
"nextServiceDate",
|
||||
"NextServiceDate",
|
||||
"nextDate",
|
||||
"NextDate",
|
||||
"startDate",
|
||||
"StartDate",
|
||||
) || listItem.nextDate,
|
||||
status:
|
||||
typeof isActive === "boolean"
|
||||
? isActive
|
||||
? "Active"
|
||||
: "Inactive"
|
||||
: listItem.status || "Active",
|
||||
assignedTo: listItem.assignedTo,
|
||||
notes: readString(record, "notes", "Notes"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Unified save payload — always uses PMSchedule/Save (fixes Create vs Save inconsistency). */
|
||||
export function mapPmScheduleToSavePayload(schedule: Partial<PmSchedule> & { id?: number }) {
|
||||
const isActive =
|
||||
schedule.status === "Active" || schedule.status === undefined || schedule.status === "";
|
||||
|
||||
return {
|
||||
Id: schedule.id ?? 0,
|
||||
Name: schedule.assetName || schedule.asset || "",
|
||||
Frequency: schedule.frequency || "Quarterly",
|
||||
StartDate: schedule.nextServiceDate || "",
|
||||
IsActive: isActive,
|
||||
AssetId: schedule.assetId ?? null,
|
||||
LocationId: schedule.locationId ?? null,
|
||||
};
|
||||
}
|
||||
14
src/domain/pm-schedules/schemas/pm-schedule-schema.ts
Normal file
14
src/domain/pm-schedules/schemas/pm-schedule-schema.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const pmScheduleSchema = z.object({
|
||||
asset: z.string().min(1, "Asset ID is required"),
|
||||
assetName: z.string().min(1, "Asset name is required"),
|
||||
frequency: z.enum(["Daily", "Weekly", "Monthly", "Quarterly", "Semi-Annual", "Annual"]),
|
||||
lastServiceDate: z.string().min(1, "Last service date is required"),
|
||||
nextServiceDate: z.string().min(1, "Next service date is required"),
|
||||
status: z.enum(["Active", "Inactive", "Suspended"]),
|
||||
assignedTo: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
export type PmScheduleFormValues = z.infer<typeof pmScheduleSchema>;
|
||||
37
src/domain/pm-schedules/types/pm-schedule.ts
Normal file
37
src/domain/pm-schedules/types/pm-schedule.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
export interface PmScheduleListItem {
|
||||
id: number;
|
||||
asset: string;
|
||||
frequency: string;
|
||||
lastDate: string;
|
||||
nextDate: string;
|
||||
status: string;
|
||||
assignedTo: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface PmSchedule {
|
||||
id: number;
|
||||
asset: string;
|
||||
assetName: string;
|
||||
assetId: number | null;
|
||||
locationId: number | null;
|
||||
frequency: string;
|
||||
lastServiceDate: string;
|
||||
nextServiceDate: string;
|
||||
status: string;
|
||||
assignedTo: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface PmSchedulesListParams {
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface PmSchedulesListResult {
|
||||
items: PmScheduleListItem[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
19
src/domain/pm-schedules/use-cases/use-delete-pm-schedule.ts
Normal file
19
src/domain/pm-schedules/use-cases/use-delete-pm-schedule.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { toast } from "react-toastify";
|
||||
import { pmSchedulesApi } from "@/domain/pm-schedules/api/pm-schedules-api";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useDeletePmSchedule(): UseMutationResult<void, Error, number> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => pmSchedulesApi.delete(id),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.pmSchedules.all });
|
||||
toast.success("PM schedule deleted successfully!");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to delete PM schedule");
|
||||
},
|
||||
});
|
||||
}
|
||||
13
src/domain/pm-schedules/use-cases/use-pm-schedule-detail.ts
Normal file
13
src/domain/pm-schedules/use-cases/use-pm-schedule-detail.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
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";
|
||||
import { requireQueryParam } from "@/lib/query/require-query-param";
|
||||
|
||||
export function usePmScheduleDetail(id?: string): UseQueryResult<PmSchedule, Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.pmSchedules.detail(id ?? ""),
|
||||
queryFn: () => pmSchedulesApi.getById(requireQueryParam(id, "PM schedule id")),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
17
src/domain/pm-schedules/use-cases/use-pm-schedules-list.ts
Normal file
17
src/domain/pm-schedules/use-cases/use-pm-schedules-list.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
import { pmSchedulesApi } from "@/domain/pm-schedules/api/pm-schedules-api";
|
||||
import type {
|
||||
PmSchedulesListParams,
|
||||
PmSchedulesListResult,
|
||||
} from "@/domain/pm-schedules/types/pm-schedule";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function usePmSchedulesList(
|
||||
params: PmSchedulesListParams,
|
||||
): UseQueryResult<PmSchedulesListResult, Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.pmSchedules.list(params as Record<string, unknown>),
|
||||
queryFn: () => pmSchedulesApi.getList(params),
|
||||
placeholderData: (previous) => previous,
|
||||
});
|
||||
}
|
||||
37
src/domain/pm-schedules/use-cases/use-save-pm-schedule.ts
Normal file
37
src/domain/pm-schedules/use-cases/use-save-pm-schedule.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { pmSchedulesApi } from "@/domain/pm-schedules/api/pm-schedules-api";
|
||||
import type { PmScheduleFormValues } from "@/domain/pm-schedules/schemas/pm-schedule-schema";
|
||||
import type { PmSchedule } from "@/domain/pm-schedules/types/pm-schedule";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useSavePmSchedule(
|
||||
id?: string,
|
||||
): UseMutationResult<PmSchedule, Error, PmScheduleFormValues> {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (values: PmScheduleFormValues) =>
|
||||
pmSchedulesApi.save({
|
||||
id: id ? Number(id) : 0,
|
||||
asset: values.asset,
|
||||
assetName: values.assetName,
|
||||
frequency: values.frequency,
|
||||
lastServiceDate: values.lastServiceDate,
|
||||
nextServiceDate: values.nextServiceDate,
|
||||
status: values.status,
|
||||
assignedTo: values.assignedTo,
|
||||
notes: values.notes,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.pmSchedules.all });
|
||||
toast.success("PM schedule saved successfully!");
|
||||
navigate("/pmschedules");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to save PM schedule");
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue