|
- "use client";
-
- import { DoResult } from "@/app/api/do";
- import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo ,startBatchReleaseAsync, getBatchReleaseProgress} from "@/app/api/do/actions";
-
- import { useRouter } from "next/navigation";
- import React, { ForwardedRef, useCallback, useEffect, useMemo, useState } from "react";
- import { useTranslation } from "react-i18next";
- import { Criterion } from "../SearchBox";
- import { isEmpty, sortBy, uniqBy, upperFirst } from "lodash";
- import { arrayToDateString, arrayToDayjs } from "@/app/utils/formatUtil";
- import SearchBox from "../SearchBox/SearchBox";
- import { EditNote } from "@mui/icons-material";
- import InputDataGrid from "../InputDataGrid";
- import { CreateConsoDoInput } from "@/app/api/do/actions";
- import { TableRow } from "../InputDataGrid/InputDataGrid";
- import {
- FooterPropsOverrides,
- GridColDef,
- GridRowModel,
- GridToolbarContainer,
- useGridApiRef,
- } from "@mui/x-data-grid";
- import {
- FormProvider,
- SubmitErrorHandler,
- SubmitHandler,
- useForm,
- } from "react-hook-form";
- import { Box, Button, Grid, Stack, Typography, TablePagination} from "@mui/material";
- import StyledDataGrid from "../StyledDataGrid";
- import { GridRowSelectionModel } from "@mui/x-data-grid";
- import Swal from "sweetalert2";
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
-
- type Props = {
- filterArgs?: Record<string, any>;
- searchQuery?: Record<string, any>;
- onDeliveryOrderSearch: () => void;
- };
- type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" , string>;
- type SearchParamNames = keyof SearchBoxInputs;
-
- // put all this into a new component
- // ConsoDoForm
- type EntryError =
- | {
- [field in keyof DoResult]?: string;
- }
- | undefined;
- type DoRow = TableRow<Partial<DoResult>, EntryError>;
-
-
- const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSearch}) => {
- const apiRef = useGridApiRef();
-
- const formProps = useForm<CreateConsoDoInput>({
- defaultValues: {},
- });
- const errors = formProps.formState.errors;
-
-
- const { t } = useTranslation("do");
- const router = useRouter();
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
- console.log("🔍 DoSearch - session:", session);
- console.log("🔍 DoSearch - currentUserId:", currentUserId);
- const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
- const [rowSelectionModel, setRowSelectionModel] =
- useState<GridRowSelectionModel>([]);
-
- const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]);
- const [totalCount, setTotalCount] = useState(0);
-
- const [pagingController, setPagingController] = useState({
- pageNum: 1,
- pageSize: 10,
- });
-
- const [currentSearchParams, setCurrentSearchParams] = useState<SearchBoxInputs>({
- code: "",
- status: "",
- estimatedArrivalDate: "",
- orderDate: "",
- supplierName: "",
- shopName: "",
- deliveryOrderLines: "",
- codeTo: "",
- statusTo: "",
- estimatedArrivalDateTo: "",
- orderDateTo: "",
- supplierNameTo: "",
- shopNameTo: "",
- deliveryOrderLinesTo: ""
- });
-
- const [hasSearched, setHasSearched] = useState(false);
- const [hasResults, setHasResults] = useState(false);
-
- // 当搜索条件变化时,重置到第一页
- useEffect(() => {
- setPagingController(p => ({
- ...p,
- pageNum: 1,
- }));
- }, [currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate]);
-
-
- const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
- () => [
- { label: t("Code"), paramName: "code", type: "text" },
- { label: t("Shop Name"), paramName: "shopName", type: "text" },
- {
- label: t("Estimated Arrival"),
- paramName: "estimatedArrivalDate",
- type: "date",
- },
- {
- label: t("Status"),
- paramName: "status",
- type: "autocomplete",
- options:[
- {label: t('Pending'), value: 'pending'},
- {label: t('Receiving'), value: 'receiving'},
- {label: t('Completed'), value: 'completed'}
- ]
- }
- ],
- [t],
- );
-
- const onReset = useCallback(async () => {
- try {
- setSearchAllDos([]);
- setTotalCount(0);
- setHasSearched(false);
- setHasResults(false);
- setPagingController({ pageNum: 1, pageSize: 10 });
- }
- catch (error) {
- console.error("Error: ", error);
- setSearchAllDos([]);
- setTotalCount(0);
- }
- }, []);
-
- const onDetailClick = useCallback(
- (doResult: DoResult) => {
- if (typeof window !== 'undefined') {
- sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams));
- }
- router.push(`/do/edit?id=${doResult.id}`);
- },
- [router, currentSearchParams],
- );
-
- const validationTest = useCallback(
- (
- newRow: GridRowModel<DoRow>,
- ): EntryError => {
- const error: EntryError = {};
- console.log(newRow);
- return Object.keys(error).length > 0 ? error : undefined;
- },
- [],
- );
-
- const columns = useMemo<GridColDef[]>(
- () => [
- {
- field: "id",
- headerName: t("Details"),
- width: 100,
- renderCell: (params) => (
- <Button
- variant="outlined"
- size="small"
- startIcon={<EditNote />}
- onClick={() => onDetailClick(params.row)}
- >
- {t("Details")}
- </Button>
- ),
- },
- {
- field: "code",
- headerName: t("code"),
- flex: 1.5,
- },
- {
- field: "shopName",
- headerName: t("Shop Name"),
- flex: 1,
- },
- {
- field: "supplierName",
- headerName: t("Supplier Name"),
- flex: 1,
- },
- {
- field: "orderDate",
- headerName: t("Order Date"),
- flex: 1,
- renderCell: (params) => {
- return params.row.orderDate
- ? arrayToDateString(params.row.orderDate)
- : "N/A";
- },
- },
- {
- field: "estimatedArrivalDate",
- headerName: t("Estimated Arrival"),
- flex: 1,
- renderCell: (params) => {
- return params.row.estimatedArrivalDate
- ? arrayToDateString(params.row.estimatedArrivalDate)
- : "N/A";
- },
- },
- {
- field: "status",
- headerName: t("Status"),
- flex: 1,
- renderCell: (params) => {
- return t(upperFirst(params.row.status));
- },
- },
- ],
- [t, arrayToDateString, onDetailClick],
- );
-
- const onSubmit = useCallback<SubmitHandler<CreateConsoDoInput>>(
- async (data, event) => {
- const hasErrors = false;
- console.log(errors);
- },
- [errors],
- );
- const onSubmitError = useCallback<SubmitErrorHandler<CreateConsoDoInput>>(
- (errors) => {},
- [],
- );
-
- //SEARCH FUNCTION
- const handleSearch = useCallback(async (query: SearchBoxInputs) => {
- try {
- setCurrentSearchParams(query);
-
- let estArrStartDate = query.estimatedArrivalDate;
- const time = "T00:00:00";
-
- if(estArrStartDate != ""){
- estArrStartDate = query.estimatedArrivalDate + time;
- }
-
- let status = "";
- if(query.status == "All"){
- status = "";
- }
- else{
- status = query.status;
- }
-
- // 调用新的 API,传入分页参数
- const response = await fetchDoSearch(
- query.code || "",
- query.shopName || "",
- status,
- "", // orderStartDate - 不再使用
- "", // orderEndDate - 不再使用
- estArrStartDate,
- "", // estArrEndDate - 不再使用
- pagingController.pageNum, // 传入当前页码
- pagingController.pageSize // 传入每页大小
- );
-
- setSearchAllDos(response.records);
- setTotalCount(response.total); // 设置总记录数
- setHasSearched(true);
- setHasResults(response.records.length > 0);
-
- } catch (error) {
- console.error("Error: ", error);
- setSearchAllDos([]);
- setTotalCount(0);
- setHasSearched(true);
- setHasResults(false);
- }
- }, [pagingController]);
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- const savedSearchParams = sessionStorage.getItem('doSearchParams');
-
- if (savedSearchParams) {
- try {
- const params = JSON.parse(savedSearchParams);
- setCurrentSearchParams(params);
-
- // 自动使用保存的搜索条件重新搜索,获取最新数据
- const timer = setTimeout(async () => {
- await handleSearch(params);
- // 搜索完成后,清除 sessionStorage
- if (typeof window !== 'undefined') {
- sessionStorage.removeItem('doSearchParams');
- sessionStorage.removeItem('doSearchResults');
- sessionStorage.removeItem('doSearchHasSearched');
- }
- }, 100);
-
- return () => clearTimeout(timer);
- } catch (e) {
- console.error('Error restoring search state:', e);
- // 如果出错,也清除 sessionStorage
- if (typeof window !== 'undefined') {
- sessionStorage.removeItem('doSearchParams');
- sessionStorage.removeItem('doSearchResults');
- sessionStorage.removeItem('doSearchHasSearched');
- }
- }
- }
- }
- }, [handleSearch]);
-
- const debouncedSearch = useCallback((query: SearchBoxInputs) => {
- if (searchTimeout) {
- clearTimeout(searchTimeout);
- }
-
- const timeout = setTimeout(() => {
- handleSearch(query);
- }, 300);
-
- setSearchTimeout(timeout);
- }, [handleSearch, searchTimeout]);
-
- // 分页变化时重新搜索
- const handlePageChange = useCallback((event: unknown, newPage: number) => {
- const newPagingController = {
- ...pagingController,
- pageNum: newPage + 1,
- };
- setPagingController(newPagingController);
- // 如果已经搜索过,重新搜索
- if (hasSearched && currentSearchParams) {
- // 使用新的分页参数重新搜索
- const searchWithNewPage = async () => {
- try {
- let estArrStartDate = currentSearchParams.estimatedArrivalDate;
- const time = "T00:00:00";
-
- if(estArrStartDate != ""){
- estArrStartDate = currentSearchParams.estimatedArrivalDate + time;
- }
-
- let status = "";
- if(currentSearchParams.status == "All"){
- status = "";
- }
- else{
- status = currentSearchParams.status;
- }
-
- const response = await fetchDoSearch(
- currentSearchParams.code || "",
- currentSearchParams.shopName || "",
- status,
- "",
- "",
- estArrStartDate,
- "",
- newPagingController.pageNum,
- newPagingController.pageSize
- );
-
- setSearchAllDos(response.records);
- setTotalCount(response.total);
- } catch (error) {
- console.error("Error: ", error);
- }
- };
- searchWithNewPage();
- }
- }, [pagingController, hasSearched, currentSearchParams]);
-
- const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
- const newPageSize = parseInt(event.target.value, 10);
- const newPagingController = {
- pageNum: 1, // 改变每页大小时重置到第一页
- pageSize: newPageSize,
- };
- setPagingController(newPagingController);
- // 如果已经搜索过,重新搜索
- if (hasSearched && currentSearchParams) {
- const searchWithNewPageSize = async () => {
- try {
- let estArrStartDate = currentSearchParams.estimatedArrivalDate;
- const time = "T00:00:00";
-
- if(estArrStartDate != ""){
- estArrStartDate = currentSearchParams.estimatedArrivalDate + time;
- }
-
- let status = "";
- if(currentSearchParams.status == "All"){
- status = "";
- }
- else{
- status = currentSearchParams.status;
- }
-
- const response = await fetchDoSearch(
- currentSearchParams.code || "",
- currentSearchParams.shopName || "",
- status,
- "",
- "",
- estArrStartDate,
- "",
- 1, // 重置到第一页
- newPageSize
- );
-
- setSearchAllDos(response.records);
- setTotalCount(response.total);
- } catch (error) {
- console.error("Error: ", error);
- }
- };
- searchWithNewPageSize();
- }
- }, [hasSearched, currentSearchParams]);
-
- const handleBatchRelease = useCallback(async () => {
- try {
- // 根据当前搜索条件获取所有匹配的记录(不分页)
- let estArrStartDate = currentSearchParams.estimatedArrivalDate;
- const time = "T00:00:00";
-
- if(estArrStartDate != ""){
- estArrStartDate = currentSearchParams.estimatedArrivalDate + time;
- }
-
- let status = "";
- if(currentSearchParams.status == "All"){
- status = "";
- }
- else{
- status = currentSearchParams.status;
- }
-
- // 显示加载提示
- const loadingSwal = Swal.fire({
- title: t("Loading"),
- text: t("Fetching all matching records..."),
- allowOutsideClick: false,
- allowEscapeKey: false,
- showConfirmButton: false,
- didOpen: () => {
- Swal.showLoading();
- }
- });
-
- // 获取所有匹配的记录
- const allMatchingDos = await fetchAllDoSearch(
- currentSearchParams.code || "",
- currentSearchParams.shopName || "",
- status,
- estArrStartDate
- );
-
- Swal.close();
-
- if (allMatchingDos.length === 0) {
- await Swal.fire({
- icon: "warning",
- title: t("No Records"),
- text: t("No matching records found for batch release."),
- confirmButtonText: t("OK")
- });
- return;
- }
-
- // 显示确认对话框
- const result = await Swal.fire({
- icon: "question",
- title: t("Batch Release"),
- html: `
- <div>
- <p>${t("Selected Shop(s): ")}${allMatchingDos.length}</p>
- <p style="font-size: 0.9em; color: #666; margin-top: 8px;">
-
- ${currentSearchParams.code ? `${t("Code")}: ${currentSearchParams.code} ` : ""}
- ${currentSearchParams.shopName ? `${t("Shop Name")}: ${currentSearchParams.shopName} ` : ""}
- ${currentSearchParams.estimatedArrivalDate ? `${t("Estimated Arrival")}: ${currentSearchParams.estimatedArrivalDate} ` : ""}
- ${status ? `${t("Status")}: ${status} ` : ""}
- </p>
- </div>
- `,
- showCancelButton: true,
- confirmButtonText: t("Confirm"),
- cancelButtonText: t("Cancel"),
- confirmButtonColor: "#8dba00",
- cancelButtonColor: "#F04438"
- });
-
- if (result.isConfirmed) {
- const idsToRelease = allMatchingDos.map(d => d.id);
-
- try {
- const startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 });
- const jobId = startRes?.entity?.jobId;
-
- if (!jobId) {
- await Swal.fire({ icon: "error", title: t("Error"), text: t("Failed to start batch release") });
- return;
- }
-
- const progressSwal = Swal.fire({
- title: t("Releasing"),
- text: "0% (0 / 0)",
- allowOutsideClick: false,
- allowEscapeKey: false,
- showConfirmButton: false,
- didOpen: () => {
- Swal.showLoading();
- }
- });
-
- const timer = setInterval(async () => {
- try {
- const p = await getBatchReleaseProgress(jobId);
-
- const e = p?.entity || {};
- const total = e.total ?? 0;
- const finished = e.finished ?? 0;
- const percentage = total > 0 ? Math.round((finished / total) * 100) : 0;
-
- const textContent = document.querySelector('.swal2-html-container');
- if (textContent) {
- textContent.textContent = `${percentage}% (${finished} / ${total})`;
- }
-
- if (p.code === "FINISHED" || e.running === false) {
- clearInterval(timer);
- await new Promise(resolve => setTimeout(resolve, 500));
- Swal.close();
-
- await Swal.fire({
- icon: "success",
- title: t("Completed"),
- text: t("Batch release completed successfully."),
- confirmButtonText: t("Confirm"),
- confirmButtonColor: "#8dba00"
- });
-
- if (currentSearchParams && Object.keys(currentSearchParams).length > 0) {
- await handleSearch(currentSearchParams);
- }
- }
- } catch (err) {
- console.error("progress poll error:", err);
- }
- }, 800);
- } catch (error) {
- console.error("Batch release error:", error);
- await Swal.fire({
- icon: "error",
- title: t("Error"),
- text: t("An error occurred during batch release"),
- confirmButtonText: t("OK")
- });
- }
- }
- } catch (error) {
- console.error("Error fetching all matching records:", error);
- await Swal.fire({
- icon: "error",
- title: t("Error"),
- text: t("Failed to fetch matching records"),
- confirmButtonText: t("OK")
- });
- }
- }, [t, currentUserId, currentSearchParams, handleSearch]);
-
- return (
- <>
- <FormProvider {...formProps}>
- <Stack
- spacing={2}
- component="form"
- onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
- >
- <Grid container>
- <Grid item xs={8}>
- <Typography variant="h4" marginInlineEnd={2}>
- {t("Delivery Order")}
- </Typography>
- </Grid>
- <Grid
- item
- xs={4}
- display="flex"
- justifyContent="end"
- alignItems="end"
- >
- <Stack spacing={2} direction="row">
- {hasSearched && hasResults && (
- <Button
- name="batch_release"
- variant="contained"
- onClick={handleBatchRelease}
- >
- {t("Batch Release")}
- </Button>
- )}
- </Stack>
- </Grid>
- </Grid>
-
- <SearchBox
- criteria={searchCriteria}
- onSearch={handleSearch}
- onReset={onReset}
- />
-
- <StyledDataGrid
- rows={searchAllDos}
- columns={columns}
- checkboxSelection
- rowSelectionModel={rowSelectionModel}
- onRowSelectionModelChange={(newRowSelectionModel) => {
- setRowSelectionModel(newRowSelectionModel);
- formProps.setValue("ids", newRowSelectionModel);
- }}
- slots={{
- footer: FooterToolbar,
- noRowsOverlay: NoRowsOverlay,
- }}
- />
-
- <TablePagination
- component="div"
- count={totalCount}
- page={(pagingController.pageNum - 1)}
- rowsPerPage={pagingController.pageSize}
- onPageChange={handlePageChange}
- onRowsPerPageChange={handlePageSizeChange}
- rowsPerPageOptions={[10, 25, 50]}
- />
-
- </Stack>
- </FormProvider>
- </>
- );
- };
-
-
- const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
- return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
- };
- const NoRowsOverlay: React.FC = () => {
- const { t } = useTranslation("home");
- return (
- <Box
- display="flex"
- justifyContent="center"
- alignItems="center"
- height="100%"
- >
- <Typography variant="caption">{t("Add some entries!")}</Typography>
- </Box>
- );
- };
-
- export default DoSearch;
|