|
- "use client";
-
- import React, { useCallback, useEffect, useMemo, useState } from "react";
- import SearchBox, { Criterion } from "../SearchBox";
- import { EquipmentResult } from "@/app/api/settings/equipment";
- import { useTranslation } from "react-i18next";
- import EquipmentSearchResults, { Column } from "./EquipmentSearchResults";
- import { EditNote } from "@mui/icons-material";
- import { useRouter } from "next/navigation";
- 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";
- import IconButton from "@mui/material/IconButton";
- import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
- import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
- import CircularProgress from "@mui/material/CircularProgress";
- import TableRow from "@mui/material/TableRow";
- import TableCell from "@mui/material/TableCell";
- import Collapse from "@mui/material/Collapse";
- import Grid from "@mui/material/Grid";
- import DeleteIcon from "@mui/icons-material/Delete";
- import AddIcon from "@mui/icons-material/Add";
- import Button from "@mui/material/Button";
- import Dialog from "@mui/material/Dialog";
- import DialogTitle from "@mui/material/DialogTitle";
- import DialogContent from "@mui/material/DialogContent";
- import DialogContentText from "@mui/material/DialogContentText";
- import DialogActions from "@mui/material/DialogActions";
- import TextField from "@mui/material/TextField";
- import Autocomplete from "@mui/material/Autocomplete";
- import InputAdornment from "@mui/material/InputAdornment";
-
- type Props = {
- equipments: EquipmentResult[];
- tabIndex?: number;
- };
- type SearchQuery = Partial<Omit<EquipmentResult, "id">>;
- type SearchParamNames = keyof SearchQuery;
-
- const EquipmentSearch: React.FC<Props> = ({ equipments, tabIndex = 0 }) => {
- const [filteredEquipments, setFilteredEquipments] =
- useState<EquipmentResult[]>([]);
- const { t } = useTranslation("common");
- const router = useRouter();
- const [filterObjByTab, setFilterObjByTab] = useState<Record<number, SearchQuery>>({
- 0: {},
- 1: {},
- });
- const [pagingControllerByTab, setPagingControllerByTab] = useState<Record<number, { pageNum: number; pageSize: number }>>({
- 0: { pageNum: 1, pageSize: 10 },
- 1: { pageNum: 1, pageSize: 10 },
- });
- const [totalCount, setTotalCount] = useState(0);
- const [isLoading, setIsLoading] = useState(true);
- const [isReady, setIsReady] = useState(false);
-
- const filterObj = filterObjByTab[tabIndex] || {};
- const pagingController = pagingControllerByTab[tabIndex] || { pageNum: 1, pageSize: 10 };
-
- const [expandedRows, setExpandedRows] = useState<Set<string | number>>(new Set());
- const [equipmentDetailsMap, setEquipmentDetailsMap] = useState<Map<string | number, EquipmentResult[]>>(new Map());
- const [loadingDetailsMap, setLoadingDetailsMap] = useState<Map<string | number, boolean>>(new Map());
-
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
- const [itemToDelete, setItemToDelete] = useState<{ id: string | number; equipmentId: string | number } | null>(null);
- const [deleting, setDeleting] = useState(false);
-
- const [addDialogOpen, setAddDialogOpen] = useState(false);
- const [equipmentList, setEquipmentList] = useState<EquipmentResult[]>([]);
- const [selectedDescription, setSelectedDescription] = useState<string>("");
- const [selectedName, setSelectedName] = useState<string>("");
- const [selectedEquipmentCode, setSelectedEquipmentCode] = useState<string>("");
- const [equipmentCodePrefix, setEquipmentCodePrefix] = useState<string>("");
- const [equipmentCodeNumber, setEquipmentCodeNumber] = useState<string>("");
- const [isExistingCombination, setIsExistingCombination] = useState(false);
- const [loadingEquipments, setLoadingEquipments] = useState(false);
- const [saving, setSaving] = 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(() => {
- if (tabIndex === 1) {
- return [
- {
- label: "設備名稱/設備編號",
- paramName: "equipmentCode",
- type: "text"
- },
- {
- label: t("Repair and Maintenance Status"),
- paramName: "repairAndMaintenanceStatus",
- type: "select",
- options: ["正常使用中", "正在維護中"]
- },
- ];
- }
-
- return [
- { label: "設備編號", paramName: "code", type: "text" },
- ];
- }, [t, tabIndex]);
-
- const onMaintenanceEditClick = useCallback(
- (equipment: EquipmentResult) => {
- router.push(`/settings/equipment/MaintenanceEdit?id=${equipment.id}`);
- },
- [router],
- );
-
- const onDeleteClick = useCallback(
- (equipment: EquipmentResult) => {},
- [router],
- );
-
- const fetchEquipmentDetailsByEquipmentId = useCallback(async (equipmentId: string | number) => {
- setLoadingDetailsMap(prev => new Map(prev).set(equipmentId, true));
- try {
- const response = await axiosInstance.get<{
- records: EquipmentResult[];
- total: number;
- }>(`${NEXT_PUBLIC_API_URL}/EquipmentDetail/byEquipmentId/${equipmentId}`);
-
- if (response.status === 200) {
- setEquipmentDetailsMap(prev => new Map(prev).set(equipmentId, response.data.records || []));
- }
- } catch (error) {
- console.error("Error fetching equipment details:", error);
- setEquipmentDetailsMap(prev => new Map(prev).set(equipmentId, []));
- } finally {
- setLoadingDetailsMap(prev => new Map(prev).set(equipmentId, false));
- }
- }, []);
-
- const handleDeleteClick = useCallback((detailId: string | number, equipmentId: string | number) => {
- setItemToDelete({ id: detailId, equipmentId });
- setDeleteDialogOpen(true);
- }, []);
-
- const handleDeleteConfirm = useCallback(async () => {
- if (!itemToDelete) return;
-
- setDeleting(true);
- try {
- const response = await axiosInstance.delete(
- `${NEXT_PUBLIC_API_URL}/EquipmentDetail/delete/${itemToDelete.id}`
- );
-
- if (response.status === 200 || response.status === 204) {
- setEquipmentDetailsMap(prev => {
- const newMap = new Map(prev);
- const currentDetails = newMap.get(itemToDelete.equipmentId) || [];
- const updatedDetails = currentDetails.filter(detail => detail.id !== itemToDelete.id);
- newMap.set(itemToDelete.equipmentId, updatedDetails);
- return newMap;
- });
- }
- } catch (error) {
- console.error("Error deleting equipment detail:", error);
- alert("刪除失敗,請稍後再試");
- } finally {
- setDeleting(false);
- setDeleteDialogOpen(false);
- setItemToDelete(null);
- }
- }, [itemToDelete]);
-
- const handleDeleteCancel = useCallback(() => {
- setDeleteDialogOpen(false);
- setItemToDelete(null);
- }, []);
-
- const fetchEquipmentList = useCallback(async () => {
- setLoadingEquipments(true);
- try {
- const response = await axiosInstance.get<{
- records: EquipmentResult[];
- total: number;
- }>(`${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`, {
- params: {
- pageNum: 1,
- pageSize: 1000,
- },
- });
-
- if (response.status === 200) {
- setEquipmentList(response.data.records || []);
- }
- } catch (error) {
- console.error("Error fetching equipment list:", error);
- setEquipmentList([]);
- } finally {
- setLoadingEquipments(false);
- }
- }, []);
-
- const handleAddClick = useCallback(() => {
- setAddDialogOpen(true);
- fetchEquipmentList();
- }, [fetchEquipmentList]);
-
- const handleAddDialogClose = useCallback(() => {
- setAddDialogOpen(false);
- setSelectedDescription("");
- setSelectedName("");
- setSelectedEquipmentCode("");
- setEquipmentCodePrefix("");
- setEquipmentCodeNumber("");
- setIsExistingCombination(false);
- }, []);
-
- const availableDescriptions = useMemo(() => {
- const descriptions = equipmentList
- .map((eq) => eq.description)
- .filter((desc): desc is string => Boolean(desc));
- return Array.from(new Set(descriptions));
- }, [equipmentList]);
-
- const availableNames = useMemo(() => {
- const names = equipmentList
- .map((eq) => eq.name)
- .filter((name): name is string => Boolean(name));
- return Array.from(new Set(names));
- }, [equipmentList]);
-
- useEffect(() => {
- const checkAndGenerateEquipmentCode = async () => {
- if (!selectedDescription || !selectedName) {
- setIsExistingCombination(false);
- setSelectedEquipmentCode("");
- return;
- }
-
- const equipmentCode = `${selectedDescription}-${selectedName}`;
- const existingEquipment = equipmentList.find((eq) => eq.code === equipmentCode);
-
- if (existingEquipment) {
- setIsExistingCombination(true);
-
- try {
- const existingDetailsResponse = await axiosInstance.get<{
- records: EquipmentResult[];
- total: number;
- }>(`${NEXT_PUBLIC_API_URL}/EquipmentDetail/byDescriptionIncludingDeleted/${encodeURIComponent(equipmentCode)}`);
-
- let newEquipmentCode = "";
-
- if (existingDetailsResponse.data.records && existingDetailsResponse.data.records.length > 0) {
- const equipmentCodePatterns = existingDetailsResponse.data.records
- .map((detail) => {
- if (!detail.equipmentCode) return null;
- const match = detail.equipmentCode.match(/^([A-Za-z]+)(\d+)$/);
- if (match) {
- const originalNumber = match[2];
- return {
- prefix: match[1],
- number: parseInt(match[2], 10),
- paddingLength: originalNumber.length
- };
- }
- return null;
- })
- .filter((pattern): pattern is { prefix: string; number: number; paddingLength: number } => pattern !== null);
-
- if (equipmentCodePatterns.length > 0) {
- const prefix = equipmentCodePatterns[0].prefix;
- const maxEquipmentCodeNumber = Math.max(...equipmentCodePatterns.map(p => p.number));
- const maxPaddingLength = Math.max(...equipmentCodePatterns.map(p => p.paddingLength));
- const nextNumber = maxEquipmentCodeNumber + 1;
- newEquipmentCode = `${prefix}${String(nextNumber).padStart(maxPaddingLength, '0')}`;
- } else {
- newEquipmentCode = `LSS${String(1).padStart(2, '0')}`;
- }
- } else {
- newEquipmentCode = `LSS${String(1).padStart(2, '0')}`;
- }
-
- setSelectedEquipmentCode(newEquipmentCode);
- } catch (error) {
- console.error("Error checking existing equipment details:", error);
- setIsExistingCombination(false);
- setSelectedEquipmentCode("");
- }
- } else {
- setIsExistingCombination(false);
- setSelectedEquipmentCode("");
- setEquipmentCodePrefix("");
- setEquipmentCodeNumber("");
- }
- };
-
- checkAndGenerateEquipmentCode();
- }, [selectedDescription, selectedName, equipmentList]);
-
- useEffect(() => {
- const generateNumberForPrefix = async () => {
- if (isExistingCombination || !equipmentCodePrefix) {
- return;
- }
-
- if (equipmentCodePrefix.length !== 3 || !/^[A-Z]{3}$/.test(equipmentCodePrefix)) {
- setEquipmentCodeNumber("");
- setSelectedEquipmentCode(equipmentCodePrefix);
- return;
- }
-
- try {
- const response = await axiosInstance.get<{
- records: EquipmentResult[];
- total: number;
- }>(`${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`, {
- params: {
- pageNum: 1,
- pageSize: 1000,
- },
- });
-
- let maxNumber = 0;
- let maxPaddingLength = 2;
-
- if (response.data.records && response.data.records.length > 0) {
- const matchingCodes = response.data.records
- .map((detail) => {
- if (!detail.equipmentCode) return null;
- const match = detail.equipmentCode.match(new RegExp(`^${equipmentCodePrefix}(\\d+)$`));
- if (match) {
- const numberStr = match[1];
- return {
- number: parseInt(numberStr, 10),
- paddingLength: numberStr.length
- };
- }
- return null;
- })
- .filter((item): item is { number: number; paddingLength: number } => item !== null);
-
- if (matchingCodes.length > 0) {
- maxNumber = Math.max(...matchingCodes.map(c => c.number));
- maxPaddingLength = Math.max(...matchingCodes.map(c => c.paddingLength));
- }
- }
-
- const nextNumber = maxNumber + 1;
- const numberStr = String(nextNumber).padStart(maxPaddingLength, '0');
- setEquipmentCodeNumber(numberStr);
- setSelectedEquipmentCode(`${equipmentCodePrefix}${numberStr}`);
- } catch (error) {
- console.error("Error generating equipment code number:", error);
- setEquipmentCodeNumber("");
- setSelectedEquipmentCode(equipmentCodePrefix);
- }
- };
-
- generateNumberForPrefix();
- }, [equipmentCodePrefix, isExistingCombination]);
-
- const handleToggleExpand = useCallback(
- (id: string | number, code: string) => {
- setExpandedRows(prev => {
- const newSet = new Set(prev);
- if (newSet.has(id)) {
- newSet.delete(id);
- } else {
- newSet.add(id);
- if (!equipmentDetailsMap.has(id)) {
- fetchEquipmentDetailsByEquipmentId(id);
- }
- }
- return newSet;
- });
- },
- [equipmentDetailsMap, fetchEquipmentDetailsByEquipmentId]
- );
-
- const generalDataColumns = useMemo<Column<EquipmentResult>[]>(
- () => [
- {
- name: "code",
- label: "設備編號",
- renderCell: (item) => (
- <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
- <IconButton
- size="small"
- onClick={(e) => {
- e.stopPropagation();
- handleToggleExpand(item.id, item.code);
- }}
- sx={{ padding: 0.5 }}
- >
- {expandedRows.has(item.id) ? (
- <KeyboardArrowUpIcon fontSize="small" />
- ) : (
- <KeyboardArrowDownIcon fontSize="small" />
- )}
- </IconButton>
- <Typography>{item.code}</Typography>
- </Box>
- ),
- },
- ],
- [t, handleToggleExpand, expandedRows],
- );
-
- 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[];
- total: number;
- }
-
- const refetchData = useCallback(
- async (filterObj: SearchQuery) => {
- 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 };
-
- 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,
- ...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>>(
- endpoint,
- { params },
- );
- 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 || 0);
- } else {
- throw "400";
- }
- } catch (error) {
- console.error("Error fetching equipment types:", error);
- setFilteredEquipments([]);
- setTotalCount(0);
- } finally {
- setIsLoading(false);
- }
- },
- [pagingController.pageNum, pagingController.pageSize, tabIndex],
- );
-
- useEffect(() => {
- if (isReady) {
- refetchData(filterObj);
- }
- }, [filterObj, pagingController.pageNum, pagingController.pageSize, tabIndex, isReady, refetchData]);
-
- const onReset = useCallback(() => {
- setFilterObjByTab(prev => ({
- ...prev,
- [tabIndex]: {},
- }));
- setPagingControllerByTab(prev => ({
- ...prev,
- [tabIndex]: {
- pageNum: 1,
- pageSize: prev[tabIndex]?.pageSize || 10,
- },
- }));
- }, [tabIndex]);
-
- const handleSaveEquipmentDetail = useCallback(async () => {
- if (!selectedName || !selectedDescription) {
- return;
- }
-
- if (!isExistingCombination) {
- if (equipmentCodePrefix.length !== 3 || !/^[A-Z]{3}$/.test(equipmentCodePrefix)) {
- alert("請輸入3個大寫英文字母作為設備編號前綴");
- return;
- }
- if (!equipmentCodeNumber) {
- alert("設備編號生成中,請稍候");
- return;
- }
- }
-
- setSaving(true);
- try {
-
- const equipmentCode = `${selectedDescription}-${selectedName}`;
-
- let equipment = equipmentList.find((eq) => eq.code === equipmentCode);
- let equipmentId: string | number;
-
- if (!equipment) {
- const equipmentResponse = await axiosInstance.post<EquipmentResult>(
- `${NEXT_PUBLIC_API_URL}/Equipment/save`,
- {
- code: equipmentCode,
- name: selectedName,
- description: selectedDescription,
- id: null,
- }
- );
- equipment = equipmentResponse.data;
- equipmentId = equipment.id;
- } else {
- equipmentId = equipment.id;
- }
-
- const existingDetailsResponse = await axiosInstance.get<{
- records: EquipmentResult[];
- total: number;
- }>(`${NEXT_PUBLIC_API_URL}/EquipmentDetail/byDescriptionIncludingDeleted/${encodeURIComponent(equipmentCode)}`);
-
- let newName = "1號";
- let newEquipmentCode = "";
-
- if (existingDetailsResponse.data.records && existingDetailsResponse.data.records.length > 0) {
- const numbers = existingDetailsResponse.data.records
- .map((detail) => {
- const match = detail.name?.match(/(\d+)號/);
- return match ? parseInt(match[1], 10) : 0;
- })
- .filter((num) => num > 0);
-
- if (numbers.length > 0) {
- const maxNumber = Math.max(...numbers);
- newName = `${maxNumber + 1}號`;
- }
-
- if (isExistingCombination) {
- const equipmentCodePatterns = existingDetailsResponse.data.records
- .map((detail) => {
- if (!detail.equipmentCode) return null;
- const match = detail.equipmentCode.match(/^([A-Za-z]+)(\d+)$/);
- if (match) {
- const originalNumber = match[2];
- return {
- prefix: match[1],
- number: parseInt(match[2], 10),
- paddingLength: originalNumber.length
- };
- }
- return null;
- })
- .filter((pattern): pattern is { prefix: string; number: number; paddingLength: number } => pattern !== null);
-
- if (equipmentCodePatterns.length > 0) {
- const prefix = equipmentCodePatterns[0].prefix;
- const maxEquipmentCodeNumber = Math.max(...equipmentCodePatterns.map(p => p.number));
- const maxPaddingLength = Math.max(...equipmentCodePatterns.map(p => p.paddingLength));
- const nextNumber = maxEquipmentCodeNumber + 1;
- newEquipmentCode = `${prefix}${String(nextNumber).padStart(maxPaddingLength, '0')}`;
- } else {
- newEquipmentCode = `LSS${String(1).padStart(2, '0')}`;
- }
- } else {
- if (isExistingCombination) {
- newEquipmentCode = selectedEquipmentCode;
- } else {
- newEquipmentCode = `${equipmentCodePrefix}${equipmentCodeNumber}`;
- }
- }
- } else {
- if (isExistingCombination) {
- newEquipmentCode = `LSS${String(1).padStart(2, '0')}`;
- } else {
- newEquipmentCode = `${equipmentCodePrefix}${equipmentCodeNumber}`;
- }
- }
-
- const detailCode = `${equipmentCode}-${newName}`;
-
- await axiosInstance.post<EquipmentResult>(
- `${NEXT_PUBLIC_API_URL}/EquipmentDetail/save`,
- {
- code: detailCode,
- name: newName,
- description: equipmentCode,
- equipmentCode: newEquipmentCode,
- id: null,
- equipmentTypeId: equipmentId,
- repairAndMaintenanceStatus: false,
- }
- );
-
- handleAddDialogClose();
-
- if (tabIndex === 0) {
- await refetchData(filterObj);
-
- if (equipmentDetailsMap.has(equipmentId)) {
- await fetchEquipmentDetailsByEquipmentId(equipmentId);
- }
- }
-
- alert("新增成功");
- } catch (error: any) {
- console.error("Error saving equipment detail:", error);
- const errorMessage = error.response?.data?.message || error.message || "保存失敗,請稍後再試";
- alert(errorMessage);
- } finally {
- setSaving(false);
- }
- }, [selectedName, selectedDescription, selectedEquipmentCode, equipmentCodePrefix, equipmentCodeNumber, isExistingCombination, equipmentList, refetchData, filterObj, handleAddDialogClose, tabIndex, equipmentDetailsMap, fetchEquipmentDetailsByEquipmentId]);
-
- const renderExpandedRow = useCallback((item: EquipmentResult): React.ReactNode => {
- if (tabIndex !== 0) {
- return null;
- }
-
- const details = equipmentDetailsMap.get(item.id) || [];
- const isLoading = loadingDetailsMap.get(item.id) || false;
-
- return (
- <TableRow key={`expanded-${item.id}`}>
- <TableCell colSpan={columns.length} sx={{ py: 0, border: 0 }}>
- <Collapse in={expandedRows.has(item.id)} timeout="auto" unmountOnExit>
- <Box sx={{ margin: 2 }}>
- {isLoading ? (
- <Box sx={{ display: "flex", alignItems: "center", gap: 2, p: 2 }}>
- <CircularProgress size={20} />
- <Typography>載入中...</Typography>
- </Box>
- ) : details.length === 0 ? (
- <Typography sx={{ p: 2 }}>無相關設備詳細資料</Typography>
- ) : (
- <Box>
- <Typography variant="subtitle2" sx={{ mb: 2, fontWeight: "bold" }}>
- 設備詳細資料 (設備編號: {item.code})
- </Typography>
- <Grid container spacing={2}>
- {details.map((detail) => (
- <Grid item xs={6} key={detail.id}>
- <Box
- sx={{
- p: 2,
- border: "1px solid",
- borderColor: "divider",
- borderRadius: 1,
- height: "100%",
- position: "relative",
- }}
- >
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 1 }}>
- <Typography variant="body2" sx={{ fontWeight: 500 }}>
- 編號: {detail.code || "-"}
- </Typography>
- <IconButton
- size="small"
- color="error"
- onClick={() => handleDeleteClick(detail.id, item.id)}
- sx={{ ml: 1 }}
- >
- <DeleteIcon fontSize="small" />
- </IconButton>
- </Box>
- {detail.name && (
- <Typography variant="caption" color="text.secondary" sx={{ display: "block" }}>
- 名稱: {detail.name}
- </Typography>
- )}
- {detail.description && (
- <Typography variant="caption" color="text.secondary" sx={{ display: "block" }}>
- 描述: {detail.description}
- </Typography>
- )}
- {detail.equipmentCode && (
- <Typography variant="caption" color="text.secondary" sx={{ display: "block" }}>
- 設備編號: {detail.equipmentCode}
- </Typography>
- )}
- </Box>
- </Grid>
- ))}
- </Grid>
- </Box>
- )}
- </Box>
- </Collapse>
- </TableCell>
- </TableRow>
- );
- }, [columns.length, equipmentDetailsMap, loadingDetailsMap, expandedRows, tabIndex, handleDeleteClick]);
-
- return (
- <>
- <SearchBox
- criteria={searchCriteria}
- onSearch={(query) => {
- setFilterObjByTab(prev => {
- const newState = { ...prev };
- newState[tabIndex] = query as unknown as SearchQuery;
- return newState;
- });
- }}
- onReset={onReset}
- />
- {tabIndex === 0 && (
- <Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2 }}>
- <Typography variant="h6" component="h2">
- 設備編號
- </Typography>
- <Button
- variant="contained"
- startIcon={<AddIcon />}
- onClick={handleAddClick}
- color="primary"
- >
- 新增
- </Button>
- </Box>
- )}
- <Box sx={{
- "& .MuiTableContainer-root": {
- overflowY: "auto",
- "&::-webkit-scrollbar": {
- width: "17px"
- }
- }
- }}>
- <EquipmentSearchResults<EquipmentResult>
- items={filteredEquipments}
- columns={columns}
- setPagingController={(newController) => {
- setPagingControllerByTab(prev => {
- const newState = { ...prev };
- newState[tabIndex] = typeof newController === 'function'
- ? newController(prev[tabIndex] || { pageNum: 1, pageSize: 10 })
- : newController;
- return newState;
- });
- }}
- pagingController={pagingController}
- totalCount={totalCount}
- isAutoPaging={false}
- renderExpandedRow={renderExpandedRow}
- hideHeader={tabIndex === 0}
- />
- </Box>
-
- {/* Delete Confirmation Dialog */}
- {deleteDialogOpen && (
- <Dialog
- open={deleteDialogOpen}
- onClose={handleDeleteCancel}
- aria-labelledby="delete-dialog-title"
- aria-describedby="delete-dialog-description"
- >
- <DialogTitle id="delete-dialog-title">
- 確認刪除
- </DialogTitle>
- <DialogContent>
- <DialogContentText id="delete-dialog-description">
- 您確定要刪除此設備詳細資料嗎?此操作無法復原。
- </DialogContentText>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleDeleteCancel} disabled={deleting}>
- 取消
- </Button>
- <Button onClick={handleDeleteConfirm} color="error" disabled={deleting} autoFocus>
- {deleting ? "刪除中..." : "刪除"}
- </Button>
- </DialogActions>
- </Dialog>
- )}
-
- {/* Add Equipment Detail Dialog */}
- <Dialog
- open={addDialogOpen}
- onClose={handleAddDialogClose}
- aria-labelledby="add-dialog-title"
- maxWidth="sm"
- fullWidth
- >
- <DialogTitle id="add-dialog-title">
- 新增設備詳細資料
- </DialogTitle>
- <DialogContent>
- <Box sx={{ pt: 2 }}>
- <Autocomplete
- freeSolo
- options={availableDescriptions}
- value={selectedDescription || null}
- onChange={(event, newValue) => {
- setSelectedDescription(newValue || '');
- }}
- onInputChange={(event, newInputValue) => {
- setSelectedDescription(newInputValue);
- }}
- loading={loadingEquipments}
- disabled={loadingEquipments || saving}
- renderInput={(params) => (
- <TextField
- {...params}
- label="種類"
- placeholder="選擇或輸入種類"
- />
- )}
- sx={{ mb: 2 }}
- />
- <Autocomplete
- freeSolo
- options={availableNames}
- value={selectedName || null}
- onChange={(event, newValue) => {
- setSelectedName(newValue || '');
- }}
- onInputChange={(event, newInputValue) => {
- setSelectedName(newInputValue);
- }}
- loading={loadingEquipments}
- disabled={loadingEquipments || saving}
- componentsProps={{
- popper: {
- placement: 'bottom-start',
- modifiers: [
- {
- name: 'flip',
- enabled: false,
- },
- {
- name: 'preventOverflow',
- enabled: true,
- },
- ],
- },
- }}
- renderInput={(params) => (
- <TextField
- {...params}
- label="名稱"
- placeholder="選擇或輸入名稱"
- />
- )}
- />
- <Box sx={{ mt: 2 }}>
- <TextField
- fullWidth
- label="設備編號"
- value={isExistingCombination ? selectedEquipmentCode : equipmentCodePrefix}
- onChange={(e) => {
- if (!isExistingCombination) {
- const input = e.target.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 3);
- setEquipmentCodePrefix(input);
- }
- }}
- disabled={isExistingCombination || loadingEquipments || saving}
- placeholder={isExistingCombination ? "自動生成" : "輸入3個大寫英文字母"}
- required={!isExistingCombination}
- InputProps={{
- endAdornment: !isExistingCombination && equipmentCodeNumber ? (
- <InputAdornment position="end">
- <Typography
- sx={{
- color: 'text.secondary',
- fontSize: '1rem',
- fontWeight: 500,
- minWidth: '30px',
- textAlign: 'right',
- }}
- >
- {equipmentCodeNumber}
- </Typography>
- </InputAdornment>
- ) : null,
- }}
- helperText={!isExistingCombination && equipmentCodePrefix.length > 0 && equipmentCodePrefix.length !== 3
- ? "必須輸入3個大寫英文字母"
- : !isExistingCombination && equipmentCodePrefix.length === 3 && !/^[A-Z]{3}$/.test(equipmentCodePrefix)
- ? "必須是大寫英文字母"
- : ""}
- error={!isExistingCombination && equipmentCodePrefix.length > 0 && (equipmentCodePrefix.length !== 3 || !/^[A-Z]{3}$/.test(equipmentCodePrefix))}
- />
- </Box>
- </Box>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleAddDialogClose} disabled={saving}>
- 取消
- </Button>
- <Button
- onClick={handleSaveEquipmentDetail}
- variant="contained"
- disabled={!selectedName || !selectedDescription || (!isExistingCombination && !selectedEquipmentCode) || loadingEquipments || saving}
- >
- {saving ? "保存中..." : "新增"}
- </Button>
- </DialogActions>
- </Dialog>
- </>
- );
- };
-
- export default EquipmentSearch;
|