Sfoglia il codice sorgente

stock take block, created round created again hide another sldie create

stockTakeUpdate
CANCERYS\kw093 2 settimane fa
parent
commit
29c350ccac
8 ha cambiato i file con 130 aggiunte e 46 eliminazioni
  1. +26
    -0
      src/app/utils/readableServerMessage.ts
  2. +2
    -27
      src/components/InventorySearch/InventoryLotLineTable.tsx
  3. +13
    -2
      src/components/PoDetail/QrModal.tsx
  4. +14
    -2
      src/components/PutAwayScan/PutAwayModal.tsx
  5. +72
    -14
      src/components/StockTakeManagement/PickerCardList.tsx
  6. +1
    -1
      src/components/StockTakeManagement/StockTakeTab.tsx
  7. +1
    -0
      src/i18n/en/stockTake.json
  8. +1
    -0
      src/i18n/zh/stockTake.json

+ 26
- 0
src/app/utils/readableServerMessage.ts Vedi File

@@ -0,0 +1,26 @@
/** Prefer stock-take / API reason over a generic "Server error: 409 …" wrapper. */
export function readableServerMessage(error: unknown, fallback: string): string {
const raw =
error instanceof Error ? error.message : typeof error === "string" ? error : "";
const sentence = raw.match(/盤點進行中,(?:區域\s+\S+|貨品\s+.+?)\s*暫停出入庫/);
if (sentence) return sentence[0];
// Spring CONFLICT often puts the reason in "error" (not "message").
const fieldPattern = /"(?:message|detail|error)"\s*:\s*"((?:\\.|[^"\\])*)"/g;
const fields: string[] = [];
let match: RegExpExecArray | null = fieldPattern.exec(raw);
while (match) {
if (match[1]) fields.push(match[1]);
match = fieldPattern.exec(raw);
}
for (let i = fields.length - 1; i >= 0; i -= 1) {
const text = fields[i]
.replace(/\\"/g, '"')
.replace(/\\n/g, " ")
.replace(/\\u([0-9a-fA-F]{4})/g, (_full: string, hex: string) =>
String.fromCharCode(parseInt(hex, 16)),
)
.trim();
if (text && !text.startsWith("Server error") && !text.startsWith("{")) return text;
}
return fallback;
}

+ 2
- 27
src/components/InventorySearch/InventoryLotLineTable.tsx Vedi File

@@ -35,31 +35,7 @@ import CheckIcon from "@mui/icons-material/Check";
import { submitStockAdjustment, StockAdjustmentLineRequest, fetchLatestAdjustmentRemarks } from "@/app/api/stockAdjustment/actions";
import { useSession } from "next-auth/react";
import { AUTH, hasAbility } from "@/authorities";

function readableServerMessage(error: unknown, fallback: string) {
const raw =
error instanceof Error ? error.message : typeof error === "string" ? error : "";
const sentence = raw.match(/盤點進行中,(?:區域\s+\S+|貨品\s+.+?)\s*暫停出入庫/);
if (sentence) return sentence[0];
const fieldPattern = /"(?:message|detail)"\s*:\s*"((?:\\.|[^"\\])*)"/g;
const fields: string[] = [];
let match: RegExpExecArray | null = fieldPattern.exec(raw);
while (match) {
if (match[1]) fields.push(match[1]);
match = fieldPattern.exec(raw);
}
for (let i = fields.length - 1; i >= 0; i -= 1) {
const text = fields[i]
.replace(/\\"/g, '"')
.replace(/\\n/g, " ")
.replace(/\\u([0-9a-fA-F]{4})/g, (_full: string, hex: string) =>
String.fromCharCode(parseInt(hex, 16)),
)
.trim();
if (text && !text.startsWith("Server error") && !text.startsWith("{")) return text;
}
return fallback;
}
import { readableServerMessage } from "@/app/utils/readableServerMessage";

