Просмотр исходного кода

Supporting Function: Equipment Repair & Maintenance

master
B.E.N.S.O.N 4 дней назад
Родитель
Сommit
7cc2716b40
11 измененных файлов: 1105 добавлений и 73 удалений
  1. +52
    -0
      src/app/(main)/settings/equipment/EquipmentTabs.tsx
  2. +29
    -0
      src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx
  3. +7
    -14
      src/app/(main)/settings/equipment/page.tsx
  4. +5
    -0
      src/app/api/settings/equipment/index.ts
  5. +229
    -40
      src/components/EquipmentSearch/EquipmentSearch.tsx
  6. +482
    -0
      src/components/EquipmentSearch/EquipmentSearchResults.tsx
  7. +24
    -17
      src/components/EquipmentSearch/EquipmentSearchWrapper.tsx
  8. +242
    -0
      src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx
  9. +18
    -1
      src/i18n/en/common.json
  10. +17
    -1
      src/i18n/zh/common.json
  11. +0
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt

+ 52
- 0
src/app/(main)/settings/equipment/EquipmentTabs.tsx Просмотреть файл

@@ -0,0 +1,52 @@
"use client";

import { useState, useEffect } from "react";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import { useTranslation } from "react-i18next";
import { useRouter, useSearchParams } from "next/navigation";

type EquipmentTabsProps = {
onTabChange?: (tabIndex: number) => void;
};

const EquipmentTabs: React.FC<EquipmentTabsProps> = ({ onTabChange }) => {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation("common");
const tabFromUrl = searchParams.get("tab");
const initialTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0;
const [tabIndex, setTabIndex] = useState(initialTabIndex);

useEffect(() => {
const tabFromUrl = searchParams.get("tab");
const newTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0;
if (newTabIndex !== tabIndex) {
setTabIndex(newTabIndex);
onTabChange?.(newTabIndex);
}
}, [searchParams, tabIndex, onTabChange]);

const handleTabChange = (_e: React.SyntheticEvent, newValue: number) => {
setTabIndex(newValue);
onTabChange?.(newValue);
const params = new URLSearchParams(searchParams.toString());
if (newValue === 0) {
params.delete("tab");
} else {
params.set("tab", newValue.toString());
}
router.push(`/settings/equipment?${params.toString()}`, { scroll: false });
};

return (
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("General Data")} />
<Tab label={t("Repair and Maintenance")} />
</Tabs>
);
};

export default EquipmentTabs;

+ 29
- 0
src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx Просмотреть файл

@@ -0,0 +1,29 @@
import React from "react";
import { SearchParams } from "@/app/utils/fetchUtil";
import { I18nProvider, getServerI18n } from "@/i18n";
import { Typography } from "@mui/material";
import isString from "lodash/isString";
import { notFound } from "next/navigation";
import UpdateMaintenanceForm from "@/components/UpdateMaintenance/UpdateMaintenanceForm";

type Props = {} & SearchParams;

const MaintenanceEditPage: React.FC<Props> = async ({ searchParams }) => {
const type = "common";
const { t } = await getServerI18n(type);
const id = isString(searchParams["id"])
? parseInt(searchParams["id"])
: undefined;
if (!id) {
notFound();
}
return (
<>
<Typography variant="h4">{t("Update Equipment Maintenance and Repair")}</Typography>
<I18nProvider namespaces={[type]}>
<UpdateMaintenanceForm id={id} />
</I18nProvider>
</>
);
};
export default MaintenanceEditPage;

+ 7
- 14
src/app/(main)/settings/equipment/page.tsx Просмотреть файл

@@ -1,15 +1,18 @@
import { TypeEnum } from "@/app/utils/typeEnum";
import EquipmentSearch from "@/components/EquipmentSearch";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
import { fetchAllEquipments } from "@/app/api/settings/equipment";
import { I18nProvider } from "@/i18n";
import EquipmentSearchWrapper from "@/components/EquipmentSearch/EquipmentSearchWrapper";

