|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- "use client";
-
- import {
- Box,
- Button,
- Chip,
- CircularProgress,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- Paper,
- Stack,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TablePagination,
- TableRow,
- TextField,
- Typography,
- } from "@mui/material";
- import { useCallback, useEffect, useState, useMemo } from "react";
- import { useTranslation } from "react-i18next";
- import SearchBox, { Criterion } from "../SearchBox";
- import SearchResults, { Column } from "../SearchResults";
- import { defaultPagingController } from "../SearchResults/SearchResults";
- import {
- fetchAllPickExecutionIssues,
- PickExecutionIssue,
- updatePickExecutionIssueStatus,
- FetchPickExecutionIssuesParams,
- } from "@/app/api/pickOrder/actions";
- import dayjs from "dayjs";
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
-
- type SearchQuery = {
- issueNo: string;
- pickOrderCode: string;
- itemCode: string;
- lotNo: string;
- type: string;
- status: string;
- };
-
- type SearchParamNames = keyof SearchQuery;
-
- const PickExecutionIssuesTab: React.FC = () => {
- const { t } = useTranslation(["stockTake", "pickOrder"]);
-
- // States
- const [issues, setIssues] = useState<PickExecutionIssue[]>([]);
- const [loading, setLoading] = useState(false);
- const [pagingController, setPagingController] = useState(defaultPagingController);
- const [totalCount, setTotalCount] = useState(0);
-
- // Search inputs
- const defaultInputs = useMemo(() => ({
- issueNo: "",
- pickOrderCode: "",
- itemCode: "",
- lotNo: "",
- type: "",
- status: "",
- }), []);
- const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs);
-
- // Handle dialog
- const [selectedIssue, setSelectedIssue] = useState<PickExecutionIssue | null>(null);
- const [handleDialogOpen, setHandleDialogOpen] = useState(false);
- const [handleRemark, setHandleRemark] = useState("");
-
- // Search criteria
- const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
- () => [
- { label: t("Issue No"), paramName: "issueNo", type: "text" },
- { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text" },
- { label: t("Item Code"), paramName: "itemCode", type: "text" },
- { label: t("Lot No"), paramName: "lotNo", type: "text" },
- {
- label: t("Type"),
- paramName: "type",
- type: "select-labelled",
- options: [
- { value: "jo", label: t("jo") },
- { value: "do", label: t("do") },
- { value: "material", label: t("material") },
- ],
- },
- {
- label: t("Status"),
- paramName: "status",
- type: "select-labelled",
- options: [
- { value: "pending", label: t("pending") },
- { value: "resolved", label: t("resolved") },
- ],
- },
- ],
- [t],
- );
-
- // Fetch issues
- const refetchIssuesData = useCallback(async (
- query: Record<SearchParamNames, string>,
- actionType: "reset" | "search" | "paging" | "init",
- pagingController: typeof defaultPagingController,
- ) => {
- console.log("%c Action Type.", "color:red", actionType);
-
- setLoading(true);
- try {
- // Build API parameters with proper type casting
- let apiParams: FetchPickExecutionIssuesParams | undefined = undefined;
-
- if (query.type && query.type !== "All") {
- // Type assertion to ensure the string is one of the valid types
- const validType = query.type as "jo" | "do" | "material";
- apiParams = { type: validType };
- }
-
- // Call API with type parameter if specified
- const result = await fetchAllPickExecutionIssues(apiParams);
-
- // Apply client-side filtering for other parameters
- let filteredResult = result;
-
- if (query.issueNo) {
- filteredResult = filteredResult.filter(issue =>
- issue.issueNo.toLowerCase().includes(query.issueNo.toLowerCase())
- );
- }
-
- if (query.pickOrderCode) {
- filteredResult = filteredResult.filter(issue =>
- issue.pickOrderCode.toLowerCase().includes(query.pickOrderCode.toLowerCase())
- );
- }
-
- if (query.itemCode) {
- filteredResult = filteredResult.filter(issue =>
- issue.itemCode.toLowerCase().includes(query.itemCode.toLowerCase())
- );
- }
-
- if (query.lotNo) {
- filteredResult = filteredResult.filter(issue =>
- issue.lotNo && issue.lotNo.toLowerCase().includes(query.lotNo.toLowerCase())
- );
- }
-
- if (query.status && query.status !== "All") {
- filteredResult = filteredResult.filter(issue => {
- if (query.status === "pending") {
- return issue.handleStatus.toLowerCase() === "pending";
- }
- if (query.status === "resolved") {
- return issue.handleStatus.toLowerCase() !== "pending";
- }
- return true;
- });
- }
-
- setIssues(filteredResult);
- setTotalCount(filteredResult.length);
-
- } catch (error) {
- console.error("Error fetching pick execution issues:", error);
- setIssues([]);
- setTotalCount(0);
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- refetchIssuesData(defaultInputs, "init", defaultPagingController);
- }, [refetchIssuesData, defaultInputs]);
-
- useEffect(() => {
- refetchIssuesData(inputs, "paging", pagingController);
- }, [pagingController, refetchIssuesData, inputs]);
-
- // Handlers
- const onReset = useCallback(() => {
- refetchIssuesData(defaultInputs, "reset", defaultPagingController);
- setInputs(() => defaultInputs);
- setPagingController(() => defaultPagingController);
- }, [refetchIssuesData, defaultInputs]);
-
- const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
- refetchIssuesData(query, "search", defaultPagingController);
- setInputs(() => query);
- setPagingController(() => defaultPagingController);
- }, [refetchIssuesData]);
-
- const handleViewDetail = (issue: PickExecutionIssue) => {
- setSelectedIssue(issue);
- setHandleDialogOpen(true);
- setHandleRemark("");
- };
-
- const handleCloseDialog = () => {
- setHandleDialogOpen(false);
- setSelectedIssue(null);
- setHandleRemark("");
- };
-
- const handleMarkAsResolved = async () => {
- if (!selectedIssue) return;
-
- try {
- await updatePickExecutionIssueStatus({
- issueId: selectedIssue.id,
- handleStatus: "resolved",
- handleDate: new Date().toISOString(),
- handleRemark: handleRemark,
- });
-
- // Refresh data
- await refetchIssuesData(inputs, "search", pagingController);
- handleCloseDialog();
- } catch (error) {
- console.error("Error updating issue status:", error);
- }
- };
-
- const getIssueCategoryColor = (category: string) => {
- switch (category.toLowerCase()) {
- case "lot_issue":
- return "error";
- case "quality_issue":
- return "warning";
- case "quantity_mismatch":
- return "info";
- default:
- return "default";
- }
- };
-
- // Table columns
- const columns: Column<PickExecutionIssue>[] = useMemo(
- () => [
- { name: "issueNo", label: t("Issue No") },
- { name: "pickOrderCode", label: t("Pick Order") },
- {
- name: "itemCode",
- label: t("Item"),
- renderCell: (params) => (
- <Box>
- <Typography variant="body2">{params.itemCode}</Typography>
- <Typography variant="caption" color="text.secondary">
- {params.itemDescription}
- </Typography>
- </Box>
- ),
- },
- {
- name: "lotNo",
- label: t("Lot No"),
- renderCell: (params) => params.lotNo || "-"
- },
- { name: "requiredQty", label: t("Required Qty"), align: "right", type: "integer" },
- {
- name: "actualPickQty",
- label: t("Actual Qty"),
- align: "right",
- type: "integer",
- renderCell: (params) => params.actualPickQty || 0
- },
- {
- name: "missQty",
- label: t("Miss Qty"),
- align: "right",
- type: "integer",
- renderCell: (params) => params.missQty || 0
- },
- {
- name: "issueCategory",
- label: t("Category"),
- renderCell: (params) => (
- <Chip
- label={t(params.issueCategory)}
- size="small"
- color={getIssueCategoryColor(params.issueCategory)}
- />
- ),
- },
- {
- name: "handleStatus",
- label: t("Status"),
- renderCell: (params) => (
- <Chip
- label={t(params.handleStatus)}
- size="small"
- color={
- params.handleStatus.toLowerCase() === "pending"
- ? "warning"
- : "success"
- }
- />
- ),
- },
- {
- name: "pickExecutionDate",
- label: t("Date"),
- renderCell: (params) => dayjs(params.pickExecutionDate).format(OUTPUT_DATE_FORMAT),
- },
- {
- name: "id",
- label: t("Action"),
- renderCell: (params) => (
- <Button size="small" onClick={() => handleViewDetail(params)}>
- {t("Detail")}
- </Button>
- ),
- },
- ],
- [t, handleViewDetail],
- );
-
- return (
- <Box>
- <SearchBox
- criteria={searchCriteria}
- onSearch={onSearch}
- onReset={onReset}
- />
-
- <SearchResults<PickExecutionIssue>
- items={issues}
- columns={columns}
- pagingController={pagingController}
- setPagingController={setPagingController}
- totalCount={totalCount}
- />
-
- {/* Handle Issue Dialog */}
- <Dialog open={handleDialogOpen} onClose={handleCloseDialog} maxWidth="md" fullWidth>
- <DialogTitle>{t("Issue Detail")}</DialogTitle>
- <DialogContent>
- {selectedIssue && (
- <Stack spacing={2} sx={{ mt: 2 }}>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Issue No")}
- </Typography>
- <Typography variant="body1">{selectedIssue.issueNo}</Typography>
- </Box>
-
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Pick Order")}
- </Typography>
- <Typography variant="body1">{selectedIssue.pickOrderCode}</Typography>
- </Box>
-
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Item")}
- </Typography>
- <Typography variant="body1">
- {selectedIssue.itemCode} - {selectedIssue.itemDescription}
- </Typography>
- </Box>
-
- {selectedIssue.lotNo && (
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Lot No")}
- </Typography>
- <Typography variant="body1">{selectedIssue.lotNo}</Typography>
- </Box>
- )}
-
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Location")}
- </Typography>
- <Typography variant="body1">{selectedIssue.storeLocation || "-"}</Typography>
- </Box>
-
- <Stack direction="row" spacing={3}>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Required Qty")}
- </Typography>
- <Typography variant="body1">{selectedIssue.requiredQty}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Actual Qty")}
- </Typography>
- <Typography variant="body1">{selectedIssue.actualPickQty || 0}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Miss Qty")}
- </Typography>
- <Typography variant="body1" color="error">
- {selectedIssue.missQty || 0}
- </Typography>
- </Box>
- </Stack>
-
- {selectedIssue.issueRemark && (
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Issue Remark")}
- </Typography>
- <Typography variant="body1">{selectedIssue.issueRemark}</Typography>
- </Box>
- )}
-
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Picker")}
- </Typography>
- <Typography variant="body1">{selectedIssue.pickerName || "-"}</Typography>
- </Box>
-
- <Box>
- <Typography variant="subtitle2" color="text.secondary">
- {t("Status")}
- </Typography>
- <Chip
- label={t(selectedIssue.handleStatus)}
- size="small"
- color={
- selectedIssue.handleStatus.toLowerCase() === "pending"
- ? "warning"
- : "success"
- }
- />
- </Box>
-
- {selectedIssue.handleStatus.toLowerCase() === "pending" && (
- <TextField
- label={t("Handle Remark")}
- multiline
- rows={3}
- value={handleRemark}
- onChange={(e) => setHandleRemark(e.target.value)}
- fullWidth
- />
- )}
- </Stack>
- )}
- </DialogContent>
- <DialogActions>
- <Button onClick={handleCloseDialog}>{t("Close")}</Button>
- {selectedIssue && selectedIssue.handleStatus.toLowerCase() === "pending" && (
- <Button onClick={handleMarkAsResolved} variant="contained" color="primary">
- {t("Mark as Resolved")}
- </Button>
- )}
- </DialogActions>
- </Dialog>
- </Box>
- );
- };
-
- export default PickExecutionIssuesTab;
|