| @@ -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; | |||||
| @@ -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; | |||||
| @@ -1,15 +1,18 @@ | |||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import EquipmentSearch from "@/components/EquipmentSearch"; | |||||
| import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
| import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import Tab from "@mui/material/Tab"; | |||||
| import Tabs from "@mui/material/Tabs"; | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
| import { fetchAllEquipments } from "@/app/api/settings/equipment"; | import { fetchAllEquipments } from "@/app/api/settings/equipment"; | ||||
| import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
| import EquipmentSearchWrapper from "@/components/EquipmentSearch/EquipmentSearchWrapper"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Equipment Type", | title: "Equipment Type", | ||||
| }; | }; | ||||
| @@ -17,8 +20,6 @@ export const metadata: Metadata = { | |||||
| const productSetting: React.FC = async () => { | const productSetting: React.FC = async () => { | ||||
| const type = "common"; | const type = "common"; | ||||
| const { t } = await getServerI18n(type); | const { t } = await getServerI18n(type); | ||||
| const equipments = await fetchAllEquipments(); | |||||
| // preloadClaims(); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -31,22 +32,14 @@ const productSetting: React.FC = async () => { | |||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| {t("Equipment")} | {t("Equipment")} | ||||
| </Typography> | </Typography> | ||||
| {/* <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="product/create" | |||||
| > | |||||
| {t("Create product")} | |||||
| </Button> */} | |||||
| </Stack> | </Stack> | ||||
| <Suspense fallback={<EquipmentSearch.Loading />}> | |||||
| <Suspense fallback={<EquipmentSearchWrapper.Loading />}> | |||||
| <I18nProvider namespaces={["common", "project"]}> | <I18nProvider namespaces={["common", "project"]}> | ||||
| <EquipmentSearch /> | |||||
| <EquipmentSearchWrapper /> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| </Suspense> | </Suspense> | ||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default productSetting; | |||||
| export default productSetting; | |||||
| @@ -13,7 +13,12 @@ export type EquipmentResult = { | |||||
| name: string; | name: string; | ||||
| description: string | undefined; | description: string | undefined; | ||||
| equipmentTypeId: string | number | undefined; | equipmentTypeId: string | number | undefined; | ||||
| equipmentCode?: string; | |||||
| action?: any; | action?: any; | ||||
| repairAndMaintenanceStatus?: boolean | number; | |||||
| latestRepairAndMaintenanceDate?: string | Date; | |||||
| lastRepairAndMaintenanceDate?: string | Date; | |||||
| repairAndMaintenanceRemarks?: string; | |||||
| }; | }; | ||||
| export type Result = { | export type Result = { | ||||
| @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { EquipmentResult } from "@/app/api/settings/equipment"; | import { EquipmentResult } from "@/app/api/settings/equipment"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | |||||
| import EquipmentSearchResults, { Column } from "./EquipmentSearchResults"; | |||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | import { GridDeleteIcon } from "@mui/x-data-grid"; | ||||
| @@ -12,32 +12,90 @@ import { TypeEnum } from "@/app/utils/typeEnum"; | |||||
| import axios from "axios"; | import axios from "axios"; | ||||
| import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | 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 = { | type Props = { | ||||
| equipments: EquipmentResult[]; | equipments: EquipmentResult[]; | ||||
| tabIndex?: number; | |||||
| }; | }; | ||||
| type SearchQuery = Partial<Omit<EquipmentResult, "id">>; | type SearchQuery = Partial<Omit<EquipmentResult, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| const EquipmentSearch: React.FC<Props> = ({ equipments, tabIndex = 0 }) => { | |||||
| const [filteredEquipments, setFilteredEquipments] = | const [filteredEquipments, setFilteredEquipments] = | ||||
| useState<EquipmentResult[]>(equipments); | |||||
| useState<EquipmentResult[]>([]); | |||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [filterObj, setFilterObj] = useState({}); | const [filterObj, setFilterObj] = useState({}); | ||||
| const [pagingController, setPagingController] = useState({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 1, | pageNum: 1, | ||||
| pageSize: 10, | pageSize: 10, | ||||
| // totalCount: 0, | |||||
| }); | }); | ||||
| const [totalCount, setTotalCount] = useState(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>[] = 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("Code"), paramName: "code", type: "text" }, | ||||
| { label: t("Description"), paramName: "description", type: "text" }, | { label: t("Description"), paramName: "description", type: "text" }, | ||||
| ]; | ]; | ||||
| return searchCriteria; | |||||
| }, [t, equipments]); | |||||
| }, [t, tabIndex]); | |||||
| const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
| (equipment: EquipmentResult) => { | (equipment: EquipmentResult) => { | ||||
| @@ -46,12 +104,19 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| [router], | [router], | ||||
| ); | ); | ||||
| const onMaintenanceEditClick = useCallback( | |||||
| (equipment: EquipmentResult) => { | |||||
| router.push(`/settings/equipment/MaintenanceEdit?id=${equipment.id}`); | |||||
| }, | |||||
| [router], | |||||
| ); | |||||
| const onDeleteClick = useCallback( | const onDeleteClick = useCallback( | ||||
| (equipment: EquipmentResult) => {}, | (equipment: EquipmentResult) => {}, | ||||
| [router], | [router], | ||||
| ); | ); | ||||
| const columns = useMemo<Column<EquipmentResult>[]>( | |||||
| const generalDataColumns = useMemo<Column<EquipmentResult>[]>( | |||||
| () => [ | () => [ | ||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| @@ -78,9 +143,91 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| onClick: onDeleteClick, | 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> { | interface ApiResponse<T> { | ||||
| records: T[]; | records: T[]; | ||||
| @@ -89,73 +236,115 @@ const EquipmentSearch: React.FC<Props> = ({ equipments }) => { | |||||
| const refetchData = useCallback( | const refetchData = useCallback( | ||||
| async (filterObj: SearchQuery) => { | 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; | 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 = { | const params = { | ||||
| pageNum: pagingController.pageNum, | pageNum: pagingController.pageNum, | ||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| ...filterObj, | |||||
| ...transformedFilter, | |||||
| }; | }; | ||||
| try { | try { | ||||
| const endpoint = tabIndex === 1 | |||||
| ? `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage` | |||||
| : `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`; | |||||
| const response = await axiosInstance.get<ApiResponse<EquipmentResult>>( | const response = await axiosInstance.get<ApiResponse<EquipmentResult>>( | ||||
| `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`, | |||||
| endpoint, | |||||
| { params }, | { 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) { | if (response.status == 200) { | ||||
| setFilteredEquipments(response.data.records); | |||||
| setTotalCount(response.data.total); | |||||
| return response; | |||||
| setFilteredEquipments(response.data.records || []); | |||||
| setTotalCount(response.data.total || 0); | |||||
| } else { | } else { | ||||
| throw "400"; | throw "400"; | ||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error fetching equipment types:", 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(() => { | useEffect(() => { | ||||
| refetchData(filterObj); | |||||
| }, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||||
| if (isReady) { | |||||
| refetchData(filterObj); | |||||
| } | |||||
| }, [filterObj, pagingController.pageNum, pagingController.pageSize, tabIndex, isReady, refetchData]); | |||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| setFilteredEquipments(equipments); | |||||
| }, [equipments]); | |||||
| setFilterObj({}); | |||||
| setPagingController({ | |||||
| pageNum: 1, | |||||
| pageSize: pagingController.pageSize, | |||||
| }); | |||||
| }, [pagingController.pageSize]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| // setFilteredItems( | |||||
| // equipmentTypes.filter((pm) => { | |||||
| // return ( | |||||
| // pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| // pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||||
| // ); | |||||
| // }) | |||||
| // ); | |||||
| setFilterObj({ | setFilterObj({ | ||||
| ...query, | ...query, | ||||
| }); | }); | ||||
| }} | }} | ||||
| onReset={onReset} | 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; | |||||
| @@ -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; | |||||
| @@ -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 EquipmentSearch from "./EquipmentSearch"; | ||||
| import EquipmentSearchLoading from "./EquipmentSearchLoading"; | |||||
| import EquipmentTabs from "@/app/(main)/settings/equipment/EquipmentTabs"; | |||||
| import { useSearchParams } from "next/navigation"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof EquipmentSearchLoading; | 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; | EquipmentSearchWrapper.Loading = EquipmentSearchLoading; | ||||
| export default EquipmentSearchWrapper; | |||||
| export default EquipmentSearchWrapper; | |||||
| @@ -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; | |||||
| @@ -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" | |||||
| } | } | ||||
| @@ -366,5 +366,21 @@ | |||||
| "Shop Detail": "店鋪詳情", | "Shop Detail": "店鋪詳情", | ||||
| "Truck Lane Detail": "卡車路線詳情", | "Truck Lane Detail": "卡車路線詳情", | ||||
| "Filter by Status": "按狀態篩選", | "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": "保存數據時出錯" | |||||
| } | } | ||||