Browse Source

update

MergeProblem1
CANCERYS\kw093 21 hours ago
parent
commit
10dbc666f2
6 changed files with 982 additions and 113 deletions
  1. +12
    -0
      src/app/api/bom/client.ts
  2. +52
    -0
      src/app/api/bom/index.ts
  3. +28
    -0
      src/app/api/stockTake/actions.ts
  4. +789
    -42
      src/components/ImportBom/ImportBomDetailTab.tsx
  5. +42
    -26
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  6. +59
    -45
      src/components/StockTakeManagement/StockTakeTab.tsx

+ 12
- 0
src/app/api/bom/client.ts View File

@@ -8,6 +8,7 @@ import type {
ImportBomItemPayload,
BomCombo,
BomDetailResponse,
EditBomRequest,
} from "./index";

export async function uploadBomFiles(
@@ -87,6 +88,17 @@ export async function fetchBomComboClient(): Promise<BomCombo[]> {
);
return response.data;
}

export async function editBomClient(
id: number,
request: EditBomRequest,
): Promise<BomDetailResponse> {
const response = await axiosInstance.put<BomDetailResponse>(
`${NEXT_PUBLIC_API_URL}/bom/${id}`,
request,
);
return response.data;
}
export type BomExcelCheckProgress = {
batchId: string;
totalFiles: number;


+ 52
- 0
src/app/api/bom/index.ts View File

@@ -61,6 +61,7 @@ export const fetchBomScores = cache(async () => {
export interface BomMaterialDto {
itemCode?: string;
itemName?: string;
isConsumable?: boolean;
baseQty?: number;
baseUom?: string;
stockQty?: number;
@@ -71,8 +72,10 @@ export interface BomMaterialDto {

export interface BomProcessDto {
seqNo?: number;
processCode?: string;
processName?: string;
processDescription?: string;
equipmentCode?: string;
equipmentName?: string;
durationInMinute?: number;
prepTimeInMinute?: number;
@@ -97,4 +100,53 @@ export interface BomDetailResponse {
outputQtyUom?: string;
materials: BomMaterialDto[];
processes: BomProcessDto[];
}

export interface EditBomRequest {
// basic fields
description?: string;
outputQty?: number;
outputQtyUom?: string;
yield?: number;

// baseScore inputs (server will recalculate)
isDark?: number;
isFloat?: number;
isDense?: number;
scrapRate?: number;
allergicSubstances?: number;
timeSequence?: number;
complexity?: number;
isDrink?: boolean;

materials?: EditBomMaterialRequest[];
processes?: EditBomProcessRequest[];
}

export interface EditBomMaterialRequest {
id?: number;
// At least one of itemId/itemCode should be present.
itemId?: number;
itemCode?: string;
qty: number;
isConsumable?: boolean;
}

export interface EditBomProcessRequest {
id?: number;
seqNo?: number;
processId?: number;
processCode?: string;
equipmentId?: number;
equipmentCode?: string;
newEquipment?: {
code: string;
name: string;
description?: string;
equipmentTypeId?: number;
};
description?: string;
durationInMinute?: number;
prepTimeInMinute?: number;
postProdTimeInMinute?: number;
}

+ 28
- 0
src/app/api/stockTake/actions.ts View File

@@ -122,6 +122,34 @@ export const getApproverInventoryLotDetailsAll = async (
);
return response;
}
export const getApproverInventoryLotDetailsAllPending = async (
stockTakeId?: number | null,
pageNum: number = 0,
pageSize: number = 2147483647
) => {
const params = new URLSearchParams();
params.append("pageNum", String(pageNum));
params.append("pageSize", String(pageSize));
if (stockTakeId != null && stockTakeId > 0) {
params.append("stockTakeId", String(stockTakeId));
}
const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllPending?${params.toString()}`;
return serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(url, { method: "GET" });
}
export const getApproverInventoryLotDetailsAllApproved = async (
stockTakeId?: number | null,
pageNum: number = 0,
pageSize: number = 50
) => {
const params = new URLSearchParams();
params.append("pageNum", String(pageNum));
params.append("pageSize", String(pageSize));
if (stockTakeId != null && stockTakeId > 0) {
params.append("stockTakeId", String(stockTakeId));
}
const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllApproved?${params.toString()}`;
return serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(url, { method: "GET" });
}

export const importStockTake = async (data: FormData) => {
const importStockTake = await serverFetchJson<string>(


+ 789
- 42
src/components/ImportBom/ImportBomDetailTab.tsx View File

@@ -16,13 +16,27 @@ import {
TableRow,
TableCell,
TableBody,
Button,
TextField,
Checkbox,
FormControlLabel,
IconButton,
} from "@mui/material";
import type { BomCombo, BomDetailResponse } from "@/app/api/bom";
import { fetchBomComboClient, fetchBomDetailClient } from "@/app/api/bom/client";
import {
editBomClient,
fetchBomComboClient,
fetchBomDetailClient,
} from "@/app/api/bom/client";
import type { SelectChangeEvent } from "@mui/material/Select";
import { useTranslation } from "react-i18next";
import SearchBox, { Criterion } from "../SearchBox";
import { useMemo, useCallback } from "react";
import AddIcon from "@mui/icons-material/Add";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Cancel";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
const ImportBomDetailTab: React.FC = () => {
const { t } = useTranslation( "common" );
const [bomList, setBomList] = useState<BomCombo[]>([]);
@@ -32,6 +46,89 @@ const ImportBomDetailTab: React.FC = () => {
const [loadingDetail, setLoadingDetail] = useState(false);
const [filteredBoms, setFilteredBoms] = useState<BomCombo[]>([])
const [currentBom, setCurrentBom] = useState<BomCombo | null>(null);

type EditMaterialRow = {
key: string;
id?: number;
itemCode?: string;
itemName?: string;
qty: number;
isConsumable: boolean;

baseUom?: string;
stockQty?: number;
stockUom?: string;
salesQty?: number;
salesUom?: string;
};

type EditProcessRow = {
key: string;
id?: number;
seqNo?: number;
processCode?: string;
processName?: string;
description: string;
equipmentCode?: string;
durationInMinute: number;
prepTimeInMinute: number;
postProdTimeInMinute: number;
};

const [isEditing, setIsEditing] = useState(false);
const [editLoading, setEditLoading] = useState(false);
const [editError, setEditError] = useState<string | null>(null);
const [editBasic, setEditBasic] = useState<{
description: string;
outputQty: number;
outputQtyUom: string;

isDark: number;
isFloat: number;
isDense: number;
scrapRate: number;
allergicSubstances: number;
timeSequence: number;
complexity: number;
isDrink: boolean;
} | null>(null);

const [editMaterials, setEditMaterials] = useState<EditMaterialRow[]>([]);
const [editProcesses, setEditProcesses] = useState<EditProcessRow[]>([]);

// Process add form (uses dropdown selections).
const [processAddForm, setProcessAddForm] = useState<{
processCode: string;
equipmentCode: string;
description: string;
durationInMinute: number;
prepTimeInMinute: number;
postProdTimeInMinute: number;
}>({
processCode: "",
equipmentCode: "",
description: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
});

const processCodeOptions = useMemo(() => {
const codes = new Set<string>();
(detail?.processes ?? []).forEach((p) => {
if (p.processCode) codes.add(p.processCode);
});
return Array.from(codes);
}, [detail]);

const equipmentCodeOptions = useMemo(() => {
const codes = new Set<string>();
(detail?.processes ?? []).forEach((p) => {
if (p.equipmentCode) codes.add(p.equipmentCode);
});
return Array.from(codes);
}, [detail]);

useEffect(() => {
const loadList = async () => {
setLoadingList(true);
@@ -143,6 +240,201 @@ const ImportBomDetailTab: React.FC = () => {
}
};

const genKey = () => Math.random().toString(36).slice(2);

const startEdit = useCallback(() => {
if (!detail) return;

setEditError(null);
setEditBasic({
description: detail.description ?? "",
outputQty: detail.outputQty ?? 0,
outputQtyUom: detail.outputQtyUom ?? "",

isDark: detail.isDark ?? 0,
isFloat: detail.isFloat ?? 0,
isDense: detail.isDense ?? 0,
scrapRate: detail.scrapRate ?? 0,
allergicSubstances: detail.allergicSubstances ?? 0,
timeSequence: detail.timeSequence ?? 0,
complexity: detail.complexity ?? 0,
isDrink: detail.isDrink ?? false,
});

setEditMaterials(
(detail.materials ?? []).map((m) => ({
key: genKey(),
id: undefined,
itemCode: m.itemCode ?? "",
itemName: m.itemName ?? "",
qty: m.baseQty ?? 0,
isConsumable: m.isConsumable ?? false,
baseUom: m.baseUom,
stockQty: m.stockQty,
stockUom: m.stockUom,
salesQty: m.salesQty,
salesUom: m.salesUom,
})),
);

setEditProcesses(
(detail.processes ?? []).map((p) => ({
key: genKey(),
id: undefined,
seqNo: p.seqNo,
processCode: p.processCode ?? "",
processName: p.processName,
description: p.processDescription ?? "",
equipmentCode: p.equipmentCode ?? p.equipmentName ?? "",
durationInMinute: p.durationInMinute ?? 0,
prepTimeInMinute: p.prepTimeInMinute ?? 0,
postProdTimeInMinute: p.postProdTimeInMinute ?? 0,
})),
);

setIsEditing(true);
}, [detail]);

const cancelEdit = useCallback(() => {
setIsEditing(false);
setEditLoading(false);
setEditError(null);
setEditBasic(null);
setEditMaterials([]);
setEditProcesses([]);
setProcessAddForm({
processCode: "",
equipmentCode: "",
description: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
});
}, []);

const addMaterialRow = useCallback(() => {
setEditMaterials((prev) => [
...prev,
{
key: genKey(),
itemCode: "",
itemName: "",
qty: 0,
isConsumable: false,
baseUom: "",
stockQty: undefined,
stockUom: "",
salesQty: undefined,
salesUom: "",
},
]);
}, []);

const addProcessRow = useCallback(() => {
setEditProcesses((prev) => [
...prev,
{
key: genKey(),
seqNo: undefined,
processCode: "",
processName: "",
description: "",
equipmentCode: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
},
]);
}, []);

const addProcessFromForm = useCallback(() => {
const pCode = processAddForm.processCode.trim();
if (!pCode) {
setEditError("請先選擇工序 Process Code");
return;
}

setEditProcesses((prev) => [
...prev,
{
key: genKey(),
seqNo: undefined,
processCode: pCode,
processName: "",
description: processAddForm.description ?? "",
equipmentCode: processAddForm.equipmentCode.trim(),
durationInMinute: processAddForm.durationInMinute ?? 0,
prepTimeInMinute: processAddForm.prepTimeInMinute ?? 0,
postProdTimeInMinute: processAddForm.postProdTimeInMinute ?? 0,
},
]);

setProcessAddForm({
processCode: "",
equipmentCode: "",
description: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
});
setEditError(null);
}, [processAddForm]);

const deleteMaterialRow = useCallback((key: string) => {
setEditMaterials((prev) => prev.filter((r) => r.key !== key));
}, []);

const deleteProcessRow = useCallback((key: string) => {
setEditProcesses((prev) => prev.filter((r) => r.key !== key));
}, []);

const handleSaveEdit = useCallback(async () => {
if (!detail || !editBasic) return;
setEditLoading(true);
setEditError(null);

try {
for (const p of editProcesses) {
if (!p.processCode?.trim()) {
throw new Error("工序行 Process Code 不能为空");
}
}

const payload: any = {
description: editBasic.description || undefined,
outputQty: editBasic.outputQty,
outputQtyUom: editBasic.outputQtyUom || undefined,

isDark: editBasic.isDark,
isFloat: editBasic.isFloat,
isDense: editBasic.isDense,
scrapRate: editBasic.scrapRate,
allergicSubstances: editBasic.allergicSubstances,
timeSequence: editBasic.timeSequence,
complexity: editBasic.complexity,
isDrink: editBasic.isDrink,
processes: editProcesses.map((p) => ({
id: p.id,
seqNo: p.seqNo,
processCode: p.processCode?.trim() || undefined,
equipmentCode: p.equipmentCode?.trim() || undefined,
description: p.description || undefined,
durationInMinute: p.durationInMinute,
prepTimeInMinute: p.prepTimeInMinute,
postProdTimeInMinute: p.postProdTimeInMinute,
})),
};

const updated = await editBomClient(detail.id, payload);
setDetail(updated);
setIsEditing(false);
} catch (e: any) {
setEditError(e?.message || "保存失败");
} finally {
setEditLoading(false);
}
}, [detail, editBasic, editProcesses]);

return (
<Stack spacing={2}>
<SearchBox<BomSearchKey>
@@ -175,33 +467,253 @@ const ImportBomDetailTab: React.FC = () => {

{/* Basic Info 列表 */}
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle1" gutterBottom>
{t("Basic Info")}
</Typography>

<Stack spacing={0.5}>
{/* 第一行:輸出數量 + 類型 */}
<Typography variant="body2">
{t("Output Quantity")}:{" "}
{detail.outputQty} {detail.outputQtyUom}
{" "}
{t("Type")}: {detail.description ?? "-"}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ mb: 1 }}
>
<Typography variant="subtitle1" gutterBottom>
{t("Basic Info")}
</Typography>

{/* 第二行:各種指標,排成一行 key:value, key:value */}
<Typography variant="body2">
{t("Allergic Substances")}: {renderAllergic(detail.allergicSubstances)}
{" "}{t("Depth")}: {detail.isDark ?? "-"}
{" "}{t("Float")}: {renderIsFloat(detail.isFloat)}
{" "}{t("Density")}: {renderIsDense(detail.isDense)}
</Typography>
{!isEditing ? (
<Button
size="small"
startIcon={<EditIcon />}
variant="outlined"
onClick={startEdit}
>
{t("Edit")}
</Button>
) : (
<Stack direction="row" spacing={1}>
<Button
size="small"
startIcon={<SaveIcon />}
variant="contained"
disabled={editLoading}
onClick={handleSaveEdit}
>
{editLoading ? t("Saving...") : t("Save")}
</Button>
<Button
size="small"
startIcon={<CancelIcon />}
variant="outlined"
disabled={editLoading}
onClick={cancelEdit}
>
{t("Cancel")}
</Button>
</Stack>
)}
</Stack>

<Typography variant="body2">
{t("Time Sequence")}: {renderTimeSequence(detail.timeSequence)}
{" "}{t("Complexity")}: {renderComplexity(detail.complexity)}
{" "}{t("Base Score")}: {detail.baseScore ?? "-"}
{editError && (
<Typography variant="body2" color="error">
{editError}
</Typography>
</Stack>
)}

{!isEditing && (
<Stack spacing={0.5}>
{/* 第一行:輸出數量 + 類型 */}
<Typography variant="body2">
{t("Output Quantity")}: {detail.outputQty} {detail.outputQtyUom}
{" "}
{t("Type")}: {detail.description ?? "-"}
</Typography>

{/* 第二行:各種指標,排成一行 key:value, key:value */}
<Typography variant="body2">
{t("Allergic Substances")}:{" "}
{renderAllergic(detail.allergicSubstances)}
{" "}{t("Depth")}: {detail.isDark ?? "-"}
{" "}{t("Float")}: {renderIsFloat(detail.isFloat)}
{" "}{t("Density")}: {renderIsDense(detail.isDense)}
</Typography>

<Typography variant="body2">
{t("Time Sequence")}: {renderTimeSequence(detail.timeSequence)}
{" "}{t("Complexity")}: {renderComplexity(detail.complexity)}
{" "}{t("Base Score")}: {detail.baseScore ?? "-"}
</Typography>
</Stack>
)}

{isEditing && editBasic && (
<Stack spacing={1}>
<TextField
size="small"
label={t("Type")}
value={editBasic.description}
onChange={(e) =>
setEditBasic((p) => (p ? { ...p, description: e.target.value } : p))
}
fullWidth
/>

<Stack direction="row" spacing={1}>
<TextField
size="small"
label={t("Output Quantity")}
type="number"
value={editBasic.outputQty}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, outputQty: Number(e.target.value) } : p
)
}
/>
<TextField
size="small"
label={t("Output Quantity UOM")}
value={editBasic.outputQtyUom}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, outputQtyUom: e.target.value } : p
)
}
/>
</Stack>

<Stack direction="row" spacing={1}>
<TextField
size="small"
label={t("Scrap Rate")}
type="number"
value={editBasic.scrapRate}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, scrapRate: Number(e.target.value) } : p
)
}
/>
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel>{t("Allergic Substances")}</InputLabel>
<Select
label={t("Allergic Substances")}
value={editBasic.allergicSubstances}
onChange={(e) =>
setEditBasic((p) =>
p
? {
...p,
allergicSubstances: Number(e.target.value),
}
: p,
)
}
>
<MenuItem value={0}>有</MenuItem>
<MenuItem value={5}>沒有</MenuItem>
</Select>
</FormControl>
</Stack>

<Stack direction="row" spacing={1}>
<TextField
size="small"
label={t("Depth")}
type="number"
value={editBasic.isDark}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, isDark: Number(e.target.value) } : p
)
}
inputProps={{ min: 1, max: 5, step: 1 }}
/>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>{t("Float")}</InputLabel>
<Select
label={t("Float")}
value={editBasic.isFloat}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, isFloat: Number(e.target.value) } : p,
)
}
>
<MenuItem value={5}>沉</MenuItem>
<MenuItem value={3}>浮</MenuItem>
<MenuItem value={0}>不適用</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>{t("Density")}</InputLabel>
<Select
label={t("Density")}
value={editBasic.isDense}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, isDense: Number(e.target.value) } : p,
)
}
>
<MenuItem value={5}>淡</MenuItem>
<MenuItem value={3}>濃</MenuItem>
<MenuItem value={0}>不適用</MenuItem>
</Select>
</FormControl>
</Stack>