type AdjustmentEntry = InventoryLotLineResult & {
adjustedQty: number;
@@ -331,8 +307,7 @@ const prevAdjustmentModalOpenRef = useRef(false);
setStockAdjustmentModalOpen(false);
await onStockAdjustmentSuccess?.();
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);
msgError(message || t("Save failed"));
msgError(readableServerMessage(e, t("Save failed")));
} finally {
setIsUploading(false);
setIsAdjustSaving(false);


+ 13
- 2
src/components/PoDetail/QrModal.tsx Vedi File

@@ -29,6 +29,7 @@ import { Check } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "next/navigation";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { readableServerMessage } from "@/app/utils/readableServerMessage";

interface Props extends Omit<ModalProps, "children"> {
warehouse: WarehouseResult[];
@@ -173,6 +174,12 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => {
}
// return;
const res = await updateStockInLine(args);
if (res?.type === "putaway_held" || res?.code === "PUTAWAY_HELD") {
setServerError(
res.message || "盤點進行中,自動上架將於盤點結束後入庫",
);
return;
}
if (Boolean(res.id)) {
// update entries
console.log(res);
@@ -182,8 +189,12 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => {
console.log(res);
// if (res)
} catch (e) {
// server error
setServerError(t("An error has occurred. Please try again later."));
setServerError(
readableServerMessage(
e,
t("An error has occurred. Please try again later."),
),
);
console.log(e);
}
},


+ 14
- 2
src/components/PutAwayScan/PutAwayModal.tsx Vedi File

@@ -39,6 +39,7 @@ import { QrCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { msg, msgError } from "../Swal/CustomAlerts";
import { PutAwayRecord } from ".";
import FgStockInForm from "../StockIn/FgStockInForm";
import { readableServerMessage } from "@/app/utils/readableServerMessage";

import Swal from "sweetalert2";

@@ -449,6 +450,13 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
}
console.log("%c Submitting Data:", "color:blue", args);
const res = await updateStockInLine(args);
if (res?.type === "putaway_held" || res?.code === "PUTAWAY_HELD") {
const heldMsg =
res.message || "盤點進行中,自動上架將於盤點結束後入庫";
setServerError(heldMsg);
msgError(heldMsg);
return;
}
if (Boolean(res.id)) {
// update entries
console.log("%c Update Success:", "color:green", res);
@@ -475,8 +483,12 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
// console.log(res);
// if (res)
} catch (e) {
// server error
setServerError(t("An error has occurred. Please try again later."));
const message = readableServerMessage(
e,
t("An error has occurred. Please try again later."),
);
setServerError(message);
msgError(message);
console.log(e);
}
},


+ 72
- 14
src/components/StockTakeManagement/PickerCardList.tsx Vedi File

@@ -32,8 +32,12 @@ import {
ToggleButton,
ToggleButtonGroup,
Checkbox,
Accordion,
AccordionSummary,
AccordionDetails,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
import { useRouter } from "next/navigation";
import { SessionWithTokens } from "@/config/authConfig";
@@ -318,6 +322,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
dayjs.extend(duration);

const [loading, setLoading] = useState(true);
/** Same UX as Approver: Search Criteria accordion can collapse / expand. */
const [showFilters, setShowFilters] = useState(true);
/** 這一輪全部卡片,篩選在前端做,點選不再重打 API */
const [roundRecords, setRoundRecords] = useState<AllPickedStockTakeListReponse[]>([]);
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -342,7 +348,9 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const [createFloorTabIndex, setCreateFloorTabIndex] = useState(0);
const [createDialogSearchQuery, setCreateDialogSearchQuery] = useState("");
const [createMode, setCreateMode] = useState<"section" | "item">("section");
/** Open item round → hide 「依區域」; open section round → hide 「依貨品」 (symmetric). */
const [blockSectionCreate, setBlockSectionCreate] = useState(false);
const [blockItemCreate, setBlockItemCreate] = useState(false);
const [createDialogItemCode, setCreateDialogItemCode] = useState("");
const [createDialogItemName, setCreateDialogItemName] = useState("");
const [createDialogItemHits, setCreateDialogItemHits] = useState<CreateDialogItemHit[]>([]);
@@ -718,15 +726,28 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
getStockTakeRoundCards()
.then((res) => {
if (cancelled) return;
const blocked = (res.rounds || []).some(
(round) => round.open && round.stockTakeRoundMode === "item",
const openRounds = res.rounds || [];
const blockSection = openRounds.some(
(round) =>
round.open &&
String(round.stockTakeRoundMode ?? "").trim().toLowerCase() === "item",
);
setBlockSectionCreate(blocked);
if (blocked) setCreateMode("item");
const blockItem = openRounds.some((round) => {
if (!round.open) return false;
const mode = String(round.stockTakeRoundMode ?? "section").trim().toLowerCase();
return mode === "section" || mode === "";
});
setBlockSectionCreate(blockSection);
setBlockItemCreate(blockItem);
if (!blockSection) setCreateMode("section");
else if (!blockItem) setCreateMode("item");
})
.catch((e) => {
console.error("Failed to check open item stock take rounds:", e);
if (!cancelled) setBlockSectionCreate(false);
console.error("Failed to check open stock take create modes:", e);
if (!cancelled) {
setBlockSectionCreate(false);
setBlockItemCreate(false);
}
});
return () => {
cancelled = true;
@@ -1070,7 +1091,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
setCreateDialogSelectedItems([]);
setCreateMode("section");
setCreateMode(blockSectionCreate && !blockItemCreate ? "item" : "section");
} catch (e) {
console.error(e);
} finally {
@@ -1078,6 +1099,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
createStockTakeInFlightRef.current = false;
}
}, [
blockItemCreate,
blockSectionCreate,
createDialogPlanStart,
createDialogRoundName,
createDialogSelectedItems,
@@ -1235,7 +1258,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
setOpenConfirmDialog(false);
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
setCreateMode(blockSectionCreate ? "item" : "section");
setCreateMode(blockSectionCreate && !blockItemCreate ? "item" : "section");
setCreateDialogItemCode("");
setCreateDialogItemName("");
setCreateDialogItemHits([]);
@@ -1246,11 +1269,28 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
<Box>
{!hideSectionList && (
<>
<Card elevation={0} sx={{ mb: 2 }}>
<CardContent>
<Typography variant="overline" sx={{ display: "block", mb: 1 }}>
{t("Search Criteria")}
<Accordion
expanded={showFilters}
onChange={(_, expanded) => setShowFilters(expanded)}
elevation={0}
sx={{ mb: 2, border: "1px solid", borderColor: "divider", borderRadius: 1 }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
minHeight: 36,
px: 2,
"&.Mui-expanded": { minHeight: 36 },
"& .MuiAccordionSummary-content, & .MuiAccordionSummary-content.Mui-expanded": { my: 0.75 },
}}
>
<Typography variant="body2">
<strong>{t("Search Criteria")}</strong>
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ pt: 0, pb: 1.5, px: 2 }}>
<Card elevation={0} sx={{ mb: 0, boxShadow: "none" }}>
<CardContent sx={{ px: 0, py: 0, "&:last-child": { pb: 0 } }}>
<Grid container spacing={2} alignItems="flex-start">
<Grid item xs={12}>
<Tabs
@@ -1546,6 +1586,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</CardActions>
</CardContent>
</Card>
</AccordionDetails>
</Accordion>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>

@@ -1728,18 +1770,34 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</Tooltip>
</DialogTitle>
<Box sx={{ px: 2, flexShrink: 0 }}>
{blockSectionCreate && blockItemCreate ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 1.5 }}>
{t("Cannot create while both section and item rounds are open")}
</Typography>
) : (
<Tabs
value={createMode}
value={
createMode === "item" && blockItemCreate
? "section"
: createMode === "section" && blockSectionCreate
? "item"
: createMode
}
onChange={(_, v: "section" | "item") => {
if (!v) return;
if (v === "section" && blockSectionCreate) return;
if (v === "item" && blockItemCreate) return;
setCreateMode(v);
}}
>
{blockSectionCreate ? null : (
<Tab value="section" label={t("Create by section")} />
)}
<Tab value="item" label={t("Create by item")} />
{blockItemCreate ? null : (
<Tab value="item" label={t("Create by item")} />
)}
</Tabs>
)}
</Box>
<DialogContent
dividers


