CANCERYS\kw093 5 дней назад
Родитель
Сommit
3da220f236
12 измененных файлов: 4927 добавлений и 1610 удалений
  1. +92
    -0
      src/app/api/pickOrder/actions.ts
  2. +30
    -0
      src/app/api/qc/actions.ts
  3. +25
    -1
      src/app/api/settings/item/actions.ts
  4. +13
    -0
      src/app/api/user/actions.ts
  5. +367
    -273
      src/components/PickOrderSearch/AssignAndRelease.tsx
  6. +1824
    -0
      src/components/PickOrderSearch/Jobcreatitem.tsx
  7. +547
    -394
      src/components/PickOrderSearch/PickExecution.tsx
  8. +24
    -9
      src/components/PickOrderSearch/PickOrderSearch.tsx
  9. +372
    -206
      src/components/PickOrderSearch/PickQcStockInModalVer3.tsx
  10. +481
    -170
      src/components/PickOrderSearch/assignTo.tsx
  11. +1116
    -546
      src/components/PickOrderSearch/newcreatitem.tsx
  12. +36
    -11
      src/i18n/zh/pickOrder.json

+ 92
- 0
src/app/api/pickOrder/actions.ts Просмотреть файл

@@ -119,6 +119,98 @@ export interface CurrentInventoryItemInfo {
requiredQty: number;
}

export interface SavePickOrderGroupRequest {
groupIds?: number[];
names?: string[];
targetDate?: string;
pickOrderId?: number | null;
}

export interface PickOrderGroupInfo {
id: number;
name: string;
targetDate: string | null;
pickOrderId: number | null;
}

export interface AssignPickOrderInputs {
pickOrderIds: number[];
assignTo: number;
}

// Missing function 1: newassignPickOrder
export const newassignPickOrder = async (data: AssignPickOrderInputs) => {
const response = await serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/pickOrder/assign`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("pickorder");
return response;
};

// Missing function 2: releaseAssignedPickOrders
export const releaseAssignedPickOrders = async (data: AssignPickOrderInputs) => {
const response = await serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/pickOrder/release-assigned`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("pickorder");
return response;
};
// Get latest group name and create it automatically
export const getLatestGroupNameAndCreate = async () => {
return serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/pickOrder/groups/latest`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
};

// Get all groups
export const fetchAllGroups = cache(async () => {
return serverFetchJson<PickOrderGroupInfo[]>(
`${BASE_API_URL}/pickOrder/groups/list`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
});

// Create or update groups (flexible - can handle both cases)
export const createOrUpdateGroups = async (data: SavePickOrderGroupRequest) => {
const response = await serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/pickOrder/groups/create`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("pickorder");
return response;
};