export const metadata: Metadata = {
title: "Equipment Type",
};
@@ -17,8 +20,6 @@ export const metadata: Metadata = {
const productSetting: React.FC = async () => {
const type = "common";
const { t } = await getServerI18n(type);
const equipments = await fetchAllEquipments();
// preloadClaims();

return (
<>
@@ -31,22 +32,14 @@ const productSetting: React.FC = async () => {
<Typography variant="h4" marginInlineEnd={2}>
{t("Equipment")}
</Typography>
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="product/create"
>
{t("Create product")}
</Button> */}
</Stack>
<Suspense fallback={<EquipmentSearch.Loading />}>
<Suspense fallback={<EquipmentSearchWrapper.Loading />}>
<I18nProvider namespaces={["common", "project"]}>
<EquipmentSearch />
<EquipmentSearchWrapper />
</I18nProvider>
</Suspense>
</>
);
};

export default productSetting;
export default productSetting;

+ 5
- 0
src/app/api/settings/equipment/index.ts Просмотреть файл

@@ -13,7 +13,12 @@ export type EquipmentResult = {
name: string;
description: string | undefined;
equipmentTypeId: string | number | undefined;
equipmentCode?: string;
action?: any;
repairAndMaintenanceStatus?: boolean | number;
latestRepairAndMaintenanceDate?: string | Date;
lastRepairAndMaintenanceDate?: string | Date;
repairAndMaintenanceRemarks?: string;
};

export type Result = {


+ 229
- 40
src/components/EquipmentSearch/EquipmentSearch.tsx Просмотреть файл

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { EquipmentResult } from "@/app/api/settings/equipment";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EquipmentSearchResults, { Column } from "./EquipmentSearchResults";
import { EditNote } from "@mui/icons-material";
import { useRouter, useSearchParams } from "next/navigation";
import { GridDeleteIcon } from "@mui/x-data-grid";
@@ -12,32 +12,90 @@ import { TypeEnum } from "@/app/utils/typeEnum";
import axios from "axios";
import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api";
import axiosInstance from "@/app/(main)/axios/axiosInstance";
import { arrayToDateTimeString } from "@/app/utils/formatUtil";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

type Props = {
equipments: EquipmentResult[];
tabIndex?: number;
};
type SearchQuery = Partial<Omit<EquipmentResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const EquipmentSearch: React.FC<Props> = ({ equipments }) => {
const EquipmentSearch: React.FC<Props> = ({ equipments, tabIndex = 0 }) => {
const [filteredEquipments, setFilteredEquipments] =
useState<EquipmentResult[]>(equipments);
useState<EquipmentResult[]>([]);
const { t } = useTranslation("common");
const router = useRouter();
const [filterObj, setFilterObj] = useState({});
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
// totalCount: 0,
});
const [totalCount, setTotalCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const checkReady = () => {
try {
const token = localStorage.getItem("accessToken");
const hasAuthHeader = axiosInstance.defaults.headers?.common?.Authorization ||
axiosInstance.defaults.headers?.Authorization;
if (token && hasAuthHeader) {
setIsReady(true);
} else if (token) {
setTimeout(checkReady, 50);
} else {
setTimeout(checkReady, 100);
}
} catch (e) {
console.warn("localStorage unavailable", e);
}
};
const timer = setTimeout(checkReady, 100);
return () => clearTimeout(timer);
}, []);
const displayDateTime = useCallback((dateValue: string | Date | number[] | null | undefined): string => {
if (!dateValue) return "-";
if (Array.isArray(dateValue)) {
return arrayToDateTimeString(dateValue);
}
if (typeof dateValue === "string") {
return dateValue;
}
return String(dateValue);
}, []);
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => {
const searchCriteria: Criterion<SearchParamNames>[] = [
if (tabIndex === 1) {
return [
{
label: "設備名稱/設備編號",
paramName: "equipmentCode",
type: "text"
},
{
label: t("Repair and Maintenance Status"),
paramName: "repairAndMaintenanceStatus",
type: "select",
options: ["正常使用中", "正在維護中"]
},
];
}
return [
{ label: t("Code"), paramName: "code", type: "text" },
{ label: t("Description"), paramName: "description", type: "text" },
];
return searchCriteria;
}, [t, equipments]);
}, [t, tabIndex]);