<Stack direction="row" spacing={1}>
<FormControl size="small" sx={{ minWidth: 180 }}>
<InputLabel>{t("Time Sequence")}</InputLabel>
<Select
label={t("Time Sequence")}
value={editBasic.timeSequence}
onChange={(e) =>
setEditBasic((p) =>
p
? { ...p, timeSequence: Number(e.target.value) }
: p,
)
}
>
<MenuItem value={5}>上午</MenuItem>
<MenuItem value={1}>下午</MenuItem>
<MenuItem value={0}>不適用</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 180 }}>
<InputLabel>{t("Complexity")}</InputLabel>
<Select
label={t("Complexity")}
value={editBasic.complexity}
onChange={(e) =>
setEditBasic((p) =>
p
? { ...p, complexity: Number(e.target.value) }
: p,
)
}
>
<MenuItem value={10}>簡單</MenuItem>
<MenuItem value={5}>中度</MenuItem>
<MenuItem value={3}>複雜</MenuItem>
<MenuItem value={0}>不適用</MenuItem>
</Select>
</FormControl>
</Stack>

<FormControlLabel
control={
<Checkbox
checked={editBasic.isDrink}
onChange={(e) =>
setEditBasic((p) =>
p ? { ...p, isDrink: e.target.checked } : p
)
}
/>
}
label={t("Is Drink")}
/>
</Stack>
)}
</Paper>
{/* 材料列表 */}
<Paper variant="outlined" sx={{ p: 2 }}>
@@ -243,36 +755,271 @@ const ImportBomDetailTab: React.FC = () => {
<Typography variant="subtitle1" gutterBottom>
{t("Process & Equipment")}
</Typography>
{isEditing && (
<Box sx={{ mb: 1 }}>
<Stack direction="row" spacing={1} flexWrap="wrap">
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>{t("Process Code")}</InputLabel>
<Select
label={t("Process Code")}
value={processAddForm.processCode}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
processCode: String(e.target.value),
}))
}
>
{processCodeOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>

<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>{t("Equipment Code")}</InputLabel>
<Select
label={t("Equipment Code")}
value={processAddForm.equipmentCode}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
equipmentCode: String(e.target.value),
}))
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentCodeOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>