// Get groups by pick order ID
export const fetchGroupsByPickOrderId = cache(async (pickOrderId: number) => {
return serverFetchJson<PickOrderGroupInfo[]>(
`${BASE_API_URL}/pickOrder/groups/${pickOrderId}`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
});

export const fetchPickOrderDetails = cache(async (ids: string) => {
return serverFetchJson<GetPickOrderInfoResponse>(


+ 30
- 0
src/app/api/qc/actions.ts Просмотреть файл

@@ -16,7 +16,25 @@ export interface QcResult {
stockOutLineId?: number;
failQty: number;
}
export interface SaveQcResultRequest {
qcItemId: number;
itemId: number;
stockInLineId: number | null;
stockOutLineId: number;
failQty: number;
type: string;
remarks: string;
qcPassed: boolean;
}

export interface SaveQcResultResponse {
id: number | null;
name: string;
code: string;
type?: string;
message: string | null;
errorPosition: string;
}
export const fetchQcItemCheck = cache(async (itemId?: number) => {
let url = `${BASE_API_URL}/qcCheck`;
if (itemId) url += `/${itemId}`;
@@ -39,3 +57,15 @@ export const fetchPickOrderQcResult = cache(async (id: number) => {
},
);
});
export const savePickOrderQcResult = async (data: SaveQcResultRequest) => {
const response = await serverFetchJson<SaveQcResultResponse>(
`${BASE_API_URL}/qcResult/new`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("qc");
return response;
};

+ 25
- 1
src/app/api/settings/item/actions.ts Просмотреть файл

@@ -6,11 +6,12 @@ import {
} from "@/app/utils/fetchUtil";
import { revalidateTag } from "next/cache";
import { BASE_API_URL } from "@/config/api";
import { CreateItemResponse } from "../../utils";
import { CreateItemResponse, RecordsRes } from "../../utils";
import { ItemQc, ItemsResult } from ".";
import { QcChecksInputs } from "../qcCheck/actions";
import { cache } from "react";


// export type TypeInputs = {
// id: number;
// name: string
@@ -56,6 +57,7 @@ export interface ItemCombo {
label: string,
uomId: number,
uom: string,
uomDesc: string,
group?: string,
currentStockBalance?: number,
}
@@ -65,3 +67,25 @@ export const fetchAllItemsInClient = cache(async () => {
next: { tags: ["items"] },
});
});
export const fetchPickOrderItemsByPageClient = cache(
async (queryParams?: Record<string, any>) => {
if (queryParams) {
const queryString = new URLSearchParams(queryParams).toString();
return serverFetchJson<RecordsRes<any>>(
`${BASE_API_URL}/items/pickOrderItems?${queryString}`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
} else {
return serverFetchJson<RecordsRes<any>>(
`${BASE_API_URL}/items/pickOrderItems`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
}
},
);

+ 13
- 0
src/app/api/user/actions.ts Просмотреть файл

@@ -30,6 +30,13 @@ export interface NameList {
name: string;
}

export interface NewNameList {
id: number;
name: string;
title: string;
department: string;
}

export const fetchUserDetails = cache(async (id: number) => {
return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, {
next: { tags: ["user"] },
@@ -42,6 +49,12 @@ export const fetchNameList = cache(async () => {
});
});

export const fetchNewNameList = cache(async () => {
return serverFetchJson<NewNameList[]>(`${BASE_API_URL}/user/new-name-list`, {
next: { tags: ["user"] },
});
});

export const editUser = async (id: number, data: UserInputs) => {
const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "PUT",


+ 367
- 273
src/components/PickOrderSearch/AssignAndRelease.tsx Просмотреть файл

@@ -9,9 +9,6 @@ import {
Modal,
TextField,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
Table,
TableBody,
TableCell,
@@ -19,36 +16,27 @@ import {
TableHead,
TableRow,
Paper,
Checkbox,
TablePagination,
Alert,
AlertTitle,
} from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults/SearchResults";
import {
PickOrderResult,
} from "@/app/api/pickOrder";
import {
assignPickOrder,
fetchPickOrderClient,
newassignPickOrder,
AssignPickOrderInputs,
fetchPickOrderWithStockClient,
releasePickOrder,
ReleasePickOrderInputs,
GetPickOrderInfo,
GetPickOrderLineInfo,
} from "@/app/api/pickOrder/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
FormProvider,
useForm,
} from "react-hook-form";
import { isEmpty, upperCase, upperFirst } from "lodash";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions";
import { FormProvider, useForm } from "react-hook-form";
import { isEmpty, sortBy, uniqBy, upperFirst, groupBy } from "lodash";
import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
import useUploadContext from "../UploadProvider/useUploadContext";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import SearchBox, { Criterion } from "../SearchBox";
import { flatten, intersectionWith, sortBy, uniqBy } from "lodash";
import { arrayToDayjs } from "@/app/utils/formatUtil";
import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions";

dayjs.extend(arraySupport);

@@ -56,6 +44,56 @@ interface Props {
filterArgs: Record<string, any>;
}

// 使用 fetchPickOrderItemsByPageClient 返回的数据结构
interface ItemRow {
id: string;
pickOrderId: number;
pickOrderCode: string;
itemId: number;
itemCode: string;
itemName: string;
requiredQty: number;
currentStock: number;
unit: string;
targetDate: any;
status: string;
consoCode?: string;
assignTo?: number;
groupName?: string;
}

// 分组后的数据结构
interface GroupedItemRow {
pickOrderId: number;
pickOrderCode: string;
targetDate: any;
status: string;
consoCode?: string;
items: ItemRow[];
}

// 新增的 PickOrderRow 和 PickOrderLineRow 接口
interface PickOrderRow {
id: string; // Change from number to string to match API response
code: string;
targetDate: string;
type: string;
status: string;
assignTo: number;
groupName: string;
consoCode?: string;
pickOrderLines: PickOrderLineRow[];
}

interface PickOrderLineRow {
id: string;
itemCode: string;
itemName: string;
requiredQty: number;
availableQty: number;
uomDesc: string;
}

const style = {
position: "absolute",
top: "50%",
@@ -71,73 +109,85 @@ const style = {
const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder");
const { setIsUploading } = useUploadContext();
// State for Pick Orders
const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]);
const [filteredPickOrder, setFilteredPickOrder] = useState([] as GetPickOrderInfo[]);
const [isLoadingPickOrders, setIsLoadingPickOrders] = useState(false);
// Update state to use pick order data directly
const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]); // Change from number[] to string[]
const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]);
const [isLoadingItems, setIsLoadingItems] = useState(false);
const [pagingController, setPagingController] = useState({
pageNum: 0,
pageNum: 1,
pageSize: 10,
});
const [totalCountPickOrders, setTotalCountPickOrders] = useState<number>();

// State for Assign & Release Modal
const [totalCountItems, setTotalCountItems] = useState<number>();
const [modalOpen, setModalOpen] = useState(false);
const [usernameList, setUsernameList] = useState<NameList[]>([]);

// Add search state
const [usernameList, setUsernameList] = useState<NewNameList[]>([]);
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [originalPickOrderData, setOriginalPickOrderData] = useState([] as GetPickOrderInfo[]);
const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]);

const formProps = useForm<ReleasePickOrderInputs>();
const formProps = useForm<AssignPickOrderInputs>();
const errors = formProps.formState.errors;

// Fetch Pick Orders with Stock Information
const fetchNewPagePickOrder = useCallback(
async (
pagingController: Record<string, number>,
filterArgs: Record<string, number>,
) => {
setIsLoadingPickOrders(true);
const params = {
...pagingController,
...filterArgs,
};
const res = await fetchPickOrderWithStockClient(params);
if (res) {
console.log(res);
setFilteredPickOrder(res.records);
setOriginalPickOrderData(res.records); // Store original data
setTotalCountPickOrders(res.total);
// Update the fetch function to process pick order data correctly
const fetchNewPageItems = useCallback(
async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => {
setIsLoadingItems(true);
try {
const params = {
...pagingController,
...filterArgs,
pageNum: (pagingController.pageNum || 1) - 1,
pageSize: pagingController.pageSize || 10,
};

const res = await fetchPickOrderWithStockClient(params);

if (res && res.records) {
// Filter out assigned status if needed
const filteredRecords = res.records.filter((pickOrder: any) => pickOrder.status !== "assigned");
// Convert pick order data to the expected format
const pickOrderRows: PickOrderRow[] = filteredRecords.map((pickOrder: any) => ({
id: pickOrder.id,
code: pickOrder.code,
targetDate: pickOrder.targetDate,
type: pickOrder.type,
status: pickOrder.status,
assignTo: pickOrder.assignTo,
groupName: pickOrder.groupName || "No Group",
consoCode: pickOrder.consoCode,
pickOrderLines: pickOrder.pickOrderLines || []
}));
setOriginalPickOrderData(pickOrderRows);
setFilteredPickOrders(pickOrderRows);
setTotalCountItems(res.total);
} else {
setFilteredPickOrders([]);
setTotalCountItems(0);
}
} catch (error) {
console.error("Error fetching pick orders:", error);
setFilteredPickOrders([]);
setTotalCountItems(0);
} finally {
setIsLoadingItems(false);
}
setIsLoadingPickOrders(false);
},
[],
);

// Add search criteria
// Update search criteria to match the new data structure
const searchCriteria: Criterion<any>[] = useMemo(
() => [
{
label: t("Pick Order Code"),
paramName: "code",
type: "text"
{
label: t("Pick Order Code"),
paramName: "code",
type: "text",
},
{
label: t("Type"),
paramName: "type",
type: "autocomplete",
options: sortBy(
uniqBy(
originalPickOrderData.map((po) => ({
value: po.type,
label: t(upperCase(po.type)),
})),
"value",
),
"label",
),
label: t("Group Name"),
paramName: "groupName",
type: "text",
},
{
label: t("Target Date From"),
@@ -146,14 +196,14 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
type: "dateRange",
},
{
label: t("Status"),
label: t("Pick Order Status"),
paramName: "status",
type: "autocomplete",
options: sortBy(
uniqBy(
originalPickOrderData.map((po) => ({
value: po.status,
label: t(upperFirst(po.status)),
originalPickOrderData.map((pickOrder) => ({
value: pickOrder.status,
label: t(upperFirst(pickOrder.status)),
})),
"value",
),
@@ -164,103 +214,144 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
[originalPickOrderData, t],
);

// Add search handler
// Update search function to work with pick order data
const handleSearch = useCallback((query: Record<string, any>) => {
console.log("AssignAndRelease search triggered with query:", query);
setSearchQuery({ ...query });
// Apply search filters to the data
const filtered = originalPickOrderData.filter((po) => {
const poTargetDateStr = arrayToDayjs(po.targetDate);

const filtered = originalPickOrderData.filter((pickOrder) => {
const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate);

const codeMatch = !query.code ||
po.code?.toLowerCase().includes((query.code || "").toLowerCase());
pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase());
const dateMatch = !query.targetDate ||
poTargetDateStr.isSame(query.targetDate) ||
poTargetDateStr.isAfter(query.targetDate);
const groupNameMatch = !query.groupName ||
pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase());
const dateToMatch = !query.targetDateTo ||
poTargetDateStr.isSame(query.targetDateTo) ||
poTargetDateStr.isBefore(query.targetDateTo);
// Date range search
let dateMatch = true;
if (query.targetDate || query.targetDateTo) {
try {
if (query.targetDate && !query.targetDateTo) {
const fromDate = dayjs(query.targetDate);
dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') ||
pickOrderTargetDateStr.isAfter(fromDate, 'day');
} else if (!query.targetDate && query.targetDateTo) {
const toDate = dayjs(query.targetDateTo);
dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') ||
pickOrderTargetDateStr.isBefore(toDate, 'day');
} else if (query.targetDate && query.targetDateTo) {
const fromDate = dayjs(query.targetDate);
const toDate = dayjs(query.targetDateTo);
dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') ||
pickOrderTargetDateStr.isAfter(fromDate, 'day')) &&
(pickOrderTargetDateStr.isSame(toDate, 'day') ||
pickOrderTargetDateStr.isBefore(toDate, 'day'));
}
} catch (error) {
console.error("Date parsing error:", error);
dateMatch = true;
}
}
const statusMatch = !query.status ||
query.status.toLowerCase() === "all" ||
po.status?.toLowerCase().includes((query.status || "").toLowerCase());
const typeMatch = !query.type ||
query.type.toLowerCase() === "all" ||
po.type?.toLowerCase().includes((query.type || "").toLowerCase());
pickOrder.status?.toLowerCase().includes((query.status || "").toLowerCase());

return codeMatch && dateMatch && dateToMatch && statusMatch && typeMatch;
return codeMatch && groupNameMatch && dateMatch && statusMatch;
});
setFilteredPickOrder(filtered);
setFilteredPickOrders(filtered);
}, [originalPickOrderData]);

// Add reset handler
const handleReset = useCallback(() => {
setSearchQuery({});
// Reset to original data
setFilteredPickOrder(originalPickOrderData);
setFilteredPickOrders(originalPickOrderData);
setTimeout(() => {
setSearchQuery({});
}, 0);
}, [originalPickOrderData]);

// Handle Assign & Release
const handleAssignAndRelease = useCallback(async (data: ReleasePickOrderInputs) => {
if (selectedRows.length === 0) return;
// Fix the pagination handlers
const handlePageChange = useCallback((event: unknown, newPage: number) => {
const newPagingController = {
...pagingController,
pageNum: newPage + 1,
};
setPagingController(newPagingController);
}, [pagingController]);

const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10);
const newPagingController = {
pageNum: 1,
pageSize: newPageSize,
};
setPagingController(newPagingController);
}, []);

// 修复:处理 pick order 选择
const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => {
if (checked) {
setSelectedPickOrderIds(prev => [...prev, pickOrderId]);
} else {
setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId));
}
}, []);

// 修复:检查 pick order 是否被选中
const isPickOrderSelected = useCallback((pickOrderId: string) => {
return selectedPickOrderIds.includes(pickOrderId);
}, [selectedPickOrderIds]);

const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => {
if (selectedPickOrderIds.length === 0) return;

setIsUploading(true);
try {
// First, assign the pick orders
const assignRes = await assignPickOrder(selectedRows as number[]);
if (assignRes) {
// Convert string IDs to numbers for the API
const numericIds = selectedPickOrderIds.map(id => parseInt(id, 10));
const assignRes = await newassignPickOrder({
pickOrderIds: numericIds,
assignTo: data.assignTo,
});

if (assignRes && assignRes.code === "SUCCESS") {
console.log("Assign successful:", assignRes);
// Get the assign code from the response
const consoCode = assignRes.consoCode || assignRes.code;
if (consoCode) {
// Then, release the assign pick order
const releaseData = {
consoCode: consoCode,
assignTo: data.assignTo
};
const releaseRes = await releasePickOrder(releaseData);
if (releaseRes) {
console.log("Release successful:", releaseRes);
setModalOpen(false);
// Clear selected rows
setSelectedRows([]);
// Refresh the pick orders list
fetchNewPagePickOrder(pagingController, filterArgs);
}
}
setModalOpen(false);
setSelectedPickOrderIds([]); // Clear selection
fetchNewPageItems(pagingController, filterArgs);
} else {
console.error("Assign failed:", assignRes);
}
} catch (error) {
console.error("Error in assign and release:", error);
console.error("Error in assign:", error);
} finally {
setIsUploading(false);
}
}, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]);
}, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]);

// Open assign & release modal
const openAssignModal = useCallback(() => {
setModalOpen(true);
// Reset form
formProps.reset();
}, [formProps]);

// Load data
// Component mount effect
useEffect(() => {
fetchNewPagePickOrder(pagingController, filterArgs);
}, [fetchNewPagePickOrder, pagingController, filterArgs]);
fetchNewPageItems(pagingController, filterArgs || {});
}, []);