const onDetailClick = useCallback(
(equipment: EquipmentResult) => {
@@ -46,12 +104,19 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => {
[router],
);

const onMaintenanceEditClick = useCallback(
(equipment: EquipmentResult) => {
router.push(`/settings/equipment/MaintenanceEdit?id=${equipment.id}`);
},
[router],
);

const onDeleteClick = useCallback(
(equipment: EquipmentResult) => {},
[router],
);

const columns = useMemo<Column<EquipmentResult>[]>(
const generalDataColumns = useMemo<Column<EquipmentResult>[]>(
() => [
{
name: "id",
@@ -78,9 +143,91 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => {
onClick: onDeleteClick,
},
],
[filteredEquipments],
[onDetailClick, onDeleteClick, t],
);

const repairMaintenanceColumns = useMemo<Column<EquipmentResult>[]>(
() => [
{
name: "id",
label: "編輯",
onClick: onMaintenanceEditClick,
buttonIcon: <EditNote />,
align: "left",
headerAlign: "left",
sx: { width: "60px", minWidth: "60px" },
},
{
name: "code",
label: "設備名稱",
align: "left",
headerAlign: "left",
sx: { width: "200px", minWidth: "200px" },
},
{
name: "equipmentCode",
label: "設備編號",
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
renderCell: (item) => {
return item.equipmentCode || "-";
},
},
{
name: "repairAndMaintenanceStatus",
label: t("Repair and Maintenance Status"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
renderCell: (item) => {
const status = item.repairAndMaintenanceStatus;
if (status === 1 || status === true) {
return (
<Typography sx={{ color: "red", fontWeight: 500 }}>
正在維護中
</Typography>
);
} else if (status === 0 || status === false) {
return (
<Typography sx={{ color: "green", fontWeight: 500 }}>
正常使用中
</Typography>
);
}
return "-";
},
},
{
name: "latestRepairAndMaintenanceDate",
label: t("Latest Repair and Maintenance Date"),
align: "left",
headerAlign: "left",
sx: { width: "200px", minWidth: "200px" },
renderCell: (item) => displayDateTime(item.latestRepairAndMaintenanceDate),
},
{
name: "lastRepairAndMaintenanceDate",
label: t("Last Repair and Maintenance Date"),
align: "left",
headerAlign: "left",
sx: { width: "200px", minWidth: "200px" },
renderCell: (item) => displayDateTime(item.lastRepairAndMaintenanceDate),
},
{
name: "repairAndMaintenanceRemarks",
label: t("Repair and Maintenance Remarks"),
align: "left",
headerAlign: "left",
sx: { width: "200px", minWidth: "200px" },
},
],
[onMaintenanceEditClick, t, displayDateTime],
);

const columns = useMemo(() => {
return tabIndex === 1 ? repairMaintenanceColumns : generalDataColumns;
}, [tabIndex, repairMaintenanceColumns, generalDataColumns]);

interface ApiResponse<T> {
records: T[];
@@ -89,73 +236,115 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => {

const refetchData = useCallback(
async (filterObj: SearchQuery) => {
const authHeader = axiosInstance.defaults.headers["Authorization"];
if (!authHeader) {
const token = localStorage.getItem("accessToken");
const hasAuthHeader = axiosInstance.defaults.headers?.common?.Authorization ||
axiosInstance.defaults.headers?.Authorization;
if (!token || !hasAuthHeader) {
console.warn("Token or auth header not ready, skipping API call");
setIsLoading(false);
return;
}

setIsLoading(true);
const transformedFilter: any = { ...filterObj };
// For maintenance tab (tabIndex === 1), if equipmentCode is provided,
// also search by code (equipment name) with the same value
if (tabIndex === 1 && transformedFilter.equipmentCode) {
transformedFilter.code = transformedFilter.equipmentCode;
}
if (transformedFilter.repairAndMaintenanceStatus) {
if (transformedFilter.repairAndMaintenanceStatus === "正常使用中") {
transformedFilter.repairAndMaintenanceStatus = false;
} else if (transformedFilter.repairAndMaintenanceStatus === "正在維護中") {
transformedFilter.repairAndMaintenanceStatus = true;
} else if (transformedFilter.repairAndMaintenanceStatus === "All") {
delete transformedFilter.repairAndMaintenanceStatus;
}
}
const params = {
pageNum: pagingController.pageNum,
pageSize: pagingController.pageSize,
...filterObj,
...transformedFilter,
};
try {
const endpoint = tabIndex === 1
? `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`
: `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`;
const response = await axiosInstance.get<ApiResponse<EquipmentResult>>(
`${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`,
endpoint,
{ params },
);
console.log(response);
console.log("API Response:", response);
console.log("Records:", response.data.records);
console.log("Total:", response.data.total);
if (response.status == 200) {
setFilteredEquipments(response.data.records);
setTotalCount(response.data.total);
return response;
setFilteredEquipments(response.data.records || []);
setTotalCount(response.data.total || 0);
} else {
throw "400";
}
} catch (error) {
console.error("Error fetching equipment types:", error);
throw error;
setFilteredEquipments([]);
setTotalCount(0);
} finally {
setIsLoading(false);
}
},
[axiosInstance, pagingController.pageNum, pagingController.pageSize],
[pagingController.pageNum, pagingController.pageSize, tabIndex],
);

useEffect(() => {
refetchData(filterObj);
}, [filterObj, pagingController.pageNum, pagingController.pageSize]);
if (isReady) {
refetchData(filterObj);
}
}, [filterObj, pagingController.pageNum, pagingController.pageSize, tabIndex, isReady, refetchData]);

const onReset = useCallback(() => {
setFilteredEquipments(equipments);
}, [equipments]);
setFilterObj({});
setPagingController({
pageNum: 1,
pageSize: pagingController.pageSize,
});
}, [pagingController.pageSize]);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredItems(
// equipmentTypes.filter((pm) => {
// return (
// pm.code.toLowerCase().includes(query.code.toLowerCase()) &&
// pm.name.toLowerCase().includes(query.name.toLowerCase())
// );
// })
// );
setFilterObj({
...query,
});
}}
onReset={onReset}
/>
<SearchResults<EquipmentResult>
items={filteredEquipments}
columns={columns}
setPagingController={setPagingController}
pagingController={pagingController}
totalCount={totalCount}
isAutoPaging={false}
/>
<Box sx={{
"& .MuiTableContainer-root": {
overflowY: "auto",
"&::-webkit-scrollbar": {
width: "17px"
}
}
}}>
<EquipmentSearchResults<EquipmentResult>
items={filteredEquipments}
columns={columns}
setPagingController={setPagingController}
pagingController={pagingController}
totalCount={totalCount}
isAutoPaging={false}
/>
</Box>
</>
);
};

export default EquipmentSearch;
export default EquipmentSearch;

+ 482
- 0
src/components/EquipmentSearch/EquipmentSearchResults.tsx Просмотреть файл

@@ -0,0 +1,482 @@
"use client";

import React, {
ChangeEvent,
Dispatch,
MouseEvent,
SetStateAction,
useCallback,
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell, { TableCellProps } from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TablePagination, {
TablePaginationProps,
} from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow";
import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton";
import {
ButtonOwnProps,
Checkbox,
Icon,
IconOwnProps,
SxProps,
Theme,
} from "@mui/material";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { filter, remove, uniq } from "lodash";

export interface ResultWithId {
id: string | number;
}

type ColumnType = "icon" | "decimal" | "integer" | "checkbox";

interface BaseColumn<T extends ResultWithId> {
name: keyof T;
label: string;
align?: TableCellProps["align"];
headerAlign?: TableCellProps["align"];
sx?: SxProps<Theme> | undefined;
style?: Partial<HTMLElement["style"]> & { [propName: string]: string };
type?: ColumnType;
renderCell?: (params: T) => React.ReactNode;
}

interface IconColumn<T extends ResultWithId> extends BaseColumn<T> {
name: keyof T;
type: "icon";
icon?: React.ReactNode;
icons?: { [columnValue in keyof T]: React.ReactNode };
color?: IconOwnProps["color"];
colors?: { [columnValue in keyof T]: IconOwnProps["color"] };
}

interface DecimalColumn<T extends ResultWithId> extends BaseColumn<T> {
type: "decimal";
}

interface IntegerColumn<T extends ResultWithId> extends BaseColumn<T> {
type: "integer";
}

interface CheckboxColumn<T extends ResultWithId> extends BaseColumn<T> {
type: "checkbox";
disabled?: (params: T) => boolean;
// checkboxIds: readonly (string | number)[],
// setCheckboxIds: (ids: readonly (string | number)[]) => void
}

interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
onClick: (item: T) => void;
buttonIcon: React.ReactNode;
buttonIcons: { [columnValue in keyof T]: React.ReactNode };
buttonColor?: IconButtonOwnProps["color"];
}

export type Column<T extends ResultWithId> =
| BaseColumn<T>
| IconColumn<T>
| DecimalColumn<T>
| CheckboxColumn<T>
| ColumnWithAction<T>;

interface Props<T extends ResultWithId> {
totalCount?: number;
items: T[];
columns: Column<T>[];
noWrapper?: boolean;
setPagingController?: Dispatch<
SetStateAction<{
pageNum: number;
pageSize: number;
}>
>;
pagingController?: { pageNum: number; pageSize: number };
isAutoPaging?: boolean;
checkboxIds?: (string | number)[];
setCheckboxIds?: Dispatch<SetStateAction<(string | number)[]>>;
onRowClick?: (item: T) => void;
}

function isActionColumn<T extends ResultWithId>(
column: Column<T>,
): column is ColumnWithAction<T> {
return Boolean((column as ColumnWithAction<T>).onClick);
}

function isIconColumn<T extends ResultWithId>(
column: Column<T>,
): column is IconColumn<T> {
return column.type === "icon";
}

function isDecimalColumn<T extends ResultWithId>(
column: Column<T>,
): column is DecimalColumn<T> {
return column.type === "decimal";
}

function isIntegerColumn<T extends ResultWithId>(
column: Column<T>,
): column is IntegerColumn<T> {
return column.type === "integer";
}

function isCheckboxColumn<T extends ResultWithId>(
column: Column<T>,
): column is CheckboxColumn<T> {
return column.type === "checkbox";
}

// Icon Component Functions
function convertObjectKeysToLowercase<T extends object>(
obj: T,
): object | undefined {
return obj
? Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]),
)
: undefined;
}