+ 1
- 1
src/components/StockTakeManagement/StockTakeTab.tsx Vedi File

@@ -229,7 +229,7 @@ const StockTakeTab: React.FC = () => {
)
) : null}
<Box sx={{ display: showingPickerDetail ? "none" : "block" }}>
{isAdmin && selectedRound && (tabValue === 1 || tabValue === 2) && (
{isAdmin && selectedRound && tabValue === 1 && (
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
<Typography
component="label"


+ 1
- 0
src/i18n/en/stockTake.json Vedi File

@@ -49,6 +49,7 @@
"Control Time": "Control Time",
"Create by item": "By item",
"Create by section": "By section",
"Cannot create while both section and item rounds are open": "Cannot create a new round while both section and item rounds are open",
"Create by item hint": "Search and select items. This round includes available lots of these items in every floor / warehouse.",
"Create by item search hint": "Enter an item code, an item name, or both. Both must match when filled.",
"Create by item selected count": "{{count}} item(s) selected",


+ 1
- 0
src/i18n/zh/stockTake.json Vedi File

@@ -130,6 +130,7 @@
"Start Stock Take Date": "盤點日期",
"Create by item": "依貨品",
"Create by section": "依區域",
"Cannot create while both section and item rounds are open": "目前同時有開著的依區域與依貨品盤點輪,無法再建立新輪",
"Create by item hint": "搜尋並勾選貨品。此輪會納入這些貨在所有樓層/倉的可用庫存。",
"Create by item search hint": "可分別輸入貨品編號、貨品名稱;兩個都填時要同時符合",
"Create by item selected count": "已選 {{count}} 項貨品",


Caricamento…
Annulla
Salva