// Dependencies change effect
useEffect(() => {
if (pagingController && (filterArgs || {})) {
fetchNewPageItems(pagingController, filterArgs || {});
}
}, [pagingController, filterArgs, fetchNewPageItems]);

// Load username list
useEffect(() => {
const loadUsernameList = async () => {
try {
const res = await fetchNameList();
const res = await fetchNewNameList();
if (res) {
setUsernameList(res);
}
@@ -271,143 +362,141 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
loadUsernameList();
}, []);

// Pick Orders columns with detailed item information
const pickOrderColumns = useMemo<Column<GetPickOrderInfo>[]>(
() => [
{
name: "id",
label: "",
type: "checkbox",
disabled: (params) => {
return !isEmpty(params.consoCode);
},
},
{
name: "code",
label: t("Pick Order Code"),
},
{
name: "pickOrderLines",
label: t("Items"),
renderCell: (params) => {
if (!params.pickOrderLines || params.pickOrderLines.length === 0) return "";
return (
<Accordion sx={{ boxShadow: 'none', '&:before': { display: 'none' } }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{ minHeight: 'auto', padding: 0 }}
>
<Typography variant="body2">
{params.pickOrderLines.length} items
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ padding: 1 }}>
<TableContainer component={Paper} sx={{ maxHeight: 200 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Item Name")}</TableCell>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Required Qty")}</TableCell>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Available Qty")}</TableCell>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Unit")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{params.pickOrderLines.map((line: GetPickOrderLineInfo, index: number) => (
<TableRow key={index}>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>
{line.itemName}
</TableCell>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>
{line.requiredQty}
</TableCell>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>
<Typography
variant="caption"
//color={line.availableQty && line.availableQty >= line.requiredQty ? 'success.main' : 'error.main'}
>
{line.availableQty ?? 0}
</Typography>
</TableCell>
<TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>
{line.uomDesc}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
);
},
},
{
name: "targetDate",
label: t("Target Date"),
renderCell: (params) => {
return (
dayjs(params.targetDate)
.add(-1, "month")
.format(OUTPUT_DATE_FORMAT)
);
},
},
{
name: "status",
label: t("Status"),
renderCell: (params) => {
return upperFirst(params.status);
},
},
],
[t],
);
// Update the table component to work with pick order data directly
const CustomPickOrderTable = () => {
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Group Name")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell align="right">{t("Order Quantity")}</TableCell>
<TableCell align="right">{t("Current Stock")}</TableCell>
<TableCell align="right">{t("Stock Unit")}</TableCell>
<TableCell>{t("Target Date")}</TableCell>
<TableCell>{t("Pick Order Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredPickOrders.length === 0 ? (
<TableRow>
<TableCell colSpan={10} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
filteredPickOrders.map((pickOrder) => (
pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => (
<TableRow key={`${pickOrder.id}-${line.id}`}>
{/* Checkbox - only show for first line of each pick order */}
<TableCell>
{index === 0 ? (
<Checkbox
checked={isPickOrderSelected(pickOrder.id)}
onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)}
disabled={!isEmpty(pickOrder.consoCode)}
/>
) : null}
</TableCell>
{/* Pick Order Code - only show for first line */}
<TableCell>
{index === 0 ? pickOrder.code : null}
</TableCell>
{/* Group Name - only show for first line */}
<TableCell>
{index === 0 ? pickOrder.groupName : null}
</TableCell>
{/* Item Code */}
<TableCell>{line.itemCode}</TableCell>
{/* Item Name */}
<TableCell>{line.itemName}</TableCell>
{/* Order Quantity */}
<TableCell align="right">{line.requiredQty}</TableCell>
{/* Current Stock */}
<TableCell align="right">
<Typography
variant="body2"
color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"}
sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }}
>
{(line.availableQty || 0).toLocaleString()}
</Typography>
</TableCell>
{/* Unit */}
<TableCell align="right">{line.uomDesc}</TableCell>
{/* Target Date - only show for first line */}
<TableCell>
{index === 0 ? (
arrayToDayjs(pickOrder.targetDate)
.add(-1, "month")
.format(OUTPUT_DATE_FORMAT)
) : null}
</TableCell>
{/* Pick Order Status - only show for first line */}
<TableCell>
{index === 0 ? upperFirst(pickOrder.status) : null}
</TableCell>
</TableRow>
))
))
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={totalCountItems || 0}
page={(pagingController.pageNum - 1)}
rowsPerPage={pagingController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50, 100]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);
};

