From 7cc2716b400cc226171703f268893be30287be53 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 5 Jan 2026 19:28:16 +0800 Subject: [PATCH] Supporting Function: Equipment Repair & Maintenance --- .../settings/equipment/EquipmentTabs.tsx | 52 ++ .../equipment/MaintenanceEdit/page.tsx | 29 ++ src/app/(main)/settings/equipment/page.tsx | 21 +- src/app/api/settings/equipment/index.ts | 5 + .../EquipmentSearch/EquipmentSearch.tsx | 269 ++++++++-- .../EquipmentSearchResults.tsx | 482 ++++++++++++++++++ .../EquipmentSearchWrapper.tsx | 41 +- .../UpdateMaintenanceForm.tsx | 242 +++++++++ src/i18n/en/common.json | 19 +- src/i18n/zh/common.json | 18 +- .../master/service/EquipmentService.kt | 0 11 files changed, 1105 insertions(+), 73 deletions(-) create mode 100644 src/app/(main)/settings/equipment/EquipmentTabs.tsx create mode 100644 src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx create mode 100644 src/components/EquipmentSearch/EquipmentSearchResults.tsx create mode 100644 src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx create mode 100644 src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt diff --git a/src/app/(main)/settings/equipment/EquipmentTabs.tsx b/src/app/(main)/settings/equipment/EquipmentTabs.tsx new file mode 100644 index 0000000..d4e6a5b --- /dev/null +++ b/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 = ({ 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 ( + + + + + ); +}; + +export default EquipmentTabs; \ No newline at end of file diff --git a/src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx b/src/app/(main)/settings/equipment/MaintenanceEdit/page.tsx new file mode 100644 index 0000000..65c233f --- /dev/null +++ b/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 = async ({ searchParams }) => { + const type = "common"; + const { t } = await getServerI18n(type); + const id = isString(searchParams["id"]) + ? parseInt(searchParams["id"]) + : undefined; + if (!id) { + notFound(); + } + return ( + <> + {t("Update Equipment Maintenance and Repair")} + + + + + ); +}; +export default MaintenanceEditPage; \ No newline at end of file diff --git a/src/app/(main)/settings/equipment/page.tsx b/src/app/(main)/settings/equipment/page.tsx index 4456a31..f55631c 100644 --- a/src/app/(main)/settings/equipment/page.tsx +++ b/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 () => { {t("Equipment")} - {/* */} - }> + }> - + ); }; -export default productSetting; +export default productSetting; \ No newline at end of file diff --git a/src/app/api/settings/equipment/index.ts b/src/app/api/settings/equipment/index.ts index 748f076..c64251a 100644 --- a/src/app/api/settings/equipment/index.ts +++ b/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 = { diff --git a/src/components/EquipmentSearch/EquipmentSearch.tsx b/src/components/EquipmentSearch/EquipmentSearch.tsx index 4f00dc6..735b2a8 100644 --- a/src/components/EquipmentSearch/EquipmentSearch.tsx +++ b/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>; type SearchParamNames = keyof SearchQuery; -const EquipmentSearch: React.FC = ({ equipments }) => { +const EquipmentSearch: React.FC = ({ equipments, tabIndex = 0 }) => { const [filteredEquipments, setFilteredEquipments] = - useState(equipments); + useState([]); 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[] = useMemo(() => { - const searchCriteria: Criterion[] = [ + 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 = ({ 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[]>( + const generalDataColumns = useMemo[]>( () => [ { name: "id", @@ -78,9 +143,91 @@ const EquipmentSearch: React.FC = ({ equipments }) => { onClick: onDeleteClick, }, ], - [filteredEquipments], + [onDetailClick, onDeleteClick, t], + ); + + const repairMaintenanceColumns = useMemo[]>( + () => [ + { + name: "id", + label: "編輯", + onClick: onMaintenanceEditClick, + buttonIcon: , + 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 ( + + 正在維護中 + + ); + } else if (status === 0 || status === false) { + return ( + + 正常使用中 + + ); + } + 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 { records: T[]; @@ -89,73 +236,115 @@ const EquipmentSearch: React.FC = ({ 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>( - `${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 ( <> { - // setFilteredItems( - // equipmentTypes.filter((pm) => { - // return ( - // pm.code.toLowerCase().includes(query.code.toLowerCase()) && - // pm.name.toLowerCase().includes(query.name.toLowerCase()) - // ); - // }) - // ); setFilterObj({ ...query, }); }} onReset={onReset} /> - - items={filteredEquipments} - columns={columns} - setPagingController={setPagingController} - pagingController={pagingController} - totalCount={totalCount} - isAutoPaging={false} - /> + + + items={filteredEquipments} + columns={columns} + setPagingController={setPagingController} + pagingController={pagingController} + totalCount={totalCount} + isAutoPaging={false} + /> + ); }; -export default EquipmentSearch; +export default EquipmentSearch; \ No newline at end of file diff --git a/src/components/EquipmentSearch/EquipmentSearchResults.tsx b/src/components/EquipmentSearch/EquipmentSearchResults.tsx new file mode 100644 index 0000000..7f84a41 --- /dev/null +++ b/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 { + name: keyof T; + label: string; + align?: TableCellProps["align"]; + headerAlign?: TableCellProps["align"]; + sx?: SxProps | undefined; + style?: Partial & { [propName: string]: string }; + type?: ColumnType; + renderCell?: (params: T) => React.ReactNode; +} + +interface IconColumn extends BaseColumn { + 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 extends BaseColumn { + type: "decimal"; +} + +interface IntegerColumn extends BaseColumn { + type: "integer"; +} + +interface CheckboxColumn extends BaseColumn { + type: "checkbox"; + disabled?: (params: T) => boolean; + // checkboxIds: readonly (string | number)[], + // setCheckboxIds: (ids: readonly (string | number)[]) => void +} + +interface ColumnWithAction extends BaseColumn { + onClick: (item: T) => void; + buttonIcon: React.ReactNode; + buttonIcons: { [columnValue in keyof T]: React.ReactNode }; + buttonColor?: IconButtonOwnProps["color"]; +} + +export type Column = + | BaseColumn + | IconColumn + | DecimalColumn + | CheckboxColumn + | ColumnWithAction; + +interface Props { + totalCount?: number; + items: T[]; + columns: Column[]; + noWrapper?: boolean; + setPagingController?: Dispatch< + SetStateAction<{ + pageNum: number; + pageSize: number; + }> + >; + pagingController?: { pageNum: number; pageSize: number }; + isAutoPaging?: boolean; + checkboxIds?: (string | number)[]; + setCheckboxIds?: Dispatch>; + onRowClick?: (item: T) => void; +} + +function isActionColumn( + column: Column, +): column is ColumnWithAction { + return Boolean((column as ColumnWithAction).onClick); +} + +function isIconColumn( + column: Column, +): column is IconColumn { + return column.type === "icon"; +} + +function isDecimalColumn( + column: Column, +): column is DecimalColumn { + return column.type === "decimal"; +} + +function isIntegerColumn( + column: Column, +): column is IntegerColumn { + return column.type === "integer"; +} + +function isCheckboxColumn( + column: Column, +): column is CheckboxColumn { + return column.type === "checkbox"; +} + +// Icon Component Functions +function convertObjectKeysToLowercase( + obj: T, +): object | undefined { + return obj + ? Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), + ) + : undefined; +} + +function handleIconColors( + column: IconColumn, + 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( + column: IconColumn, + 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 ?? ; +} +export const defaultPagingController: { pageNum: number; pageSize: number } = { + pageNum: 1, + pageSize: 10, +}; + +export type defaultSetPagingController = Dispatch< + SetStateAction<{ + pageNum: number; + pageSize: number; + }> +> + +function EquipmentSearchResults({ + items, + columns, + noWrapper, + pagingController, + setPagingController, + isAutoPaging = true, + totalCount, + checkboxIds = [], + setCheckboxIds = undefined, + onRowClick = undefined, +}: Props) { + 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, item: T, columns: Column[]) => { + // 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) => { + 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 = ( + <> + + + + + {columns.map((column, idx) => ( + isCheckboxColumn(column) ? + + 0 && currItemsWithChecked.length < currItems.length} + checked={currItems.length > 0 && currItemsWithChecked.length >= currItems.length} + onChange={handleSelectAllClick} + /> + + : + {column.label.split('\n').map((line, index) => ( +
{line}
// Render each line in a div + ))} +
+ ))} +
+
+ + {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 ( + { + setCheckboxIds + ? handleRowClick(event, item, columns) + : undefined + + if (onRowClick) { + onRowClick(item) + } + } + } + role={setCheckboxIds ? "checkbox" : undefined} + > + {columns.map((column, idx) => { + const columnName = column.name; + + return ( + + ); + })} + + ); + }) + : items.map((item) => { + return ( + { + setCheckboxIds + ? handleRowClick(event, item, columns) + : undefined + + if (onRowClick) { + onRowClick(item) + } + } + } + role={setCheckboxIds ? "checkbox" : undefined} + > + {columns.map((column, idx) => { + const columnName = column.name; + + return ( + + ); + })} + + ); + })} + +
+
+ + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } +/> + + ); + + return noWrapper ? table : {table}; +} + +// Table cells +interface TableCellsProps { + column: Column; + columnName: keyof T; + idx: number; + item: T; + checkboxIds: (string | number)[]; +} + +function TabelCells({ + column, + columnName, + idx, + item, + checkboxIds = [], +}: TableCellsProps) { + const isItemSelected = checkboxIds.includes(item.id); + + return ( + + {isActionColumn(column) ? ( + column.onClick(item)} + > + {column.buttonIcon} + + ) : isIconColumn(column) ? ( + + {handleIconIcons(column, item[columnName])} + + ) : isDecimalColumn(column) ? ( + <>{decimalFormatter.format(Number(item[columnName]))} + ) : isIntegerColumn(column) ? ( + <>{integerFormatter.format(Number(item[columnName]))} + ) : isCheckboxColumn(column) ? ( + + ) : column.renderCell ? ( + column.renderCell(item) + ) : ( + <>{item[columnName] as string} + )} + + ); +} + +export default EquipmentSearchResults; \ No newline at end of file diff --git a/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx b/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx index 0efe9a7..a1cf35d 100644 --- a/src/components/EquipmentSearch/EquipmentSearchWrapper.tsx +++ b/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 & SubComponents = async ( - { - // type, - }, -) => { - // console.log(type) - // var result = await fetchAllEquipmentTypes() - return ; + return ( + <> + + + + ); }; EquipmentSearchWrapper.Loading = EquipmentSearchLoading; -export default EquipmentSearchWrapper; +export default EquipmentSearchWrapper; \ No newline at end of file diff --git a/src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx b/src/components/UpdateMaintenance/UpdateMaintenanceForm.tsx new file mode 100644 index 0000000..1d319f2 --- /dev/null +++ b/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 = ({ id }) => { + const { t } = useTranslation("common"); + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(true); + const [equipmentData, setEquipmentData] = useState(null); + const [status, setStatus] = useState(null); + const [remarks, setRemarks] = useState(""); + + useEffect(() => { + const fetchEquipmentDetail = async () => { + try { + setFetching(true); + const response = await axiosInstance.get( + `${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 ( + + {t("Loading") || "Loading..."} + + ); + } + + if (!equipmentData) { + return ( + + {t("Equipment not found") || "Equipment not found"} + + ); + } + + return ( + + + + + {t("Equipment Information")} + + + + + + + + + + + + + + {t("Repair and Maintenance Status")} + + + + + + + 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", + }, + }} + /> + + + + + + + + + + + ); +}; + +export default UpdateMaintenanceForm; \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 2b2f3a3..d9bdc2e 100644 --- a/src/i18n/en/common.json +++ b/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" } \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 61cc8e9..fa723e6 100644 --- a/src/i18n/zh/common.json +++ b/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": "保存數據時出錯" } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt new file mode 100644 index 0000000..e69de29