|
- "use client";
-
- import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography, TextField, Button, Stack } from "@mui/material";
- import { useState, useCallback, useEffect, useRef } from "react";
- import { useSession } from "next-auth/react";
- import { useTranslation } from "react-i18next";
- import { AUTH } from "@/authorities";
- import { SessionWithTokens } from "@/config/authConfig";
- import { AllPickedStockTakeListReponse, getLatestApproverStockTakeHeader } from "@/app/api/stockTake/actions";
- import PickerCardList from "./PickerCardList";
- import type { PickerCardListFilters } from "./PickerCardList";
- import PickerStockTake from "./PickerStockTake";
- import PickerReStockTake from "./PickerReStockTake";
- import ApproverStockTakeAll from "./ApproverStockTakeAll";
- import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent";
- import {
- parseQtyGapWarnPercent,
- saveStockTakeQtyGapWarnPercent,
- } from "./qtyGapWarnSettingClient";
- import { STOCK_TAKE_QTY_GAP_WARN_PERCENT } from "./stockTakeQtyGapWarning";
-
- type ViewScope = "picker" | "approver-all";
- const DEFAULT_PICKER_CARD_LIST_FILTERS: PickerCardListFilters = {
- sectionDescription: "All",
- stockTakeSession: "",
- status: "All",
- area: "",
- storeId: "All",
- };
-
- const StockTakeTab: React.FC = () => {
- const { t } = useTranslation(["stockTake", "common"]);
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const isAdmin = (session?.abilities ?? session?.user?.abilities ?? []).some(
- (ability) => String(ability).trim() === AUTH.ADMIN,
- );
- const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
- const [qtyGapDraft, setQtyGapDraft] = useState(String(STOCK_TAKE_QTY_GAP_WARN_PERCENT));
- const [qtyGapSaving, setQtyGapSaving] = useState(false);
- const qtyGapSaveLock = useRef(false);
- const [tabValue, setTabValue] = useState(0);
- const [selectedSession, setSelectedSession] = useState<AllPickedStockTakeListReponse | null>(null);
- const [viewMode, setViewMode] = useState<"details" | "reStockTake">("details");
- const [viewScope, setViewScope] = useState<ViewScope>("picker");
- const [approverSession, setApproverSession] = useState<AllPickedStockTakeListReponse | null>(null);
- const [approverLoading, setApproverLoading] = useState(false);
- /** 從卡片列表進入明細後返回時保留分頁 */
- const [pickerListPage, setPickerListPage] = useState(0);
- const [pickerListPageSize] = useState(6);
- const [pickerSearchFilters, setPickerSearchFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS);
- const [pickerAppliedFilters, setPickerAppliedFilters] = useState<PickerCardListFilters>(DEFAULT_PICKER_CARD_LIST_FILTERS);
- const [snackbar, setSnackbar] = useState<{
- open: boolean;
- message: string;
- severity: "success" | "error" | "warning"
- }>({
- open: false,
- message: "",
- severity: "success",
- });
-
- const handleCardClick = useCallback((session: AllPickedStockTakeListReponse) => {
- setSelectedSession(session);
- setViewMode("details");
- }, []);
-
- const handleReStockTakeClick = useCallback((session: AllPickedStockTakeListReponse) => {
- setSelectedSession(session);
- setViewMode("reStockTake");
- setViewScope("picker");
- }, []);
-
- const handleBackToList = useCallback(() => {
- setSelectedSession(null);
- setViewMode("details");
- }, []);
-
- const handleSnackbar = useCallback((message: string, severity: "success" | "error" | "warning") => {
- setSnackbar({
- open: true,
- message,
- severity,
- });
- }, []);
-
- useEffect(() => {
- setQtyGapDraft(String(qtyGapWarnPercent));
- }, [qtyGapWarnPercent]);
-
- const saveQtyGapWarnPercent = useCallback(async () => {
- if (!isAdmin || qtyGapSaveLock.current) return;
- const parsed = Number(qtyGapDraft.trim());
- if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1000) {
- handleSnackbar(t("qtyGapWarnPercentInvalid"), "warning");
- return;
- }
- qtyGapSaveLock.current = true;
- setQtyGapSaving(true);
- try {
- await saveStockTakeQtyGapWarnPercent(parsed);
- setQtyGapDraft(String(parseQtyGapWarnPercent(String(parsed))));
- handleSnackbar(t("qtyGapWarnPercentSaved"), "success");
- } catch (e) {
- handleSnackbar(e instanceof Error ? e.message : t("qtyGapWarnPercentInvalid"), "error");
- } finally {
- qtyGapSaveLock.current = false;
- setQtyGapSaving(false);
- }
- }, [handleSnackbar, isAdmin, qtyGapDraft, t]);
-
- useEffect(() => {
- if (tabValue !== 1 && tabValue !== 2) return;
- setApproverLoading(true);
- getLatestApproverStockTakeHeader()
- .then((header) => {
- setApproverSession(header ?? null);
- })
- .catch((e) => {
- console.error(e);
- setApproverSession(null);
- })
- .finally(() => setApproverLoading(false));
- }, [tabValue]);
-
- if (selectedSession && viewScope === "picker") {
- return (
- <Box>
- {viewScope === "picker" && (
- tabValue === 0 ? (
- viewMode === "reStockTake" ? (
- <PickerReStockTake
- selectedSession={selectedSession}
- onBack={handleBackToList}
- onSnackbar={handleSnackbar}
- />
- ) : (
- <PickerStockTake
- selectedSession={selectedSession}
- onBack={handleBackToList}
- onSnackbar={handleSnackbar}
- />
- )
- ) : null
- )}
- <Snackbar
- open={snackbar.open}
- autoHideDuration={6000}
- onClose={() => setSnackbar({ ...snackbar, open: false })}
- >
- <Alert onClose={() => setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}>
- {snackbar.message}
- </Alert>
- </Snackbar>
- </Box>
- );
- }
-
- return (
- <Box>
- {isAdmin && (
- <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
- <Typography
- component="label"
- htmlFor="stock-take-qty-gap-warn"
- sx={{ m: 0, height: 40, fontSize: 18, fontWeight: 500, lineHeight: "40px" }}
- >
- {t("qtyGapWarnPercent")}
- </Typography>
- <TextField
- id="stock-take-qty-gap-warn"
- size="small"
- type="number"
- value={qtyGapDraft}
- onChange={(e) => setQtyGapDraft(e.target.value.replace(/[^\d]/g, ""))}
- inputProps={{ min: 0, max: 1000, inputMode: "numeric" }}
- sx={{
- width: 88,
- m: 0,
- "& .MuiFilledInput-root": { height: 40 },
- "& .MuiFilledInput-input.MuiInputBase-inputSizeSmall": {
- height: 40,
- boxSizing: "border-box",
- paddingTop: 0,
- paddingBottom: 0,
- lineHeight: "40px",
- },
- }}
- />
- <Button
- size="small"
- variant="outlined"
- disabled={qtyGapSaving}
- onClick={saveQtyGapWarnPercent}
- sx={{ height: 40 }}
- >
- {t("Save")}
- </Button>
- </Stack>
- )}
- <Tabs
- value={tabValue}
- onChange={(e, newValue) => {
- setTabValue(newValue);
- if (newValue === 0) {
- setViewScope("picker");
- } else {
- setViewScope("approver-all");
- }
- }}
- sx={{ mb: 2 }}
- >
- <Tab label={t("Picker")} />
- <Tab label={t("Approver Pending")} />
- <Tab label={t("Approver Approved")} />
- </Tabs>
-
- {tabValue === 0 && (
- <PickerCardList
- page={pickerListPage}
- pageSize={pickerListPageSize}
- onListPageChange={setPickerListPage}
- searchFilters={pickerSearchFilters}
- appliedFilters={pickerAppliedFilters}
- onSearchFiltersChange={setPickerSearchFilters}
- onAppliedFiltersChange={setPickerAppliedFilters}
- onCardClick={(session) => {
- setViewScope("picker");
- handleCardClick(session);
- }}
- onReStockTakeClick={handleReStockTakeClick}
- />
- )}
- {tabValue === 1 && (
- <Box>
- {approverLoading ? (
- <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
- <CircularProgress />
- </Box>
- ) : approverSession ? (
- <ApproverStockTakeAll
- selectedSession={approverSession}
- mode="pending"
- onSnackbar={handleSnackbar}
- />
- ) : (
- <Typography variant="body2" color="text.secondary">
- {t("No data")}
- </Typography>
- )}
- </Box>
- )}
- {tabValue === 2 && (
- <Box>
- {approverSession ? (
- <ApproverStockTakeAll
- selectedSession={approverSession}
- mode="approved"
- onSnackbar={handleSnackbar}
- />
- ) : (
- <Typography variant="body2" color="text.secondary">
- {t("No data")}
- </Typography>
- )}
- </Box>
- )}
-
- <Snackbar
- open={snackbar.open}
- autoHideDuration={6000}
- onClose={() => setSnackbar({ ...snackbar, open: false })}
- >
- <Alert onClose={() => setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}>
- {snackbar.message}
- </Alert>
- </Snackbar>
- </Box>
- );
- };
-
- export default StockTakeTab;
|