| @@ -199,6 +199,12 @@ export const getApproverStockTakeRecords = async () => { | |||||
| ); | ); | ||||
| return stockTakeRecords; | return stockTakeRecords; | ||||
| } | } | ||||
| export const getLatestApproverStockTakeHeader = async () => { | |||||
| return serverFetchJson<AllPickedStockTakeListReponse>( | |||||
| `${BASE_API_URL}/stockTakeRecord/LatestApproverStockTakeHeader`, | |||||
| { method: "GET" } | |||||
| ); | |||||
| } | |||||
| export const createStockTakeForSections = async () => { | export const createStockTakeForSections = async () => { | ||||
| const createStockTakeForSections = await serverFetchJson<Map<string, string>>( | const createStockTakeForSections = await serverFetchJson<Map<string, string>>( | ||||
| `${BASE_API_URL}/stockTake/createForSections`, | `${BASE_API_URL}/stockTake/createForSections`, | ||||
| @@ -64,7 +64,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| const [batchSaving, setBatchSaving] = useState(false); | const [batchSaving, setBatchSaving] = useState(false); | ||||
| const [updatingStatus, setUpdatingStatus] = useState(false); | const [updatingStatus, setUpdatingStatus] = useState(false); | ||||
| const [page, setPage] = useState(0); | const [page, setPage] = useState(0); | ||||
| const [pageSize, setPageSize] = useState<number | string>("all"); | |||||
| const [pageSize, setPageSize] = useState<number | string>(50); | |||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| @@ -337,7 +337,19 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| }, | }, | ||||
| [t, onSnackbar] | [t, onSnackbar] | ||||
| ); | ); | ||||
| const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||||
| // 禁止小数点、逗号、科学计数、正负号 | |||||
| if ([".", ",", "e", "E", "+", "-"].includes(e.key)) { | |||||
| e.preventDefault(); | |||||
| } | |||||
| }; | |||||
| const sanitizeIntegerInput = (value: string) => { | |||||
| // 只保留数字 | |||||
| return value.replace(/[^\d]/g, ""); | |||||
| }; | |||||
| const isIntegerString = (value: string) => /^\d+$/.test(value); | |||||
| const handleBatchSubmitAll = useCallback(async () => { | const handleBatchSubmitAll = useCallback(async () => { | ||||
| if (mode === "approved") return; | if (mode === "approved") return; | ||||
| if (!selectedSession || !currentUserId) { | if (!selectedSession || !currentUserId) { | ||||
| @@ -431,9 +443,16 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={variancePercentTolerance} | value={variancePercentTolerance} | ||||
| onChange={(e) => setVariancePercentTolerance(e.target.value)} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| setVariancePercentTolerance(clean); | |||||
| }} | |||||
| label={t("Variance %")} | label={t("Variance %")} | ||||
| sx={{ width: 100 }} | sx={{ width: 100 }} | ||||
| inputProps={{ min: 0, max: 100, step: 0.1 }} | inputProps={{ min: 0, max: 100, step: 0.1 }} | ||||
| /> | /> | ||||
| {mode === "pending" && ( | {mode === "pending" && ( | ||||
| @@ -564,7 +583,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| <Radio | <Radio | ||||
| size="small" | size="small" | ||||
| checked={selection === "first"} | checked={selection === "first"} | ||||
| disabled={mode === "approved"} | |||||
| onChange={() => | onChange={() => | ||||
| setQtySelection({ | setQtySelection({ | ||||
| ...qtySelection, | ...qtySelection, | ||||
| [detail.id]: "first", | [detail.id]: "first", | ||||
| @@ -592,6 +613,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| <Radio | <Radio | ||||
| size="small" | size="small" | ||||
| checked={selection === "second"} | checked={selection === "second"} | ||||
| disabled={mode === "approved"} | |||||
| onChange={() => | onChange={() => | ||||
| setQtySelection({ | setQtySelection({ | ||||
| ...qtySelection, | ...qtySelection, | ||||
| @@ -620,6 +642,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| <Radio | <Radio | ||||
| size="small" | size="small" | ||||
| checked={selection === "approver"} | checked={selection === "approver"} | ||||
| disabled={mode === "approved"} | |||||
| onChange={() => | onChange={() => | ||||
| setQtySelection({ | setQtySelection({ | ||||
| ...qtySelection, | ...qtySelection, | ||||
| @@ -634,12 +657,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={approverQty[detail.id] || ""} | value={approverQty[detail.id] || ""} | ||||
| onChange={(e) => | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| setApproverQty({ | setApproverQty({ | ||||
| ...approverQty, | ...approverQty, | ||||
| [detail.id]: e.target.value, | |||||
| }) | |||||
| } | |||||
| [detail.id]: clean, | |||||
| }); | |||||
| }} | |||||
| sx={{ | sx={{ | ||||
| width: 130, | width: 130, | ||||
| minWidth: 130, | minWidth: 130, | ||||
| @@ -650,18 +675,21 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| }} | }} | ||||
| placeholder={t("Stock Take Qty")} | placeholder={t("Stock Take Qty")} | ||||
| disabled={selection !== "approver"} | disabled={selection !== "approver"} | ||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| /> | /> | ||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={approverBadQty[detail.id] || ""} | value={approverBadQty[detail.id] || ""} | ||||
| onChange={(e) => | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| setApproverBadQty({ | setApproverBadQty({ | ||||
| ...approverBadQty, | ...approverBadQty, | ||||
| [detail.id]: e.target.value, | |||||
| }) | |||||
| } | |||||
| [detail.id]: clean, | |||||
| }); | |||||
| }} | |||||
| sx={{ | sx={{ | ||||
| width: 130, | width: 130, | ||||
| minWidth: 130, | minWidth: 130, | ||||
| @@ -672,6 +700,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| }} | }} | ||||
| placeholder={t("Bad Qty")} | placeholder={t("Bad Qty")} | ||||
| disabled={selection !== "approver"} | disabled={selection !== "approver"} | ||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| /> | /> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| ={" "} | ={" "} | ||||
| @@ -70,7 +70,17 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| const handleChangePage = useCallback((event: unknown, newPage: number) => { | const handleChangePage = useCallback((event: unknown, newPage: number) => { | ||||
| setPage(newPage); | setPage(newPage); | ||||
| }, []); | }, []); | ||||
| const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||||
| // 禁止小数点、逗号、科学计数、正负号 | |||||
| if ([".", ",", "e", "E", "+", "-"].includes(e.key)) { | |||||
| e.preventDefault(); | |||||
| } | |||||
| }; | |||||
| const sanitizeIntegerInput = (value: string) => { | |||||
| // 只保留数字 | |||||
| return value.replace(/[^\d]/g, ""); | |||||
| }; | |||||
| const isIntegerString = (value: string) => /^\d+$/.test(value); | |||||
| const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | ||||
| const newSize = parseInt(event.target.value, 10); | const newSize = parseInt(event.target.value, 10); | ||||
| if (newSize === -1) { | if (newSize === -1) { | ||||
| @@ -437,9 +447,11 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={inputs.firstQty} | value={inputs.firstQty} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ | setRecordInputs(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| @@ -460,9 +472,11 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={inputs.firstBadQty} | value={inputs.firstBadQty} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ | setRecordInputs(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| @@ -500,13 +514,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={inputs.secondQty} | value={inputs.secondQty} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ | setRecordInputs(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: val } | |||||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: clean } | |||||
| })); | })); | ||||
| }} | }} | ||||
| sx={{ | sx={{ | ||||
| @@ -523,13 +539,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={inputs.secondBadQty} | value={inputs.secondBadQty} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ | setRecordInputs(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: val } | |||||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: clean } | |||||
| })); | })); | ||||
| }} | }} | ||||
| sx={{ | sx={{ | ||||
| @@ -581,10 +599,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| value={inputs.remark} | value={inputs.remark} | ||||
| onChange={(e) => setRecordInputs(prev => ({ | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| inputProps={{ inputMode: "text", pattern: "[0-9]*" }} | |||||
| onChange={(e) => { | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| setRecordInputs(prev => ({ | |||||
| ...prev, | ...prev, | ||||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value } | |||||
| }))} | |||||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: clean } | |||||
| })); | |||||
| }} | |||||
| sx={{ width: 150 }} | sx={{ width: 150 }} | ||||
| /> | /> | ||||
| </> | </> | ||||
| @@ -394,7 +394,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| window.removeEventListener("keydown", handleKeyPress); | window.removeEventListener("keydown", handleKeyPress); | ||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||||
| // 禁止小数点、逗号、科学计数、正负号 | |||||
| if ([".", ",", "e", "E", "+", "-"].includes(e.key)) { | |||||
| e.preventDefault(); | |||||
| } | |||||
| }; | |||||
| const sanitizeIntegerInput = (value: string) => { | |||||
| // 只保留数字 | |||||
| return value.replace(/[^\d]/g, ""); | |||||
| }; | |||||
| const isIntegerString = (value: string) => /^\d+$/.test(value); | |||||
| const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | ||||
| if (selectedSession?.status?.toLowerCase() === "completed") { | if (selectedSession?.status?.toLowerCase() === "completed") { | ||||
| return true; | return true; | ||||
| @@ -580,9 +592,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={recordInputs[detail.id]?.firstQty || ""} | value={recordInputs[detail.id]?.firstQty || ""} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } })); | setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } })); | ||||
| }} | }} | ||||
| @@ -604,9 +618,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={recordInputs[detail.id]?.firstBadQty || ""} | value={recordInputs[detail.id]?.firstBadQty || ""} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } })); | setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } })); | ||||
| }} | }} | ||||
| @@ -656,9 +672,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={recordInputs[detail.id]?.secondQty || ""} | value={recordInputs[detail.id]?.secondQty || ""} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } })); | setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } })); | ||||
| }} | }} | ||||
| @@ -676,9 +694,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={recordInputs[detail.id]?.secondBadQty || ""} | value={recordInputs[detail.id]?.secondBadQty || ""} | ||||
| inputProps={{ min: 0, step: "any" }} | |||||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value; | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| const val = clean; | |||||
| if (val.includes("-")) return; | if (val.includes("-")) return; | ||||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } })); | setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } })); | ||||
| }} | }} | ||||
| @@ -751,13 +771,18 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| value={recordInputs[detail.id]?.remark || ""} | value={recordInputs[detail.id]?.remark || ""} | ||||
| onChange={(e) => setRecordInputs(prev => ({ | |||||
| ...prev, | |||||
| [detail.id]: { | |||||
| ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), | |||||
| remark: e.target.value | |||||
| } | |||||
| }))} | |||||
| onKeyDown={blockNonIntegerKeys} | |||||
| inputProps={{ inputMode: "text", pattern: "[0-9]*" }} | |||||
| onChange={(e) => { | |||||
| const clean = sanitizeIntegerInput(e.target.value); | |||||
| setRecordInputs(prev => ({ | |||||
| ...prev, | |||||
| [detail.id]: { | |||||
| ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), | |||||
| remark: clean | |||||
| } | |||||
| })); | |||||
| }} | |||||
| sx={{ width: 150 }} | sx={{ width: 150 }} | ||||
| /> | /> | ||||
| </> | </> | ||||
| @@ -3,7 +3,7 @@ | |||||
| import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography } from "@mui/material"; | import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography } from "@mui/material"; | ||||
| import { useState, useCallback, useEffect } from "react"; | import { useState, useCallback, useEffect } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { AllPickedStockTakeListReponse, getApproverStockTakeRecords } from "@/app/api/stockTake/actions"; | |||||
| import { AllPickedStockTakeListReponse, getLatestApproverStockTakeHeader } from "@/app/api/stockTake/actions"; | |||||
| import PickerCardList from "./PickerCardList"; | import PickerCardList from "./PickerCardList"; | ||||
| import PickerStockTake from "./PickerStockTake"; | import PickerStockTake from "./PickerStockTake"; | ||||
| import PickerReStockTake from "./PickerReStockTake"; | import PickerReStockTake from "./PickerReStockTake"; | ||||
| @@ -54,12 +54,11 @@ const StockTakeTab: React.FC = () => { | |||||
| }, []); | }, []); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (tabValue !== 1) return; | |||||
| if (tabValue !== 1 && tabValue !== 2) return; | |||||
| setApproverLoading(true); | setApproverLoading(true); | ||||
| getApproverStockTakeRecords() | |||||
| .then((records) => { | |||||
| const list = Array.isArray(records) ? records : []; | |||||
| setApproverSession(list[0] ?? null); | |||||
| getLatestApproverStockTakeHeader() | |||||
| .then((header) => { | |||||
| setApproverSession(header ?? null); | |||||
| }) | }) | ||||
| .catch((e) => { | .catch((e) => { | ||||
| console.error(e); | console.error(e); | ||||
| @@ -150,11 +149,7 @@ const StockTakeTab: React.FC = () => { | |||||
| )} | )} | ||||
| {tabValue === 2 && ( | {tabValue === 2 && ( | ||||
| <Box> | <Box> | ||||
| {approverLoading ? ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : approverSession ? ( | |||||
| {approverSession ? ( | |||||
| <ApproverStockTakeAll | <ApproverStockTakeAll | ||||
| selectedSession={approverSession} | selectedSession={approverSession} | ||||
| mode="approved" | mode="approved" | ||||
| @@ -6,6 +6,8 @@ | |||||
| "Status": "來貨狀態", | "Status": "來貨狀態", | ||||
| "Qty": "盤點數量", | "Qty": "盤點數量", | ||||
| "UoM": "單位", | "UoM": "單位", | ||||
| "Approver Pending": "審核待處理", | |||||
| "Approver Approved": "審核通過", | |||||
| "mat": "物料", | "mat": "物料", | ||||
| "variance": "差異", | "variance": "差異", | ||||
| "Plan Start Date": "計劃開始日期", | "Plan Start Date": "計劃開始日期", | ||||