function handleIconColors<T extends ResultWithId>(
column: IconColumn<T>,
value: T[keyof T],
): IconOwnProps["color"] {
const colors = convertObjectKeysToLowercase(column.colors ?? {});
const valueKey = String(value).toLowerCase() as keyof typeof colors;

if (colors && valueKey in colors) {
return colors[valueKey];
}

return column.color ?? "primary";
}

function handleIconIcons<T extends ResultWithId>(
column: IconColumn<T>,
value: T[keyof T],
): React.ReactNode {
const icons = convertObjectKeysToLowercase(column.icons ?? {});
const valueKey = String(value).toLowerCase() as keyof typeof icons;

if (icons && valueKey in icons) {
return icons[valueKey];
}

return column.icon ?? <CheckCircleOutlineIcon fontSize="small" />;
}
export const defaultPagingController: { pageNum: number; pageSize: number } = {
pageNum: 1,
pageSize: 10,
};

export type defaultSetPagingController = Dispatch<
SetStateAction<{
pageNum: number;
pageSize: number;
}>
>

function EquipmentSearchResults<T extends ResultWithId>({
items,
columns,
noWrapper,
pagingController,
setPagingController,
isAutoPaging = true,
totalCount,
checkboxIds = [],
setCheckboxIds = undefined,
onRowClick = undefined,
}: Props<T>) {
const { t } = useTranslation("common");
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
/// this
const handleChangePage: TablePaginationProps["onPageChange"] = (
_event,
newPage,
) => {
console.log(_event);
setPage(newPage);
if (setPagingController) {
setPagingController({
...(pagingController ?? defaultPagingController),
pageNum: newPage + 1,
});
}
};

const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = (
event,
) => {
console.log(event);
const newSize = +event.target.value;
setRowsPerPage(newSize);
setPage(0);
if (setPagingController) {
setPagingController({
...(pagingController ?? defaultPagingController),
pageNum: 1,
pageSize: newSize,
});
}
};

// checkbox
const currItems = useMemo(() => {
return items.length > 10 ? items
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((i) => i.id)
: items.map((i) => i.id)
}, [items, page, rowsPerPage])

const currItemsWithChecked = useMemo(() => {
return filter(checkboxIds, function (c) {
return currItems.includes(c);
})
}, [checkboxIds, items, page, rowsPerPage])

const handleRowClick = useCallback(
(event: MouseEvent<unknown>, item: T, columns: Column<T>[]) => {
// check is disabled or not
let disabled = false;
columns.forEach((col) => {
if (isCheckboxColumn(col) && col.disabled) {
disabled = col.disabled(item);
if (disabled) {
return;
}
}
});

if (disabled) {
return;
}

// set id
const id = item.id;
if (setCheckboxIds) {
const selectedIndex = checkboxIds.indexOf(id);
let newSelected: (string | number)[] = [];

if (selectedIndex === -1) {
newSelected = newSelected.concat(checkboxIds, id);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(checkboxIds.slice(1));
} else if (selectedIndex === checkboxIds.length - 1) {
newSelected = newSelected.concat(checkboxIds.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
checkboxIds.slice(0, selectedIndex),
checkboxIds.slice(selectedIndex + 1),
);
}
setCheckboxIds(newSelected);
}
},
[checkboxIds, setCheckboxIds],
);

const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (setCheckboxIds) {
const pageItemId = currItems

if (event.target.checked) {
setCheckboxIds((prev) => uniq([...prev, ...pageItemId]))
} else {
setCheckboxIds((prev) => filter(prev, function (p) { return !pageItemId.includes(p); }))
}
}
}