<TextField
size="small"
label={t("Process Description")}
value={processAddForm.description}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
description: e.target.value,
}))
}
/>

<TextField
size="small"
label={t("Duration (Minutes)")}
type="number"
value={processAddForm.durationInMinute}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
durationInMinute: Number(e.target.value),
}))
}
/>
<TextField
size="small"
label={t("Prep Time (Minutes)")}
type="number"
value={processAddForm.prepTimeInMinute}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
prepTimeInMinute: Number(e.target.value),
}))
}
/>
<TextField
size="small"
label={t("Post Prod Time (Minutes)")}
type="number"
value={processAddForm.postProdTimeInMinute}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
postProdTimeInMinute: Number(e.target.value),
}))
}
/>

<Button
size="small"
startIcon={<AddIcon />}
variant="contained"
onClick={addProcessFromForm}
>
{t("Add")}
</Button>
</Stack>
</Box>
)}
<Table size="small">
<TableHead>
<TableRow>
<TableCell> {t("Sequence")}</TableCell>
<TableCell> {t("Process Name")}</TableCell>
<TableCell> {t("Process Description")}</TableCell>
<TableCell> {t("Equipment Name")}</TableCell>
<TableCell> {t("Process Code")}</TableCell>
<TableCell> {t("Equipment Code")}</TableCell>
<TableCell align="right"> {t("Duration (Minutes)")}</TableCell>
<TableCell align="right"> {t("Prep Time (Minutes)")}</TableCell>
<TableCell align="right"> {t("Post Prod Time (Minutes)")}</TableCell>
{isEditing && (
<TableCell align="right">{t("Actions")}</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{detail.processes.map((p, i) => (
<TableRow key={i}>
<TableCell>{p.seqNo}</TableCell>
<TableCell>{p.processName}</TableCell>
<TableCell>{p.processDescription}</TableCell>
<TableCell>{p.equipmentName}</TableCell>
<TableCell align="right">
{p.durationInMinute}
</TableCell>
<TableCell align="right">
{p.prepTimeInMinute}
</TableCell>
<TableCell align="right">
{p.postProdTimeInMinute}
</TableCell>
</TableRow>
))}
{isEditing
? editProcesses.map((p) => (
<TableRow key={p.key}>
<TableCell>{p.seqNo ?? "-"}</TableCell>
<TableCell>{p.processName || "-"}</TableCell>
<TableCell>
<TextField
size="small"
value={p.description}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? { ...x, description: e.target.value }
: x,
),
)
}
/>
</TableCell>
<TableCell>
<FormControl size="small" fullWidth>
<Select
value={p.processCode ?? ""}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? {
...x,
processCode: String(e.target.value),
}
: x,
),
)
}
>
<MenuItem value="">不適用</MenuItem>
{processCodeOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>
</TableCell>
<TableCell>
<FormControl size="small" fullWidth>
<Select
value={p.equipmentCode ?? ""}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? {
...x,
equipmentCode: String(e.target.value),
}
: x,
),
)
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentCodeOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>
</TableCell>
<TableCell align="right">
<TextField
size="small"
type="number"
value={p.durationInMinute}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? { ...x, durationInMinute: Number(e.target.value) }
: x,
),
)
}
/>
</TableCell>
<TableCell align="right">
<TextField
size="small"
type="number"
value={p.prepTimeInMinute}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? { ...x, prepTimeInMinute: Number(e.target.value) }
: x,
),
)
}
/>
</TableCell>
<TableCell align="right">
<TextField
size="small"
type="number"
value={p.postProdTimeInMinute}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? {
...x,
postProdTimeInMinute: Number(e.target.value),
}
: x,
),
)
}
/>
</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => deleteProcessRow(p.key)}>
<DeleteIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))
: detail.processes.map((p, i) => (
<TableRow key={i}>
<TableCell>{p.seqNo}</TableCell>
<TableCell>{p.processName}</TableCell>
<TableCell>{p.processDescription}</TableCell>
<TableCell>{p.processCode ?? "-"}</TableCell>
<TableCell>{p.equipmentCode ?? p.equipmentName}</TableCell>
<TableCell align="right">{p.durationInMinute}</TableCell>
<TableCell align="right">{p.prepTimeInMinute}</TableCell>
<TableCell align="right">
{p.postProdTimeInMinute}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>


+ 42
- 26
src/components/StockTakeManagement/ApproverStockTakeAll.tsx View File

@@ -25,7 +25,8 @@ import {
InventoryLotDetailResponse,
SaveApproverStockTakeRecordRequest,
saveApproverStockTakeRecord,
getApproverInventoryLotDetailsAll,
getApproverInventoryLotDetailsAllPending,
getApproverInventoryLotDetailsAllApproved,
BatchSaveApproverStockTakeAllRequest,
batchSaveApproverStockTakeRecordsAll,
updateStockTakeRecordStatusToNotMatch,
@@ -37,7 +38,8 @@ import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";

interface ApproverStockTakeAllProps {
selectedSession: AllPickedStockTakeListReponse;
onBack: () => void;
mode: "pending" | "approved";
onBack?: () => void;
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
}

@@ -45,6 +47,7 @@ type QtySelectionType = "first" | "second" | "approver";

const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
selectedSession,
mode,
onBack,
onSnackbar,
}) => {
@@ -100,11 +103,17 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
actualSize = typeof size === "string" ? parseInt(size, 10) : size;
}

const response = await getApproverInventoryLotDetailsAll(
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize
);
const response = mode === "approved"
? await getApproverInventoryLotDetailsAllApproved(
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize
)
: await getApproverInventoryLotDetailsAllPending(
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize
);
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0);
} catch (e) {
@@ -115,7 +124,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
setLoadingDetails(false);
}
},
[selectedSession, total]
[selectedSession, total, mode]
);