return (
<>
{/* Search Box */}
<SearchBox
criteria={searchCriteria}
onSearch={handleSearch}
onReset={handleReset}
/>
{/* Pick Orders View */}
<SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
<Grid container rowGap={1}>
{/* Remove the button from here */}
<Grid item xs={12}>
{isLoadingPickOrders ? (
{isLoadingItems ? (
<CircularProgress size={40} />
) : (
<SearchResults<GetPickOrderInfo>
items={filteredPickOrder}
columns={pickOrderColumns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={totalCountPickOrders}
checkboxIds={selectedRows!}
setCheckboxIds={setSelectedRows}
/>
<CustomPickOrderTable />
)}
</Grid>
{/* Add the button below the table */}
<Grid item xs={12}>
<Box sx={{ display: 'flex', justifyContent: 'flex-start', mt: 2 }}>
<Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}>
<Button
disabled={selectedRows.length < 1}
disabled={selectedPickOrderIds.length < 1}
variant="outlined"
onClick={openAssignModal}
>
{t("Assign & Release")}
{t("Assign")}
</Button>
</Box>
</Grid>
</Grid>

{/* Assign & Release Modal */}
{modalOpen ? (
<Modal
open={modalOpen}
@@ -419,25 +508,37 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
<Grid container rowGap={2}>
<Grid item xs={12}>
<Typography variant="h6" component="h2">
{t("assign & Release Pick Orders")}
{t("Assign Pick Orders")}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1" color="text.secondary">
{t("Selected Pick Orders")}: {selectedRows.length}
{t("Selected Pick Orders")}: {selectedPickOrderIds.length}
</Typography>
</Grid>

<Grid item xs={12}>
<FormProvider {...formProps}>
<FormProvider {...formProps}>
<form onSubmit={formProps.handleSubmit(handleAssignAndRelease)}>
<Grid container spacing={2}>
<Grid item xs={12}>
<FormControl fullWidth>
<Autocomplete
options={usernameList}
getOptionLabel={(option) => option.name}
getOptionLabel={(option) => {
// 修改:显示更详细的用户信息
const title = option.title ? ` (${option.title})` : '';
const department = option.department ? ` - ${option.department}` : '';
return `${option.name}${title}${department}`;
}}
renderOption={(props, option) => (
<Box component="li" {...props}>
<Typography variant="body1">
{option.name}
{option.title && ` (${option.title})`}
{option.department && ` - ${option.department}`}
</Typography>
</Box>
)}
onChange={(_, value) => {
formProps.setValue("assignTo", value?.id || 0);
}}
@@ -455,23 +556,16 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="warning.main">
{t("This action will assign the selected pick orders and release them immediately.")}
{t("This action will assign the selected pick orders.")}
</Typography>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
<Button
variant="outlined"
onClick={() => setModalOpen(false)}
>
<Box sx={{ display: "flex", gap: 2, justifyContent: "flex-end" }}>
<Button variant="outlined" onClick={() => setModalOpen(false)}>
{t("Cancel")}
</Button>
<Button
type="submit"
variant="contained"
color="primary"
>
{t("Assign & Release")}
<Button type="submit" variant="contained" color="primary">
{t("Assign")}
</Button>
</Box>
</Grid>


+ 1824
- 0
src/components/PickOrderSearch/Jobcreatitem.tsx
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 547
- 394
src/components/PickOrderSearch/PickExecution.tsx
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 24
- 9
src/components/PickOrderSearch/PickOrderSearch.tsx Просмотреть файл

@@ -25,6 +25,7 @@ import AssignAndRelease from "./AssignAndRelease";
import AssignTo from "./assignTo";
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
import { fetchPickOrderClient } from "@/app/api/pickOrder/actions";
import Jobcreatitem from "./Jobcreatitem";

interface Props {
pickOrders: PickOrderResult[];
@@ -224,6 +225,12 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
}
}, [isOpenCreateModal])
// 添加处理提料单创建成功的函数
const handlePickOrderCreated = useCallback(() => {
// 切换到 Assign & Release 标签页 (tabIndex = 1)
setTabIndex(1);
}, []);

return (
<>
<Stack
@@ -313,24 +320,32 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {

<Tab label={t("Select Items")} iconPosition="end" />
<Tab label={t("Select Items")} iconPosition="end" />
<Tab label={t("Select Job Order Items")} iconPosition="end" />
<Tab label={t("Assign")} iconPosition="end" />
<Tab label={t("Release")} iconPosition="end" />
<Tab label={t("Pick Execution")} iconPosition="end" />
<Tab label={t("Pick Orders")} iconPosition="end" />
<Tab label={t("Consolidated Pick Orders")} iconPosition="end" />
{/*<Tab label={t("Pick Orders")} iconPosition="end" />*/}
{/*<Tab label={t("Consolidated Pick Orders")} iconPosition="end" />*/}
</Tabs>
{tabIndex === 4 && (
{/*{tabIndex === 4 && (
<PickOrders
filteredPickOrders={filteredPickOrders}
filterArgs={filterArgs}
/>
)}*/}
{/*{tabIndex === 5 && <ConsolidatedPickOrders filterArgs={filterArgs} />}*/}
{tabIndex === 4 && <PickExecution filterArgs={filterArgs} />}
{tabIndex === 0 && (
<NewCreateItem
filterArgs={filterArgs}
searchQuery={searchQuery}
onPickOrderCreated={handlePickOrderCreated}
/>
)}
{tabIndex === 5 && <ConsolidatedPickOrders filterArgs={filterArgs} />}
{tabIndex === 3 && <PickExecution filterArgs={filterArgs} />}
{tabIndex === 0 && <NewCreateItem filterArgs={filterArgs} searchQuery={searchQuery} />}
{tabIndex === 1 && <AssignAndRelease filterArgs={filterArgs} />}
{tabIndex === 2 && <AssignTo filterArgs={filterArgs} />}
{tabIndex === 1 && <Jobcreatitem filterArgs={filterArgs} />}
{tabIndex === 2&& <AssignAndRelease filterArgs={filterArgs} />}
{tabIndex === 3 && <AssignTo filterArgs={filterArgs} />}
</>
);
};


+ 372
- 206
src/components/PickOrderSearch/PickQcStockInModalVer3.tsx Просмотреть файл

@@ -11,24 +11,33 @@ import {
ModalProps,
Stack,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
TextField,
Radio,
RadioGroup,
FormControlLabel,
FormControl,
Tab,
Tabs,
TabsProps,
Paper,
} from "@mui/material";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate";
import { dummyQCData } from "../PoDetail/dummyQcTemplate";
import StyledDataGrid from "../StyledDataGrid";
import { GridColDef } from "@mui/x-data-grid";
import { submitDialogWithWarning } from "../Swal/CustomAlerts";
import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable";
import EscalationComponent from "../PoDetail/EscalationComponent";
import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions";

// Define QcData interface locally
interface ExtendedQcItem extends QcItemWithChecks {
qcPassed?: boolean;
failQty?: number;
remarks?: string;
}

const style = {
position: "absolute",
@@ -40,10 +49,11 @@ const style = {
px: 5,
pb: 10,
display: "block",
width: { xs: "60%", sm: "60%", md: "60%" },
width: { xs: "80%", sm: "80%", md: "80%" },
maxHeight: "90vh",
overflowY: "auto",
};


interface CommonProps extends Omit<ModalProps, "children"> {
itemDetail: GetPickOrderLineInfo & {
pickOrderCode: string;
@@ -67,31 +77,47 @@ interface Props extends CommonProps {
pickOrderCode: string;
qcResult?: PurchaseQcResult[]
};
qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem
setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem
}


const PickQcStockInModalVer2: React.FC<Props> = ({
const PickQcStockInModalVer3: React.FC<Props> = ({
open,
onClose,
itemDetail,
setItemDetail,
qc,
warehouse,
qcItems,
setQcItems,
}) => {
console.log(warehouse);
const {
t,
i18n: { language },
} = useTranslation("pickOrder");
const [qcItems, setQcItems] = useState(dummyQCData)

const [tabIndex, setTabIndex] = useState(0);
//const [qcItems, setQcItems] = useState<QcData[]>(dummyQCData);
const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState<string>("");

// Add state to store submitted data
const [submittedData, setSubmittedData] = useState<any[]>([]);

const formProps = useForm<any>({
defaultValues: {
qcAccept: true,
acceptQty: itemDetail.requiredQty ?? 0,
qcDecision: "1", // Default to accept
...itemDetail,
},
});
const { control, register, formState: { errors }, watch, setValue } = formProps;

const qcDecision = watch("qcDecision");
const accQty = watch("acceptQty");

const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
onClose?.(...args);
@@ -99,228 +125,368 @@ const PickQcStockInModalVer2: React.FC<Props> = ({
[onClose],
);

// QC submission handler
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[],
);

// Save failed QC results only
const saveQcResults = async (qcData: any) => {
try {
const qcResults = qcData.qcItems
.map((item: any) => ({
qcItemId: item.id,
itemId: itemDetail.itemId,
stockInLineId: null,
stockOutLineId: 1, // Fixed to 1 as requested
failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed
type: "pick_order_qc",
remarks: item.remarks || "",
qcPassed: item.isPassed, // ✅ This will now be included
}));

// Store the submitted data for debug display
setSubmittedData(qcResults);
console.log("Saving QC results:", qcResults);

// Use the corrected API function instead of manual fetch
for (const qcResult of qcResults) {
const response = await savePickOrderQcResult(qcResult);
console.log("QC Result save success:", response);
// Check if the response indicates success
if (!response.id) {
throw new Error(`Failed to save QC result: ${response.message || 'Unknown error'}`);
}
}
return true;
} catch (error) {
console.error("Error saving QC results:", error);
return false;
}
};

// Submit with QcComponent-style decision handling
const onSubmitQc = useCallback<SubmitHandler<any>>(
async (data, event) => {
console.log("QC Submission:", event!.nativeEvent);
setIsSubmitting(true);
// Get QC data from the shared form context
const qcAccept = data.qcAccept;
const acceptQty = data.acceptQty;
// Validate QC data
const validationErrors : string[] = [];
// Check if all QC items have results
const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined);
if (itemsWithoutResult.length > 0) {
validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`);
}
try {
const qcAccept = qcDecision === "1";
const acceptQty = Number(accQty) || itemDetail.requiredQty;

// Check if failed items have failed quantity
const failedItemsWithoutQty = qcItems.filter(item =>
item.isPassed === false && (!item.failedQty || item.failedQty <= 0)
);
if (failedItemsWithoutQty.length > 0) {
validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`);
}
const validationErrors : string[] = [];

// Check if accept quantity is valid
if (acceptQty === undefined || acceptQty <= 0) {
validationErrors.push("Accept quantity must be greater than 0");
}
const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined);
if (itemsWithoutResult.length > 0) {
validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`);
}

if (validationErrors.length > 0) {
console.error("QC Validation failed:", validationErrors);
alert(`QC failed: ${validationErrors}`);
return;
}
const failedItemsWithoutQty = qcItems.filter(item =>
item.qcPassed === false && (!item.failQty || item.failQty <= 0)
);
if (failedItemsWithoutQty.length > 0) {
validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.code).join(", ")}`);
}

const qcData = {
qcAccept: qcAccept,
acceptQty: acceptQty,
qcItems: qcItems.map(item => ({
id: item.id,
qcItem: item.qcItem,
qcDescription: item.qcDescription,
isPassed: item.isPassed,
failedQty: (item.failedQty && !item.isPassed) || 0,
remarks: item.remarks || ''
}))
};
if (qcDecision === "1" && (acceptQty === undefined || acceptQty <= 0)) {
validationErrors.push("Accept quantity must be greater than 0");
}

console.log("QC Data for submission:", qcData);
// await submitQcData(qcData);
if (validationErrors.length > 0) {
alert(`QC failed: ${validationErrors.join(", ")}`);
return;
}

if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(() => {
console.log("QC accepted with failed items");
onClose?.();
}, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""});
return;
}
const qcData = {
qcAccept,
acceptQty,
qcItems: qcItems.map(item => ({
id: item.id,
qcItem: item.code, // Use code instead of qcItem
qcDescription: item.description || "", // Use description instead of qcDescription
isPassed: item.qcPassed,
failQty: item.qcPassed ? 0 : (item.failQty ?? 0),
remarks: item.remarks || "",
})),
};

if (qcData.qcAccept) {
console.log("QC accepted");
onClose?.();
} else {
console.log("QC rejected");
onClose?.();
console.log("Submitting QC data:", qcData);

const saveSuccess = await saveQcResults(qcData);
if (!saveSuccess) {
alert("Failed to save QC results");
return;
}

// Show success message
alert("QC results saved successfully!");

if (!qcData.qcItems.every((q) => q.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(() => {
closeHandler?.({}, 'escapeKeyDown');
}, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""});
return;
}

closeHandler?.({}, 'escapeKeyDown');
} catch (error) {
console.error("Error in QC submission:", error);
alert("Error saving QC results: " + (error as Error).message);
} finally {
setIsSubmitting(false);
}
},
[qcItems, onClose, t],
[qcItems, closeHandler, t, itemDetail, qcDecision, accQty],
);

const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => {
setQcItems(prev => prev.map((item, i) =>
i === index ? { ...item, [field]: value } : item
));
}, []);
// DataGrid columns (QcComponent style)
const qcColumns: GridColDef[] = useMemo(
() => [
{
field: "code",
headerName: t("qcItem"),
flex: 2,
renderCell: (params) => (
<Box>
<b>{`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}</b><br/>
{params.row.name}<br/>
</Box>
),
},
{
field: "qcPassed",
headerName: t("qcResult"),
flex: 1.5,
renderCell: (params) => {
const current = params.row;
return (
<FormControl>
<RadioGroup
row
aria-labelledby="qc-result"
value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")}
onChange={(e) => {
const value = e.target.value === "true";
setQcItems((prev) =>
prev.map((r): ExtendedQcItem => (r.id === params.id ? { ...r, qcPassed: value } : r))
);
}}
name={`qcPassed-${params.id}`}
>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="合格"
sx={{
color: current.qcPassed === true ? "green" : "inherit",
"& .Mui-checked": {color: "green"}
}}
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="不合格"
sx={{
color: current.qcPassed === false ? "red" : "inherit",
"& .Mui-checked": {color: "red"}
}}
/>
</RadioGroup>
</FormControl>
);
},
},
{
field: "failQty",
headerName: t("failedQty"),
flex: 1,
renderCell: (params) => (
<TextField
type="number"
size="small"
value={!params.row.qcPassed ? (params.value ?? "") : "0"}
disabled={params.row.qcPassed}
onChange={(e) => {
const v = e.target.value;
const next = v === "" ? undefined : Number(v);
if (Number.isNaN(next)) return;
setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, failQty: next } : r))
);
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
inputProps={{ min: 0 }}
sx={{ width: "100%" }}
/>
),
},
{
field: "remarks",
headerName: t("remarks"),
flex: 2,
renderCell: (params) => (
<TextField
size="small"
value={params.value ?? ""}
onChange={(e) => {
const remarks = e.target.value;
setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, remarks } : r))
);
}}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
sx={{ width: "100%" }}
/>
),
},
],
[t],
);

return (
<>
<FormProvider {...formProps}>
<Modal open={open} onClose={closeHandler}>
<Box
sx={{
...style,
padding: 2,
maxHeight: "90vh",
overflowY: "auto",
marginLeft: 3,
marginRight: 3,
}}
>
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Box sx={style}>
<Grid container justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
GroupA - {itemDetail.pickOrderCode}
</Typography>
<Typography variant="body2" color="text.secondary" marginBlockEnd={2}>
記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標
</Typography>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab label={t("QC Info")} iconPosition="end" />
<Tab label={t("Escalation History")} iconPosition="end" />
</Tabs>
</Grid>
{/* QC table - same as QcFormVer2 */}
{tabIndex == 0 && (
<>
<Grid item xs={12}>
<Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
<Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}>
Group A - 急凍貨類 (QCA1-MEAT01)
</Typography>
<Typography variant="subtitle1" sx={{ color: '#666' }}>
<b>品檢類型</b>:OQC
</Typography>
<Typography variant="subtitle2" sx={{ color: '#666' }}>
記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全<br/>
監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標
</Typography>
</Box>
<StyledDataGrid
columns={qcColumns}
rows={qcItems}
autoHeight
/>
</Grid>
</>
)}
{tabIndex == 1 && (
<>
<Grid item xs={12}>
<EscalationLogTable items={[]}/>
</Grid>
</>
)}
<Grid item xs={12}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell sx={{ width: '80px' }}>QC模板代號</TableCell>
<TableCell sx={{ width: '300px' }}>檢查項目</TableCell>
<TableCell sx={{ width: '120px' }}>QC RESULT</TableCell>
<TableCell sx={{ width: '80px' }}>FAILED QTY</TableCell>
<TableCell sx={{ width: '300px' }}>REMARKS</TableCell>
</TableRow>
</TableHead>
<TableBody>
{qcItems.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{item.id}</TableCell>
<TableCell sx={{
maxWidth: '300px',
wordWrap: 'break-word',
whiteSpace: 'normal'
}}>
{item.qcDescription}
</TableCell>
<TableCell>
{/* same as QcFormVer2 */}
<FormControl>
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
value={item.isPassed === undefined ? "" : (item.isPassed ? "true" : "false")}
onChange={(e) => {
const value = e.target.value;
handleQcItemChange(index, 'isPassed', value === "true");
}}
name={`isPassed-${item.id}`}
>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="合格"
sx={{
color: item.isPassed === true ? "green" : "inherit",
"& .Mui-checked": {color: "green"}
}}
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="不合格"
sx={{
color: item.isPassed === false ? "red" : "inherit",
"& .Mui-checked": {color: "red"}
}}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
<TextField
type="number"
size="small"
value={!item.isPassed ? (item.failedQty ?? 0) : 0}
disabled={item.isPassed}
onChange={(e) => {
const v = e.target.value;
const next = v === '' ? undefined : Number(v);
if (Number.isNaN(next)) return;
handleQcItemChange(index, 'failedQty', next);
}}
inputProps={{ min: 0 }}
sx={{ width: '60px' }}
/>
</TableCell>
<TableCell>
<TextField
size="small"
value={item.remarks ?? ''}
onChange={(e) => {
const remarks = e.target.value;
handleQcItemChange(index, 'remarks', remarks);
}}
sx={{ width: '280px' }}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<FormControl>
<Controller
name="qcDecision"
control={control}
defaultValue="1"
render={({ field }) => (
<RadioGroup
row
aria-labelledby="demo-radio-buttons-group-label"
{...field}
value={field.value}
onChange={(e) => {
const value = e.target.value.toString();
if (value != "1" && Boolean(errors.acceptQty)) {
setValue("acceptQty", itemDetail.requiredQty ?? 0);
}
field.onChange(value);
}}
>
<FormControlLabel
value="1"
control={<Radio />}
label="接受出庫"
/>
<Box sx={{mr:2}}>
<TextField
type="number"
label={t("acceptQty")}
sx={{ width: '150px' }}
value={(qcDecision == 1)? accQty : 0 }
disabled={qcDecision != 1}
{...register("acceptQty", {
required: "acceptQty required!",
})}
error={Boolean(errors.acceptQty)}
helperText={errors.acceptQty?.message?.toString() || ""}
/>
</Box>

<FormControlLabel
value="2"
control={<Radio />}
sx={{"& .Mui-checked": {color: "red"}}}
label="不接受並重新揀貨"
/>
<FormControlLabel
value="3"
control={<Radio />}
sx={{"& .Mui-checked": {color: "blue"}}}
label="上報品檢結果"
/>
</RadioGroup>
)}
/>
</FormControl>
</Grid>
{/* buttons */}
{qcDecision == 3 && (
<Grid item xs={12}>
<EscalationComponent
forSupervisor={false}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
/>
</Grid>
)}
<Grid item xs={12} sx={{ mt: 2 }}>
<Stack direction="row" justifyContent="flex-start" gap={1}>
<Button
variant="contained"
onClick={formProps.handleSubmit(onSubmitQc)}
disabled={isSubmitting}
sx={{ whiteSpace: 'nowrap' }}
>
QC Accept
</Button>
<Button
variant="contained"
onClick={() => {
console.log("Sort to accept");
onClose?.();
}}
>
Sort to Accept
{isSubmitting ? "Submitting..." : "Submit QC"}
</Button>
<Button
variant="contained"
variant="outlined"
onClick={() => {
console.log("Reject and pick another lot");
onClose?.();
closeHandler?.({}, 'escapeKeyDown');
}}
>
Reject and Pick Another Lot
Cancel
</Button>
</Stack>
</Grid>
@@ -332,4 +498,4 @@ const PickQcStockInModalVer2: React.FC<Props> = ({
);
};

export default PickQcStockInModalVer2;
export default PickQcStockInModalVer3;

+ 481
- 170
src/components/PickOrderSearch/assignTo.tsx Просмотреть файл

@@ -4,93 +4,381 @@ import {
Box,
Button,
CircularProgress,
FormControl,
Grid,
Stack,
Modal,
TextField,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Checkbox,
TablePagination,
} from "@mui/material";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults/SearchResults";
import { fetchConsoPickOrderClient } from "@/app/api/pickOrder/actions";
import {
newassignPickOrder,
AssignPickOrderInputs,
releaseAssignedPickOrders,
} from "@/app/api/pickOrder/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import { isEmpty, upperFirst } from "lodash";
import { arrayToDateString } from "@/app/utils/formatUtil";
import {
FormProvider,
useForm,
} from "react-hook-form";
import { isEmpty, upperFirst, groupBy } from "lodash";
import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
import useUploadContext from "../UploadProvider/useUploadContext";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import SearchBox, { Criterion } from "../SearchBox";
import { sortBy, uniqBy } from "lodash";
import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions";

dayjs.extend(arraySupport);

interface Props {
filterArgs: Record<string, any>;
}

interface AssignmentData {
// 使用 fetchPickOrderItemsByPageClient 返回的数据结构
interface ItemRow {
id: string;
consoCode: string;
releasedDate: string | null;
pickOrderId: number;
pickOrderCode: string;
itemId: number;
itemCode: string;
itemName: string;
requiredQty: number;
currentStock: number;
unit: string;
targetDate: any;
status: string;
consoCode?: string;
assignTo?: number;
groupName?: string;
}

// 分组后的数据结构
interface GroupedItemRow {
pickOrderId: number;
pickOrderCode: string;
targetDate: any;
status: string;
assignTo: number | null;
assignedUserName?: string;
consoCode?: string;
items: ItemRow[];
}

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
pt: 5,
px: 5,
pb: 10,
width: { xs: "100%", sm: "100%", md: "100%" },
};

const AssignTo: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder");
// State
const [assignmentData, setAssignmentData] = useState<AssignmentData[]>([]);
const [isLoading, setIsLoading] = useState(false);
const { setIsUploading } = useUploadContext();

// 修复:选择状态改为按 pick order ID 存储
const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<number[]>([]);
const [filteredItems, setFilteredItems] = useState<ItemRow[]>([]);
const [isLoadingItems, setIsLoadingItems] = useState(false);
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 50,
pageSize: 10,
});
const [totalCount, setTotalCount] = useState<number>();
const [totalCountItems, setTotalCountItems] = useState<number>();
const [modalOpen, setModalOpen] = useState(false);
const [usernameList, setUsernameList] = useState<NameList[]>([]);
const [selectedUser, setSelectedUser] = useState<NameList | null>(null);
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [originalItemData, setOriginalItemData] = useState<ItemRow[]>([]);

const formProps = useForm<AssignPickOrderInputs>();
const errors = formProps.formState.errors;

// 将项目按 pick order 分组
const groupedItems = useMemo(() => {
const grouped = groupBy(filteredItems, 'pickOrderId');
return Object.entries(grouped).map(([pickOrderId, items]) => {
const firstItem = items[0];
return {
pickOrderId: parseInt(pickOrderId),
pickOrderCode: firstItem.pickOrderCode,
targetDate: firstItem.targetDate,
status: firstItem.status,
consoCode: firstItem.consoCode,
items: items,
groupName: firstItem.groupName,
} as GroupedItemRow;
});
}, [filteredItems]);

// 修复:处理 pick order 选择
const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => {
if (checked) {
setSelectedPickOrderIds(prev => [...prev, pickOrderId]);
} else {
setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId));
}
}, []);

// 修复:检查 pick order 是否被选中
const isPickOrderSelected = useCallback((pickOrderId: number) => {
return selectedPickOrderIds.includes(pickOrderId);
}, [selectedPickOrderIds]);

// 使用 fetchPickOrderItemsByPageClient 获取数据
const fetchNewPageItems = useCallback(
async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => {
console.log("=== fetchNewPageItems called ===");
console.log("pagingController:", pagingController);
console.log("filterArgs:", filterArgs);
setIsLoadingItems(true);
try {
const params = {
...pagingController,
...filterArgs,
// 新增:只获取状态为 "assigned" 的提料单
status: "assigned"
};
console.log("Final params:", params);

const res = await fetchPickOrderItemsByPageClient(params);
console.log("API Response:", res);

if (res && res.records) {
console.log("Records received:", res.records.length);
console.log("First record:", res.records[0]);
const itemRows: ItemRow[] = res.records.map((item: any) => ({
id: item.id,
pickOrderId: item.pickOrderId,
pickOrderCode: item.pickOrderCode,
itemId: item.itemId,
itemCode: item.itemCode,
itemName: item.itemName,
requiredQty: item.requiredQty,
currentStock: item.currentStock ?? 0,
unit: item.unit,
targetDate: item.targetDate,
status: item.status,
consoCode: item.consoCode,
assignTo: item.assignTo,
}));
setOriginalItemData(itemRows);
setFilteredItems(itemRows);
setTotalCountItems(res.total);
} else {
console.log("No records in response");
setFilteredItems([]);
setTotalCountItems(0);
}
} catch (error) {
console.error("Error fetching items:", error);
setFilteredItems([]);
setTotalCountItems(0);
} finally {
setIsLoadingItems(false);
}
},
[],
);

// 新增:处理 Release 操作(包含完整的库存管理)
const handleRelease = useCallback(async () => {
if (selectedPickOrderIds.length === 0) return;

// Fetch assignment data
const fetchAssignmentData = useCallback(async () => {
setIsLoading(true);
setIsUploading(true);
try {
const params = {
...pagingController,
...filterArgs,
// Add user filter if selected
...(selectedUser && { assignTo: selectedUser.id }),
};
// 调用新的 release API,包含完整的库存管理功能
const releaseRes = await releaseAssignedPickOrders({
pickOrderIds: selectedPickOrderIds,
assignTo: 0, // 这个参数在 release 时不会被使用
});

if (releaseRes && releaseRes.code === "SUCCESS") {
console.log("Release successful with inventory management:", releaseRes);
setSelectedPickOrderIds([]); // 清空选择
fetchNewPageItems(pagingController, filterArgs);
} else {
console.error("Release failed:", releaseRes);
}
} catch (error) {
console.error("Error in release:", error);
} finally {
setIsUploading(false);
}
}, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]);

const searchCriteria: Criterion<any>[] = useMemo(
() => [
{
label: t("Pick Order Code"),
paramName: "pickOrderCode",
type: "text",
},
{
label: t("Item Code"),
paramName: "itemCode",
type: "text"
},
{
label: t("Item Name"),
paramName: "itemName",
type: "text",
},
{
label: t("Target Date From"),
label2: t("Target Date To"),
paramName: "targetDate",
type: "dateRange",
},
],
[t],
);

const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);

const filtered = originalItemData.filter((item) => {
const itemTargetDateStr = arrayToDayjs(item.targetDate);

const itemCodeMatch = !query.itemCode ||
item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
console.log("Fetching with params:", params);
const itemNameMatch = !query.itemName ||
item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
const res = await fetchConsoPickOrderClient(params);
if (res) {
console.log("API response:", res);
// Enhance data with user names and add id
const enhancedData = res.records.map((record: any, index: number) => {
const userName = record.assignTo
? usernameList.find(user => user.id === record.assignTo)?.name
: null;
return {
...record,
id: record.consoCode || `temp-${index}`,
assignedUserName: userName || 'Unassigned',
};
});
setAssignmentData(enhancedData);
setTotalCount(res.total);
const pickOrderCodeMatch = !query.pickOrderCode ||
item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
// 日期范围搜索
let dateMatch = true;
if (query.targetDate || query.targetDateTo) {
try {
if (query.targetDate && !query.targetDateTo) {
const fromDate = dayjs(query.targetDate);
dateMatch = itemTargetDateStr.isSame(fromDate, 'day') ||
itemTargetDateStr.isAfter(fromDate, 'day');
} else if (!query.targetDate && query.targetDateTo) {
const toDate = dayjs(query.targetDateTo);
dateMatch = itemTargetDateStr.isSame(toDate, 'day') ||
itemTargetDateStr.isBefore(toDate, 'day');
} else if (query.targetDate && query.targetDateTo) {
const fromDate = dayjs(query.targetDate);
const toDate = dayjs(query.targetDateTo);
dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') ||
itemTargetDateStr.isAfter(fromDate, 'day')) &&
(itemTargetDateStr.isSame(toDate, 'day') ||
itemTargetDateStr.isBefore(toDate, 'day'));
}
} catch (error) {
console.error("Date parsing error:", error);
dateMatch = true;
}
}
const statusMatch = !query.status ||
query.status.toLowerCase() === "all" ||
item.status?.toLowerCase().includes((query.status || "").toLowerCase());

return itemCodeMatch && itemNameMatch && pickOrderCodeMatch && dateMatch && statusMatch;
});
console.log("Filtered items count:", filtered.length);
setFilteredItems(filtered);
}, [originalItemData]);

const handleReset = useCallback(() => {
setSearchQuery({});
setFilteredItems(originalItemData);
setTimeout(() => {
setSearchQuery({});
}, 0);
}, [originalItemData]);

// 修复:处理分页变化
const handlePageChange = useCallback((event: unknown, newPage: number) => {
const newPagingController = {
...pagingController,
pageNum: newPage + 1, // API 使用 1-based 分页
};
setPagingController(newPagingController);
}, [pagingController]);

const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10);
const newPagingController = {
pageNum: 1, // 重置到第一页
pageSize: newPageSize,
};
setPagingController(newPagingController);
}, []);

const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => {
if (selectedPickOrderIds.length === 0) return;

setIsUploading(true);
try {
// 修复:直接使用选中的 pick order IDs
const assignRes = await newassignPickOrder({
pickOrderIds: selectedPickOrderIds,
assignTo: data.assignTo,
});

if (assignRes && assignRes.code === "SUCCESS") {
console.log("Assign successful:", assignRes);
setModalOpen(false);
setSelectedPickOrderIds([]); // 清空选择
fetchNewPageItems(pagingController, filterArgs);
} else {
console.error("Assign failed:", assignRes);
}
} catch (error) {
console.error("Error fetching assignment data:", error);
console.error("Error in assign:", error);
} finally {
setIsLoading(false);
setIsUploading(false);
}
}, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]);

const openAssignModal = useCallback(() => {
setModalOpen(true);
formProps.reset();
}, [formProps]);

// 组件挂载时加载数据
useEffect(() => {
console.log("=== Component mounted ===");
fetchNewPageItems(pagingController, filterArgs || {});
}, []); // 只在组件挂载时执行一次

// 当 pagingController 或 filterArgs 变化时重新调用 API
useEffect(() => {
console.log("=== Dependencies changed ===");
if (pagingController && (filterArgs || {})) {
fetchNewPageItems(pagingController, filterArgs || {});
}
}, [pagingController, filterArgs, selectedUser, usernameList]);
}, [pagingController, filterArgs, fetchNewPageItems]);

// Load username list
useEffect(() => {
const loadUsernameList = async () => {
try {
const res = await fetchNameList();
if (res) {
console.log("Loaded username list:", res);
setUsernameList(res);
}
} catch (error) {
@@ -100,134 +388,157 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => {
loadUsernameList();
}, []);

// Fetch data when dependencies change
useEffect(() => {
fetchAssignmentData();
}, [fetchAssignmentData]);

// Handle user selection
const handleUserChange = useCallback((event: any, newValue: NameList | null) => {
setSelectedUser(newValue);
// Reset to first page when filtering
setPagingController(prev => ({ ...prev, pageNum: 1 }));
}, []);
// 自定义分组表格组件
const CustomGroupedTable = () => {
// 获取用户名的辅助函数
const getUserName = useCallback((assignToId: number | null | undefined) => {
if (!assignToId) return '-';
const user = usernameList.find(u => u.id === assignToId);
return user ? user.name : `User ${assignToId}`;
}, [usernameList]);

// Clear filter
const handleClearFilter = useCallback(() => {
setSelectedUser(null);
setPagingController(prev => ({ ...prev, pageNum: 1 }));
}, []);
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Group Name")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell align="right">{t("Order Quantity")}</TableCell>
<TableCell align="right">{t("Current Stock")}</TableCell>
<TableCell align="right">{t("Stock Unit")}</TableCell>
<TableCell>{t("Target Date")}</TableCell>
<TableCell>{t("Assigned To")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{groupedItems.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
groupedItems.map((group) => (
group.items.map((item, index) => (
<TableRow key={item.id}>
{/* Checkbox - 只在第一个项目显示,按 pick order 选择 */}
<TableCell>
{index === 0 ? (
<Checkbox
checked={isPickOrderSelected(group.pickOrderId)}
onChange={(e) => handlePickOrderSelect(group.pickOrderId, e.target.checked)}
disabled={!isEmpty(item.consoCode)}
/>
) : null}
</TableCell>
{/* Pick Order Code - 只在第一个项目显示 */}
<TableCell>
{index === 0 ? item.pickOrderCode : null}
</TableCell>
{/* Group Name */}
<TableCell>
{index === 0 ? (item.groupName || "No Group") : null}
</TableCell>
{/* Item Code */}
<TableCell>{item.itemCode}</TableCell>
{/* Item Name */}
<TableCell>{item.itemName}</TableCell>

// Columns definition
const columns = useMemo<Column<AssignmentData>[]>(
() => [
{
name: "consoCode",
label: t("Consolidated Code"),
},
{
name: "assignedUserName",
label: t("Assigned To"),
renderCell: (params) => {
if (!params.assignTo) {
return (
<Typography variant="body2" color="text.secondary">
{t("Unassigned")}
</Typography>
);
}
return (
<Typography variant="body2" color="primary">
{params.assignedUserName}
</Typography>
);
},
},
{
name: "status",
label: t("Status"),
renderCell: (params) => {
return upperFirst(params.status);
},
},
{
name: "releasedDate",
label: t("Released Date"),
renderCell: (params) => {
if (!params.releasedDate) {
return (
<Typography variant="body2" color="text.secondary">
{t("Not Released")}
</Typography>
);
{/* Order Quantity */}
<TableCell align="right">{item.requiredQty}</TableCell>
{/* Current Stock */}
<TableCell align="right">
<Typography
variant="body2"
color={item.currentStock > 0 ? "success.main" : "error.main"}
sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }}
>
{item.currentStock.toLocaleString()}
</Typography>
</TableCell>
{/* Unit */}
<TableCell align="right">{item.unit}</TableCell>
{/* Target Date - 只在第一个项目显示 */}
<TableCell>
{index === 0 ? (
arrayToDayjs(item.targetDate)
.add(-1, "month")
.format(OUTPUT_DATE_FORMAT)
) : null}
</TableCell>
{/* Assigned To - 只在第一个项目显示,显示用户名 */}
<TableCell>
{index === 0 ? (
<Typography variant="body2">
{getUserName(item.assignTo)}
</Typography>
) : null}
</TableCell>
</TableRow>
))
))
)}
</TableBody>
</Table>
</TableContainer>
{/* 修复:添加分页组件 */}
<TablePagination
component="div"
count={totalCountItems || 0}
page={(pagingController.pageNum - 1)} // 转换为 0-based
rowsPerPage={pagingController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
return arrayToDateString(params.releasedDate);
},
},
],
[t],
);
/>
</>
);
};

return (
<Stack spacing={2}>
{/* Filter Section */}
<Grid container spacing={2}>
<Grid item xs={4}>
<Autocomplete
options={usernameList}
getOptionLabel={(option) => option.name}
value={selectedUser}
onChange={handleUserChange}
renderInput={(params) => (
<TextField
{...params}
label={t("Select User to Filter")}
variant="outlined"
fullWidth
/>
)}
renderOption={(props, option) => (
<Box component="li" {...props}>
<Typography variant="body2">
{option.name} (ID: {option.id})
</Typography>
</Box>
)}
/>
</Grid>
<Grid item xs={2}>
<Button
variant="outlined"
onClick={handleClearFilter}
disabled={!selectedUser}
>
{t("Clear Filter")}
</Button>
</Grid>
</Grid>

{/* Data Table - Match PickExecution exactly */}
<Grid container spacing={2} sx={{ height: '100%', flex: 1 }}>
<Grid item xs={12} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{isLoading ? (
<Box display="flex" justifyContent="center" alignItems="center" flex={1}>
<CircularProgress size={40} />
</Box>
<>
<SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
<Grid container rowGap={1}>
<Grid item xs={12}>
{isLoadingItems ? (
<CircularProgress size={40} />
) : (
<SearchResults<AssignmentData>
items={assignmentData}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={totalCount}
/>
<CustomGroupedTable />
)}
</Grid>
<Grid item xs={12}>
<Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}>
<Button
disabled={selectedPickOrderIds.length < 1}
variant="outlined"
onClick={handleRelease}
>
{t("Release")}
</Button>
</Box>
</Grid>
</Grid>
</Stack>
</>
);
};

export default AssignTo;


export default AssignTo;

+ 1116
- 546
src/components/PickOrderSearch/newcreatitem.tsx
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 36
- 11
src/i18n/zh/pickOrder.json Просмотреть файл

@@ -7,9 +7,10 @@
"Details": "詳情",
"Supplier": "供應商",
"Status": "來貨狀態",
"Release Pick Orders": "放單",
"Escalated": "上報狀態",
"NotEscalated": "無上報",
"Assigned To": "已分配",
"Do you want to start?": "確定開始嗎?",
"Start": "開始",
"Start Success": "開始成功",
@@ -86,7 +87,7 @@
"Please scan warehouse qr code.": "請掃描倉庫 QR 碼。",

"Reject": "拒絕",
"submit": "提交",
"submit": "確認提交",
"print": "列印",
"bind": "綁定",

@@ -101,7 +102,7 @@
"Consolidated Pick Orders": "合併提料單",
"Pick Order No.": "提料單編號",
"Pick Order Date": "提料單日期",
"Pick Order Status": "提料單狀態",
"Pick Order Status": "提狀態",
"Pick Order Type": "提料單類型",
"Consolidated Code": "合併編號",
"type": "類型",
@@ -111,7 +112,7 @@
"Target Date From": "目標日期從",
"Target Date To": "目標日期到",
"Consolidate": "合併",
"Stock Unit": "庫存單位",
"create": "新增",
"detail": "詳情",
"Pick Order Detail": "提料單詳情",
@@ -130,20 +131,44 @@
"lot change": "批次變更",
"checkout": "出庫",
"Search Items": "搜尋貨品",
"Search Results": "搜尋結果",
"Search Results": "可選擇貨品",
"Second Search Results": "第二搜尋結果",
"Second Search Items": "第二搜尋項目",
"Second Search": "第二搜尋",
"Item": "貨品",
"Order Quantity": "求數量",
"Current Stock": "現時庫存",
"Order Quantity": "貨品需求數量",
"Current Stock": "現時可用庫存",
"Selected": "已選擇",
"Select Items": "選擇貨品",
"Assign": "分派提料單",
"Release": "放單",
"Pick Execution": "進行提料",
"Create Pick Order": "建立貨品提料單",
"Consumable": "食材",
"Material": "材料",
"Job Order": "工單"
}
"Consumable": "消耗品",
"Material": "食材",
"Job Order": "工單",
"End Product": "成品",
"Lot Expiry Date": "批號到期日",
"Lot Location": "批號位置",
"Available Lot": "批號可用提料數量",
"Lot Required Pick Qty": "批號所需提料數量",
"Lot Actual Pick Qty": "批號實際提料數量",
"Lot#": "批號",
"Submit": "提交",
"Created Items": "已建立貨品",
"Create New Group": "建立新分組",
"Group": "分組",
"Qty Already Picked": "已提料數量",
"Select Job Order Items": "選擇工單貨品",
"failedQty": "不合格項目數量",
"remarks": "備註",
"Qc items": "QC 項目",
"qcItem": "QC 項目",
"QC Info": "QC 資訊",
"qcResult": "QC 結果",
"acceptQty": "接受數量",
"Escalation History": "上報歷史",
"Group Name": "分組名稱",
"Job Order Code": "工單編號"

}

Загрузка…
Отмена
Сохранить