const table = (
<>
<TableContainer sx={{ maxHeight: 440 }}>
<Table stickyHeader>
<TableHead>
<TableRow>
{columns.map((column, idx) => (
isCheckboxColumn(column) ?
<TableCell
align={column.headerAlign}
sx={column.sx}
key={`${column.name.toString()}${idx}`}
>
<Checkbox
color="primary"
indeterminate={currItemsWithChecked.length > 0 && currItemsWithChecked.length < currItems.length}
checked={currItems.length > 0 && currItemsWithChecked.length >= currItems.length}
onChange={handleSelectAllClick}
/>
</TableCell>
: <TableCell
align={column.headerAlign}
sx={column.sx}
key={`${column.name.toString()}${idx}`}
>
{column.label.split('\n').map((line, index) => (
<div key={index}>{line}</div> // Render each line in a div
))}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{isAutoPaging
? items
.slice((pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage),
(pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage) + (pagingController?.pageSize ?? rowsPerPage))
.map((item) => {
return (
<TableRow
hover
tabIndex={-1}
key={item.id}
onClick={(event) => {
setCheckboxIds
? handleRowClick(event, item, columns)
: undefined

if (onRowClick) {
onRowClick(item)
}
}
}
role={setCheckboxIds ? "checkbox" : undefined}
>
{columns.map((column, idx) => {
const columnName = column.name;

return (
<TabelCells
key={`${columnName.toString()}-${idx}`}
column={column}
columnName={columnName}
idx={idx}
item={item}
checkboxIds={checkboxIds}
/>
);
})}
</TableRow>
);
})
: items.map((item) => {
return (
<TableRow hover tabIndex={-1} key={item.id}
onClick={(event) => {
setCheckboxIds
? handleRowClick(event, item, columns)
: undefined

if (onRowClick) {
onRowClick(item)
}
}
}
role={setCheckboxIds ? "checkbox" : undefined}
>
{columns.map((column, idx) => {
const columnName = column.name;

return (
<TabelCells
key={`${columnName.toString()}-${idx}`}
column={column}
columnName={columnName}
idx={idx}
item={item}
checkboxIds={checkboxIds}
/>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={!totalCount || totalCount == 0 ? items.length : totalCount}
rowsPerPage={pagingController?.pageSize ? pagingController?.pageSize : rowsPerPage}
page={pagingController?.pageNum ? pagingController?.pageNum - 1 : page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);

return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>;
}

// Table cells
interface TableCellsProps<T extends ResultWithId> {
column: Column<T>;
columnName: keyof T;
idx: number;
item: T;
checkboxIds: (string | number)[];
}

function TabelCells<T extends ResultWithId>({
column,
columnName,
idx,
item,
checkboxIds = [],
}: TableCellsProps<T>) {
const isItemSelected = checkboxIds.includes(item.id);

return (
<TableCell
align={column.align}
sx={column.sx}
key={`${columnName.toString()}-${idx}`}
>
{isActionColumn(column) ? (
<IconButton
color={column.buttonColor ?? "primary"}
onClick={() => column.onClick(item)}
>
{column.buttonIcon}
</IconButton>
) : isIconColumn(column) ? (
<Icon color={handleIconColors(column, item[columnName])}>
{handleIconIcons(column, item[columnName])}
</Icon>
) : isDecimalColumn(column) ? (
<>{decimalFormatter.format(Number(item[columnName]))}</>
) : isIntegerColumn(column) ? (
<>{integerFormatter.format(Number(item[columnName]))}</>
) : isCheckboxColumn(column) ? (
<Checkbox
disabled={column.disabled ? column.disabled(item) : undefined}
checked={isItemSelected}
/>
) : column.renderCell ? (
column.renderCell(item)
) : (
<>{item[columnName] as string}</>
)}
</TableCell>
);
}

export default EquipmentSearchResults;

+ 24
- 17
src/components/EquipmentSearch/EquipmentSearchWrapper.tsx Просмотреть файл

@@ -1,28 +1,35 @@
import { fetchAllEquipments } from "@/app/api/settings/equipment";
import EquipmentSearchLoading from "./EquipmentSearchLoading";
import { SearchParams } from "@/app/utils/fetchUtil";
import { TypeEnum } from "@/app/utils/typeEnum";
import { notFound } from "next/navigation";
"use client";

import { useState, useEffect } from "react";
import EquipmentSearch from "./EquipmentSearch";
import EquipmentSearchLoading from "./EquipmentSearchLoading";
import EquipmentTabs from "@/app/(main)/settings/equipment/EquipmentTabs";
import { useSearchParams } from "next/navigation";

interface SubComponents {
Loading: typeof EquipmentSearchLoading;
}

type Props = {
// type: TypeEnum;
};
const EquipmentSearchWrapper: React.FC & SubComponents = () => {
const searchParams = useSearchParams();
const tabFromUrl = searchParams.get("tab");
const initialTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0;
const [tabIndex, setTabIndex] = useState(initialTabIndex);

useEffect(() => {
const tabFromUrl = searchParams.get("tab");
const newTabIndex = tabFromUrl ? parseInt(tabFromUrl, 10) : 0;
setTabIndex(newTabIndex);
}, [searchParams]);

const EquipmentSearchWrapper: React.FC<Props> & SubComponents = async (
{
// type,
},
) => {
// console.log(type)
// var result = await fetchAllEquipmentTypes()
return <EquipmentSearch equipments={[]} />;
return (
<>
<EquipmentTabs onTabChange={setTabIndex} />
<EquipmentSearch equipments={[]} tabIndex={tabIndex} />
</>
);
};

EquipmentSearchWrapper.Loading = EquipmentSearchLoading;

export default EquipmentSearchWrapper;
export default EquipmentSearchWrapper;

+ 242
- 0
src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx Просмотреть файл

@@ -0,0 +1,242 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import {
Button,
Card,
CardContent,
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
Stack,
Grid,
} from "@mui/material";
import { Check, Close } from "@mui/icons-material";
import axiosInstance from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";

type Props = {
id: number;
};

type EquipmentDetailData = {
id: number;
code: string;
name: string;
equipmentCode?: string;
repairAndMaintenanceStatus?: boolean | null;
repairAndMaintenanceRemarks?: string | null;
};

const UpdateMaintenanceForm: React.FC<Props> = ({ id }) => {
const { t } = useTranslation("common");
const router = useRouter();
const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(true);
const [equipmentData, setEquipmentData] = useState<EquipmentDetailData | null>(null);
const [status, setStatus] = useState<boolean | null>(null);
const [remarks, setRemarks] = useState<string>("");

useEffect(() => {
const fetchEquipmentDetail = async () => {
try {
setFetching(true);
const response = await axiosInstance.get<EquipmentDetailData>(
`${NEXT_PUBLIC_API_URL}/EquipmentDetail/details/${id}`
);
if (response.data) {
setEquipmentData(response.data);
setStatus(response.data.repairAndMaintenanceStatus ?? null);
setRemarks(response.data.repairAndMaintenanceRemarks ?? "");
}
} catch (error) {
console.error("Error fetching equipment detail:", error);
} finally {
setFetching(false);
}
};

fetchEquipmentDetail();
}, [id]);

const handleSave = useCallback(async () => {
if (!equipmentData) return;

try {
setLoading(true);
const updateData = {
repairAndMaintenanceStatus: status,
repairAndMaintenanceRemarks: remarks,
};

await axiosInstance.put(
`${NEXT_PUBLIC_API_URL}/EquipmentDetail/update/${id}`,
updateData,
{
headers: { "Content-Type": "application/json" },
}
);

router.push("/settings/equipment?tab=1");
} catch (error) {
console.error("Error updating maintenance:", error);
alert(t("Error saving data") || "Error saving data");
} finally {
setLoading(false);
}
}, [equipmentData, status, remarks, id, router, t]);

const handleCancel = useCallback(() => {
router.push("/settings/equipment?tab=1");
}, [router]);

if (fetching) {
return (
<Stack sx={{ p: 3 }}>
<Typography>{t("Loading") || "Loading..."}</Typography>
</Stack>
);
}

if (!equipmentData) {
return (
<Stack sx={{ p: 3 }}>
<Typography>{t("Equipment not found") || "Equipment not found"}</Typography>
</Stack>
);
}

return (
<Stack
spacing={2}
component="form"
>
<Card>
<CardContent component={Stack} spacing={4}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Equipment Information")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Equipment Name") || "設備名稱"}
value={equipmentData.code || ""}
disabled
fullWidth
variant="filled"
InputLabelProps={{
shrink: !!equipmentData.code,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: { paddingTop: "8px" },
}}
/>
</Grid>

<Grid item xs={6}>
<TextField
label={t("Equipment Code") || "設備編號"}
value={equipmentData.equipmentCode || ""}
disabled
fullWidth
variant="filled"
InputLabelProps={{
shrink: !!equipmentData.equipmentCode,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: { paddingTop: "8px" },
}}
/>
</Grid>

<Grid item xs={6}>
<FormControl fullWidth variant="filled">
<InputLabel
shrink={status !== null}
sx={{ fontSize: "0.9375rem" }}
>
{t("Repair and Maintenance Status")}
</InputLabel>
<Select
value={status === null ? "" : status ? "yes" : "no"}
onChange={(e) => {
const value = e.target.value;
if (value === "yes") {
setStatus(true);
} else if (value === "no") {
setStatus(false);
} else {
setStatus(null);
}
}}
sx={{ paddingTop: "8px" }}
>
<MenuItem value="yes">{t("Yes")}</MenuItem>
<MenuItem value="no">{t("No")}</MenuItem>
</Select>
</FormControl>
</Grid>

<Grid item xs={6}>
<TextField
label={t("Repair and Maintenance Remarks")}
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
fullWidth
multiline
rows={4}
variant="filled"
InputLabelProps={{
shrink: true,
sx: { fontSize: "0.9375rem" },
}}
InputProps={{
sx: {
paddingTop: "8px",
alignItems: "flex-start",
paddingBottom: "8px",
},
}}
sx={{
"& .MuiInputBase-input": {
paddingTop: "16px",
},
}}
/>
</Grid>
</Grid>
</CardContent>
</Card>

<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
disabled={loading}
type="button"
>
{t("Cancel") || "取消"}
</Button>
<Button
variant="contained"
startIcon={<Check />}
onClick={handleSave}
disabled={loading}
type="button"
>
{t("Save") || "保存"}
</Button>
</Stack>
</Stack>
);
};

export default UpdateMaintenanceForm;

+ 18
- 1
src/i18n/en/common.json Просмотреть файл

@@ -1,3 +1,20 @@
{
"Grade {{grade}}": "Grade {{grade}}"
"Grade {{grade}}": "Grade {{grade}}",
"General Data": "General Data",
"Repair and Maintenance": "Repair and Maintenance",
"Repair and Maintenance Status": "Repair and Maintenance Status",
"Latest Repair and Maintenance Date": "Latest Repair and Maintenance Date",
"Last Repair and Maintenance Date": "Last Repair and Maintenance Date",
"Repair and Maintenance Remarks": "Repair and Maintenance Remarks",
"Update Equipment Maintenance and Repair": "Update Equipment Maintenance and Repair",
"Equipment Information": "Equipment Information",
"Loading": "Loading...",
"Equipment not found": "Equipment not found",
"Error saving data": "Error saving data",
"Cancel": "Cancel",
"Save": "Save",
"Yes": "Yes",
"No": "No",
"Equipment Name": "Equipment Name",
"Equipment Code": "Equipment Code"
}

+ 17
- 1
src/i18n/zh/common.json Просмотреть файл

@@ -366,5 +366,21 @@
"Shop Detail": "店鋪詳情",
"Truck Lane Detail": "卡車路線詳情",
"Filter by Status": "按狀態篩選",
"All": "全部"
"All": "全部",
"General Data": "基本資料",
"Repair and Maintenance": "維修和保養",
"Repair and Maintenance Status": "維修和保養狀態",
"Latest Repair and Maintenance Date": "最新維修和保養日期",
"Last Repair and Maintenance Date": "上次維修和保養日期",
"Repair and Maintenance Remarks": "維修和保養備註",
"Rows per page": "每頁行數",
"Equipment Name": "設備名稱",
"Equipment Code": "設備編號",
"Yes": "是",
"No": "否",
"Update Equipment Maintenance and Repair": "更新設備的維修和保養",
"Equipment Information": "設備資訊",
"Loading": "載入中...",
"Equipment not found": "找不到設備",
"Error saving data": "保存數據時出錯"
}

+ 0
- 0
src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt Просмотреть файл


Загрузка…
Отмена
Сохранить