useEffect(() => {
@@ -188,6 +197,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({

const handleSaveApproverStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (mode === "approved") return;
if (!selectedSession || !currentUserId) {
return;
}
@@ -285,11 +295,12 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
setSaving(false);
}
},
[selectedSession, currentUserId, qtySelection, approverQty, approverBadQty, t, onSnackbar]
[selectedSession, currentUserId, qtySelection, approverQty, approverBadQty, t, onSnackbar, mode]
);

const handleUpdateStatusToNotMatch = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (mode === "approved") return;
if (!detail.stockTakeRecordId) {
onSnackbar(t("Stock take record ID is required"), "error");
return;
@@ -328,6 +339,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
);

const handleBatchSubmitAll = useCallback(async () => {
if (mode === "approved") return;
if (!selectedSession || !currentUserId) {
return;
}
@@ -369,7 +381,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
} finally {
setBatchSaving(false);
}
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize]);
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize, mode]);

const formatNumber = (num: number | null | undefined): string => {
if (num == null) return "0";
@@ -393,12 +405,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({

return (
<Box>
<Button
onClick={onBack}
sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}
>
{t("Back to List")}
</Button>
{onBack && (
<Button
onClick={onBack}
sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}
>
{t("Back to List")}
</Button>
)}
<Stack
direction="row"
justifyContent="space-between"
@@ -422,14 +436,16 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
sx={{ width: 100 }}
inputProps={{ min: 0, max: 100, step: 0.1 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleBatchSubmitAll}
disabled={batchSaving}
>
{t("Batch Save All")}
</Button>
{mode === "pending" && (
<Button
variant="contained"
color="primary"
onClick={handleBatchSubmitAll}
disabled={batchSaving}
>
{t("Batch Save All")}
</Button>
)}
</Stack>
</Stack>
{loadingDetails ? (
@@ -748,7 +764,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
)}
</TableCell>
<TableCell>
{detail.stockTakeRecordId &&
{mode === "pending" && detail.stockTakeRecordId &&
detail.stockTakeRecordStatus !== "notMatch" && (
<Box>
<Button
@@ -768,7 +784,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
</Box>
)}
<br />
{detail.finalQty == null && (
{mode === "pending" && detail.finalQty == null && (
<Box>
<Button
size="small"


+ 59
- 45
src/components/StockTakeManagement/StockTakeTab.tsx View File

@@ -1,18 +1,15 @@
"use client";

import { Box, Tab, Tabs, Snackbar, Alert } from "@mui/material";
import { useState, useCallback } from "react";
import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography } from "@mui/material";
import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { AllPickedStockTakeListReponse } from "@/app/api/stockTake/actions";
import { AllPickedStockTakeListReponse, getApproverStockTakeRecords } from "@/app/api/stockTake/actions";
import PickerCardList from "./PickerCardList";
import ApproverCardList from "./ApproverCardList";
import PickerStockTake from "./PickerStockTake";
import PickerReStockTake from "./PickerReStockTake";
import ApproverStockTake from "./ApproverStockTake";
import ApproverAllCardList from "./ApproverAllCardList";
import ApproverStockTakeAll from "./ApproverStockTakeAll";

type ViewScope = "picker" | "approver-by-section" | "approver-all";
type ViewScope = "picker" | "approver-all";

const StockTakeTab: React.FC = () => {
const { t } = useTranslation(["inventory", "common"]);
@@ -20,6 +17,8 @@ const StockTakeTab: React.FC = () => {
const [selectedSession, setSelectedSession] = useState<AllPickedStockTakeListReponse | null>(null);
const [viewMode, setViewMode] = useState<"details" | "reStockTake">("details");
const [viewScope, setViewScope] = useState<ViewScope>("picker");
const [approverSession, setApproverSession] = useState<AllPickedStockTakeListReponse | null>(null);
const [approverLoading, setApproverLoading] = useState(false);
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
@@ -35,12 +34,6 @@ const StockTakeTab: React.FC = () => {
setViewMode("details");
}, []);

const handleApproverAllCardClick = useCallback((session: AllPickedStockTakeListReponse) => {
setSelectedSession(session);
setViewMode("details");
setViewScope("approver-all");
}, []);

const handleReStockTakeClick = useCallback((session: AllPickedStockTakeListReponse) => {
setSelectedSession(session);
setViewMode("reStockTake");
@@ -60,7 +53,22 @@ const StockTakeTab: React.FC = () => {
});
}, []);

if (selectedSession) {
useEffect(() => {
if (tabValue !== 1) return;
setApproverLoading(true);
getApproverStockTakeRecords()
.then((records) => {
const list = Array.isArray(records) ? records : [];
setApproverSession(list[0] ?? null);
})
.catch((e) => {
console.error(e);
setApproverSession(null);
})
.finally(() => setApproverLoading(false));
}, [tabValue]);

if (selectedSession && viewScope === "picker") {
return (
<Box>
{viewScope === "picker" && (
@@ -80,21 +88,6 @@ const StockTakeTab: React.FC = () => {
)
) : null
)}
{/*
{viewScope === "approver-by-section" && tabValue === 1 && (
<ApproverStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
)} */}
{viewScope === "approver-all" && tabValue === 1 && (
<ApproverStockTakeAll
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
)}
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
@@ -116,8 +109,6 @@ const StockTakeTab: React.FC = () => {
setTabValue(newValue);
if (newValue === 0) {
setViewScope("picker");
} else if (newValue === 1) {
setViewScope("approver-by-section");
} else {
setViewScope("approver-all");
}
@@ -125,8 +116,8 @@ const StockTakeTab: React.FC = () => {
sx={{ mb: 2 }}
>
<Tab label={t("Picker")} />
{/* <Tab label={t("Approver")} /> */}
<Tab label={t("Approver All")} />
<Tab label={t("Approver Pending")} />
<Tab label={t("Approver Approved")} />
</Tabs>

{tabValue === 0 && (
@@ -138,20 +129,43 @@ const StockTakeTab: React.FC = () => {
onReStockTakeClick={handleReStockTakeClick}
/>
)}
{/*
{tabValue === 1 && (
<ApproverCardList
onCardClick={(session) => {
setViewScope("approver-by-section");
handleCardClick(session);
}}
/>
<Box>
{approverLoading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : approverSession ? (
<ApproverStockTakeAll
selectedSession={approverSession}
mode="pending"
onSnackbar={handleSnackbar}
/>
) : (
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
)}
</Box>
)}
*/}
{tabValue === 1 && (
<ApproverAllCardList
onCardClick={handleApproverAllCardClick}
/>
{tabValue === 2 && (
<Box>
{approverLoading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : approverSession ? (
<ApproverStockTakeAll
selectedSession={approverSession}
mode="approved"
onSnackbar={handleSnackbar}
/>
) : (
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
)}
</Box>
)}

<Snackbar


Loading…
Cancel
Save