Explorar el Código

update

stockTakeUpdate
CANCERYS\kw093 hace 1 semana
padre
commit
e13ffba0e0
Se han modificado 5 ficheros con 373 adiciones y 155 borrados
  1. +62
    -0
      src/app/api/stockTake/actions.ts
  2. +21
    -6
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  3. +270
    -145
      src/components/StockTakeManagement/PickerCardList.tsx
  4. +10
    -2
      src/i18n/en/stockTake.json
  5. +10
    -2
      src/i18n/zh/stockTake.json

+ 62
- 0
src/app/api/stockTake/actions.ts Ver fichero

@@ -405,6 +405,65 @@ export const getBusyStockTakeSections = async () => {
);
};

export type StockTakeCreateLotOption = {
inventoryLotLineId: number;
itemId: number;
itemCode: string | null;
itemName: string | null;
lotId: number;
lotNo: string | null;
expiryDate: string | null;
warehouseId: number;
warehouseCode: string | null;
stockTakeSection: string | null;
storeId: string | null;
availableQty: number;
busy: boolean;
};

export type StockTakeCreateItemOption = {
id: number;
code: string | null;
name: string | null;
};

/** 來源:All / PP / PF / other(與庫存批次現況報告一致) */
export const getAvailableLotsForStockTakeCreate = async (
itemIds: number[],
lotOrigin?: string | null,
) => {
const ids = itemIds.filter((id) => id > 0);
if (ids.length === 0) return [] as StockTakeCreateLotOption[];
const params = new URLSearchParams();
ids.forEach((id) => params.append("itemIds", String(id)));
const origin = lotOrigin?.trim();
if (origin && origin.toLowerCase() !== "all") params.set("lotOrigin", origin);
return serverFetchJson<StockTakeCreateLotOption[]>(
`${BASE_API_URL}/stockTakeRecord/availableLotsForCreate?${params.toString()}`,
{ method: "GET" },
);
};

export const searchItemsForStockTakeCreate = async (opts: {
code?: string | null;
name?: string | null;
lotOrigin?: string | null;
}) => {
const params = new URLSearchParams();
const code = opts.code?.trim();
const name = opts.name?.trim();
const origin = opts.lotOrigin?.trim();
if (code) params.set("code", code);
if (name) params.set("name", name);
if (origin && origin.toLowerCase() !== "all") params.set("lotOrigin", origin);
// 無編號/名稱且來源=全部 → 不查(避免一次拉全庫)
if (!params.toString()) return [] as StockTakeCreateItemOption[];
return serverFetchJson<StockTakeCreateItemOption[]>(
`${BASE_API_URL}/stockTakeRecord/searchItemsForCreate?${params.toString()}`,
{ method: "GET" },
);
};

export const closeStockTakeRound = async (stockTakeRoundId: number) => {
return serverFetchJson<{ success: boolean; message?: string; closedCount?: number }>(
`${BASE_API_URL}/stockTakeRecord/closeRound`,
@@ -431,9 +490,11 @@ export const createStockTakeForItems = async (
itemIds: number[],
stockTakeRoundName?: string | null,
planStart?: string | null,
inventoryLotLineIds?: number[] | null,
) => {
const trimmedName = stockTakeRoundName?.trim();
const trimmedPlanStart = planStart?.trim();
const lotLineIds = (inventoryLotLineIds || []).filter((id) => id > 0);
return serverFetchJson<Map<string, string>>(
`${BASE_API_URL}/stockTake/createForItems`,
{
@@ -443,6 +504,7 @@ export const createStockTakeForItems = async (
},
body: JSON.stringify({
itemIds,
...(lotLineIds.length > 0 ? { inventoryLotLineIds: lotLineIds } : {}),
...(trimmedName ? { stockTakeRoundName: trimmedName } : {}),
...(trimmedPlanStart ? { planStart: trimmedPlanStart } : {}),
}),


+ 21
- 6
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Ver fichero

@@ -1651,6 +1651,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
<ToggleButtonGroup
exclusive
size="small"
color="primary"
value={searchVarianceFilterInclusive ? "in" : "out"}
onChange={(_, next: "in" | "out" | null) => {
if (next == null) return;
@@ -1658,13 +1659,27 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
setSearchVarianceFilterInclusive(inclusive);
handleSearch({ varianceFilterInclusive: inclusive });
}}
sx={{
"& .MuiToggleButton-root": {
textTransform: "none",
px: 1.5,
fontWeight: 500,
borderColor: "primary.main",
color: "text.secondary",
"&.Mui-selected": {
bgcolor: "primary.main",
color: "primary.contrastText",
fontWeight: 700,
"&:hover": {
bgcolor: "primary.dark",
color: "primary.contrastText",
},
},
},
}}
>
<ToggleButton value="out" sx={{ textTransform: "none", px: 1.25 }}>
{t("Variance out of range")}
</ToggleButton>
<ToggleButton value="in" sx={{ textTransform: "none", px: 1.25 }}>
{t("Variance in range")}
</ToggleButton>
<ToggleButton value="out">{t("Variance out of range")}</ToggleButton>
<ToggleButton value="in">{t("Variance in range")}</ToggleButton>
</ToggleButtonGroup>
<Box
sx={{


+ 270
- 145
src/components/StockTakeManagement/PickerCardList.tsx Ver fichero

@@ -52,9 +52,10 @@ import {
getStockTakeRecordsPaged,
getLatestStockTakeRoundMeta,
getBusyStockTakeSections,
getStockTakeRoundCards,
getAvailableLotsForStockTakeCreate,
searchItemsForStockTakeCreate,
type StockTakeCreateLotOption,
} from "@/app/api/stockTake/actions";
import { fetchItemsByPage } from "@/app/api/settings/item/actions";
import { fetchStockTakeSections } from "@/app/api/warehouse/actions";
import { fetchMissingStockTakeSectionIssues, fetchWarehouseListClient } from "@/app/api/warehouse/client";
import type { MissingStockTakeSectionIssueItem, StockTakeSectionInfo, WarehouseResult } from "@/app/api/warehouse";
@@ -85,13 +86,6 @@ function chunkList<T>(items: T[], size: number): T[][] {

type CreateDialogItemHit = { id: number; code: string; name: string };

function itemRecordsFromPage(res: unknown): { id: string | number; code?: string; name?: string }[] {
const records = (res as { records?: unknown[] })?.records;
return Array.isArray(records)
? (records as { id: string | number; code?: string; name?: string }[])
: [];
}

function withAllOption(options: string[]): string[] {
return options.length > 1 ? [LOCATION_ALL, ...options] : options;
}
@@ -351,15 +345,17 @@ 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 [openingCreateDialog, setOpeningCreateDialog] = useState(false);
const openCreateDialogLockRef = useRef(false);
const [createDialogItemCode, setCreateDialogItemCode] = useState("");
const [createDialogItemName, setCreateDialogItemName] = useState("");
/** 來源:All / PP / PF / other(同庫存批次現況報告) */
const [createDialogLotOrigin, setCreateDialogLotOrigin] = useState("All");
const [createDialogItemHits, setCreateDialogItemHits] = useState<CreateDialogItemHit[]>([]);
const [createDialogSelectedItems, setCreateDialogSelectedItems] = useState<CreateDialogItemHit[]>([]);
const [createDialogLotOptions, setCreateDialogLotOptions] = useState<StockTakeCreateLotOption[]>([]);
const [createDialogSelectedLotLineIds, setCreateDialogSelectedLotLineIds] = useState<number[]>([]);
const [createDialogLotsLoading, setCreateDialogLotsLoading] = useState(false);
const [createDialogItemSearching, setCreateDialogItemSearching] = useState(false);
const [sectionsLoading, setSectionsLoading] = useState(false);
const [stockTakeSectionRows, setStockTakeSectionRows] = useState<StockTakeSectionInfo[]>([]);
@@ -425,45 +421,18 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
};
}, []);

const applyCreateModeBlocksFromRounds = useCallback(
(rounds: { open?: boolean; stockTakeRoundMode?: string | null }[]) => {
const openRounds = rounds || [];
const blockSection = openRounds.some(
(round) =>
round.open &&
String(round.stockTakeRoundMode ?? "").trim().toLowerCase() === "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");
},
[],
);

const openCreateDialog = useCallback(async () => {
if (openCreateDialogLockRef.current) return;
openCreateDialogLockRef.current = true;
setOpeningCreateDialog(true);
try {
const res = await getStockTakeRoundCards();
applyCreateModeBlocksFromRounds(res.rounds || []);
} catch (e) {
console.error("Failed to check open stock take create modes:", e);
setBlockSectionCreate(false);
setBlockItemCreate(false);
setCreateMode("section");
} finally {
setOpeningCreateDialog(false);
openCreateDialogLockRef.current = false;
setOpenConfirmDialog(true);
}
}, [applyCreateModeBlocksFromRounds]);
}, []);

useEffect(() => {
if (autoOpenCreate) void openCreateDialog();
@@ -789,14 +758,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
if (createDialogPlanStart == null || !createDialogPlanStart.isValid()) return;
if (createDialogPlanStart.startOf("day").isBefore(dayjs().startOf("day"))) return;
if (createMode === "item") {
if (createDialogSelectedItems.length === 0) return;
if (createDialogSelectedLotLineIds.length === 0) return;
} else if (createDialogSelectedSections.length === 0) {
return;
}
setOpenCreateStockTakeSummaryConfirm(true);
}, [
createDialogPlanStart,
createDialogSelectedItems.length,
createDialogSelectedLotLineIds.length,
createDialogSelectedSections.length,
createMode,
]);
@@ -1070,7 +1039,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const handleCreateStockTake = useCallback(async () => {
if (createStockTakeInFlightRef.current) return;
if (createMode === "item") {
if (createDialogSelectedItems.length === 0) return;
if (createDialogSelectedLotLineIds.length === 0) return;
} else if (createDialogSelectedSections.length === 0) {
return;
}
@@ -1089,6 +1058,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
createDialogSelectedItems.map((it) => it.id),
createDialogRoundName,
planStart,
createDialogSelectedLotLineIds,
)
: await createStockTakeForSections(
createDialogSelectedSections,
@@ -1112,7 +1082,9 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
setCreateDialogSelectedItems([]);
setCreateMode(blockSectionCreate && !blockItemCreate ? "item" : "section");
setCreateDialogLotOptions([]);
setCreateDialogSelectedLotLineIds([]);
setCreateMode("section");
} catch (e) {
console.error(e);
} finally {
@@ -1120,11 +1092,10 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
createStockTakeInFlightRef.current = false;
}
}, [
blockItemCreate,
blockSectionCreate,
createDialogPlanStart,
createDialogRoundName,
createDialogSelectedItems,
createDialogSelectedLotLineIds,
createDialogSelectedSections,
createMode,
onListPageChange,
@@ -1135,25 +1106,30 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
if (!openConfirmDialog || createMode !== "item") return;
const code = createDialogItemCode.trim();
const name = createDialogItemName.trim();
if (!code && !name) {
const origin = createDialogLotOrigin.trim();
const hasOrigin = origin !== "" && origin.toLowerCase() !== "all";
// 來源=全部:需編號或名稱;來源=PP/PF/其他:可只依來源列出
if (!code && !name && !hasOrigin) {
setCreateDialogItemHits([]);
return;
}
let cancelled = false;
const timer = window.setTimeout(() => {
setCreateDialogItemSearching(true);
const params: Record<string, string | number> = { pageSize: 300, pageNum: 1 };
if (code) params.code = code;
if (name) params.name = name;
fetchItemsByPage(params)
.then((page) => {
searchItemsForStockTakeCreate({
code,
name,
lotOrigin: createDialogLotOrigin,
})
.then((rows) => {
if (cancelled) return;
const uniq: CreateDialogItemHit[] = [];
itemRecordsFromPage(page).forEach((row) => {
const id = Number(row.id);
if (!id) return;
uniq.push({ id, code: row.code || "", name: row.name || "" });
});
const uniq: CreateDialogItemHit[] = (Array.isArray(rows) ? rows : [])
.map((row) => ({
id: Number(row.id),
code: row.code || "",
name: row.name || "",
}))
.filter((row) => row.id > 0);
uniq.sort((a, b) =>
a.code.localeCompare(b.code, undefined, { numeric: true, sensitivity: "base" }),
);
@@ -1171,7 +1147,56 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
cancelled = true;
window.clearTimeout(timer);
};
}, [openConfirmDialog, createMode, createDialogItemCode, createDialogItemName]);
}, [
openConfirmDialog,
createMode,
createDialogItemCode,
createDialogItemName,
createDialogLotOrigin,
]);

useEffect(() => {
if (!openConfirmDialog || createMode !== "item") {
setCreateDialogLotOptions([]);
return;
}
const itemIds = createDialogSelectedItems.map((it) => it.id);
if (itemIds.length === 0) {
setCreateDialogLotOptions([]);
setCreateDialogSelectedLotLineIds([]);
return;
}
let cancelled = false;
setCreateDialogLotsLoading(true);
getAvailableLotsForStockTakeCreate(itemIds, createDialogLotOrigin)
.then((lots) => {
if (cancelled) return;
const list = Array.isArray(lots) ? lots : [];
setCreateDialogLotOptions(list);
setCreateDialogSelectedLotLineIds((prev) => {
const freeIds = list.filter((l) => !l.busy).map((l) => l.inventoryLotLineId);
const selectable = new Set(freeIds);
const kept = prev.filter((id) => selectable.has(id));
const keptSet = new Set(kept);
// 保留已選,並自動勾上新貨品的可盤 lot
return [...kept, ...freeIds.filter((id) => !keptSet.has(id))];
});
})
.catch((e) => {
console.error("Failed to load lots for stock take create:", e);
if (!cancelled) {
setCreateDialogLotOptions([]);
setCreateDialogSelectedLotLineIds([]);
}
})
.finally(() => {
if (!cancelled) setCreateDialogLotsLoading(false);
});
return () => {
cancelled = true;
};
}, [openConfirmDialog, createMode, createDialogSelectedItems, createDialogLotOrigin]);

useEffect(() => {
setSectionsLoading(true);
fetchStockTakeSections()
@@ -1285,11 +1310,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
setOpenConfirmDialog(false);
setCreateDialogRoundName("");
setCreateDialogPlanStart(dayjs());
setCreateMode(blockSectionCreate && !blockItemCreate ? "item" : "section");
setCreateMode("section");
setCreateDialogItemCode("");
setCreateDialogItemName("");
setCreateDialogLotOrigin("All");
setCreateDialogItemHits([]);
setCreateDialogSelectedItems([]);
setCreateDialogLotOptions([]);
setCreateDialogSelectedLotLineIds([]);
onCreateDialogClose?.();
};
return (
@@ -1801,34 +1829,16 @@ 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 === "item" && blockItemCreate
? "section"
: createMode === "section" && blockSectionCreate
? "item"
: createMode
}
value={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")} />
)}
{blockItemCreate ? null : (
<Tab value="item" label={t("Create by item")} />
)}
<Tab value="section" label={t("Create by section")} />
<Tab value="item" label={t("Create by item")} />
</Tabs>
)}
</Box>
<DialogContent
dividers
@@ -1992,13 +2002,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</Stack>
)}
<Typography variant="body2" color="text.secondary">
{createMode === "item" ? t("Total selected items label") : t("Total selected sections label")}{" "}
{createMode === "item" ? t("Total selected lots label") : t("Total selected sections label")}{" "}
<Typography component="span" variant="body2" color="primary.main" fontWeight={700}>
{createMode === "item"
? createDialogSelectedItems.length
? createDialogSelectedLotLineIds.length
: createDialogSelectedSections.length}
</Typography>{" "}
{createMode === "item" ? t("items unit") : t("sections unit")}
{createMode === "item" ? t("lots unit") : t("sections unit")}
</Typography>
</Box>

@@ -2050,9 +2060,23 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}}
sx={createDialogCompactInputSx}
/>
<FormControl size="small" sx={{ minWidth: { xs: "100%", sm: 140 } }}>
<InputLabel id="create-lot-origin-label">{t("Lot Origin")}</InputLabel>
<Select
labelId="create-lot-origin-label"
label={t("Lot Origin")}
value={createDialogLotOrigin}
onChange={(e) => setCreateDialogLotOrigin(String(e.target.value))}
>
<MenuItem value="All">{t("All")}</MenuItem>
<MenuItem value="PP">PP</MenuItem>
<MenuItem value="PF">PF</MenuItem>
<MenuItem value="other">{t("other")}</MenuItem>
</Select>
</FormControl>
</Stack>
<Typography variant="caption" color="text.secondary">
{t("Create by item selected count", { count: createDialogSelectedItems.length })}
{t("Create by item hint")}
</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.75 }}>
{createDialogSelectedItems.map((it) => (
@@ -2066,54 +2090,152 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
/>
))}
</Box>
<Box sx={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
{createDialogItemSearching ? (
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
<CircularProgress size={24} />
</Box>
) : createDialogItemHits.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{createDialogItemCode.trim() || createDialogItemName.trim()
? t("No data")
: t("Create by item search hint")}
<Box
sx={{
flex: 1,
minHeight: 0,
overflowY: "auto",
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" },
gap: 1.5,
}}
>
<Box sx={{ minHeight: 0, overflowY: "auto", border: 1, borderColor: "divider", borderRadius: 1, p: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: "block" }}>
{t("Create by item search hint")}
</Typography>
) : (
<Stack>
{createDialogItemHits.map((it) => {
const checked = createDialogSelectedItems.some((x) => x.id === it.id);
return (
<Box
key={it.id}
sx={{
display: "flex",
alignItems: "center",
gap: 1,
py: 0.5,
borderBottom: 1,
borderColor: "divider",
}}
>
<Checkbox
checked={checked}
onChange={() => {
setCreateDialogSelectedItems((prev) =>
checked ? prev.filter((x) => x.id !== it.id) : [...prev, it],
);
{createDialogItemSearching ? (
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
<CircularProgress size={24} />
</Box>
) : createDialogItemHits.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{createDialogItemCode.trim() ||
createDialogItemName.trim() ||
(createDialogLotOrigin.trim() !== "" &&
createDialogLotOrigin.trim().toLowerCase() !== "all")
? t("No data")
: t("Create by item search hint")}
</Typography>
) : (
<Stack>
{createDialogItemHits.map((it) => {
const checked = createDialogSelectedItems.some((x) => x.id === it.id);
return (
<Box
key={it.id}
sx={{
display: "flex",
alignItems: "center",
gap: 1,
py: 0.5,
borderBottom: 1,
borderColor: "divider",
}}
/>
<Box>
<Typography variant="body2" fontWeight={600}>
{it.code}
</Typography>
<Typography variant="caption" color="text.secondary">
{it.name}
</Typography>
>
<Checkbox
checked={checked}
onChange={() => {
setCreateDialogSelectedItems((prev) =>
checked ? prev.filter((x) => x.id !== it.id) : [...prev, it],
);
}}
/>
<Box>
<Typography variant="body2" fontWeight={600}>
{it.code}
</Typography>
<Typography variant="caption" color="text.secondary">
{it.name}
</Typography>
</Box>
</Box>
</Box>
);
})}
);
})}
</Stack>
)}
</Box>
<Box sx={{ minHeight: 0, overflowY: "auto", border: 1, borderColor: "divider", borderRadius: 1, p: 1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}>
<Typography variant="caption" color="text.secondary">
{t("Create by item lot detail", {
count: createDialogSelectedLotLineIds.length,
})}
</Typography>
<Button
size="small"
disabled={createDialogLotOptions.every((l) => l.busy)}
onClick={() =>
setCreateDialogSelectedLotLineIds(
createDialogLotOptions.filter((l) => !l.busy).map((l) => l.inventoryLotLineId),
)
}
>
{t("Select all free lots")}
</Button>
</Stack>
)}
{createDialogLotsLoading ? (
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
<CircularProgress size={24} />
</Box>
) : createDialogSelectedItems.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{t("Create by item select item first")}
</Typography>
) : createDialogLotOptions.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{t("No available lots for selected items")}
</Typography>
) : (
<Stack>
{createDialogLotOptions.map((lot) => {
const checked = createDialogSelectedLotLineIds.includes(lot.inventoryLotLineId);
return (
<Box
key={lot.inventoryLotLineId}
sx={{
display: "flex",
alignItems: "flex-start",
gap: 1,
py: 0.75,
borderBottom: 1,
borderColor: "divider",
opacity: lot.busy ? 0.55 : 1,
}}
>
<Checkbox
size="small"
disabled={lot.busy}
checked={checked && !lot.busy}
onChange={() => {
if (lot.busy) return;
setCreateDialogSelectedLotLineIds((prev) =>
checked
? prev.filter((id) => id !== lot.inventoryLotLineId)
: [...prev, lot.inventoryLotLineId],
);
}}
/>
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" fontWeight={600}>
{lot.itemCode} · {lot.lotNo || "-"}
</Typography>
<Typography variant="caption" color="text.secondary" display="block">
{lot.stockTakeSection || "-"}
{lot.warehouseCode ? ` · ${lot.warehouseCode}` : ""}
</Typography>
{lot.busy ? (
<Typography variant="caption" color="warning.main">
{t("Lot busy in open stock take")}
</Typography>
) : null}
</Box>
</Box>
);
})}
</Stack>
)}
</Box>
</Box>
</Stack>
) : activeCreateFloorKey != null ? (
@@ -2327,7 +2449,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
disabled={
creating ||
(createMode === "item"
? createDialogSelectedItems.length === 0
? createDialogSelectedLotLineIds.length === 0
: createDialogSelectedSections.length === 0) ||
createDialogPlanStart == null ||
!createDialogPlanStart.isValid() ||
@@ -2365,13 +2487,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
: "-"}
</Typography>
<Typography variant="body2" fontWeight={600}>
{createMode === "item" ? t("Total selected items label") : t("Total selected sections label")}{" "}
<Typography component="span"fontWeight={600} >
{createMode === "item" ? t("Total selected lots label") : t("Total selected sections label")}{" "}
<Typography component="span" fontWeight={600}>
{createMode === "item"
? createDialogSelectedItems.length
? createDialogSelectedLotLineIds.length
: createStockTakeSummarySections.length}
</Typography>{" "}
{createMode === "item" ? t("items unit") : t("sections unit")}
{createMode === "item" ? t("lots unit") : t("sections unit")}
</Typography>
</Stack>
<Box
@@ -2386,16 +2508,19 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
>
<Stack spacing={1}>
{createMode === "item"
? createDialogSelectedItems.map((it) => (
<Box key={it.id}>
<Typography variant="body2" fontWeight={600}>
{it.code}
</Typography>
<Typography variant="caption" color="text.secondary">
{it.name}
</Typography>
</Box>
))
? createDialogLotOptions
.filter((lot) => createDialogSelectedLotLineIds.includes(lot.inventoryLotLineId))
.map((lot) => (
<Box key={lot.inventoryLotLineId}>
<Typography variant="body2" fontWeight={600}>
{lot.itemCode} · {lot.lotNo || "-"}
</Typography>
<Typography variant="caption" color="text.secondary">
{lot.stockTakeSection || "-"}
{lot.warehouseCode ? ` · ${lot.warehouseCode}` : ""}
</Typography>
</Box>
))
: createStockTakeSummarySections.map((section) => {
const meta = getCreateDialogSectionMeta(section);
return (
@@ -2428,7 +2553,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
disabled={
creating ||
(createMode === "item"
? createDialogSelectedItems.length === 0
? createDialogSelectedLotLineIds.length === 0
: createDialogSelectedSections.length === 0)
}
>


+ 10
- 2
src/i18n/en/stockTake.json Ver fichero

@@ -61,12 +61,20 @@
"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.",
"Lot Origin": "Lot origin",
"Create by item hint": "Only items with available lots are listed. Pick origin (PP / PF / Other) to load the list, optionally narrow by code/name, then tick lots on the right.",
"Create by item search hint": "Pick an origin to list items, or also enter code/name to narrow. Both keywords must match when filled.",
"Create by item selected count": "{{count}} item(s) selected",
"Create by item lot detail": "Lots ({{count}} selected)",
"Create by item select item first": "Select item(s) on the left first",
"No available lots for selected items": "No available lots for the selected items",
"Lot busy in open stock take": "Already in an open stock-take round",
"Select all free lots": "Select all free lots",
"Search item code or name": "Search item code or name",
"Total selected items label": "Selected items",
"Total selected lots label": "Selected lots",
"items unit": "item(s)",
"lots unit": "lot(s)",
"Create Stock Take (Select Sections)": "Create stock take (select sections)",
"Created": "Created",
"Creation date": "Creation date",


+ 10
- 2
src/i18n/zh/stockTake.json Ver fichero

@@ -144,12 +144,20 @@
"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": "可分別輸入貨品編號、貨品名稱;兩個都填時要同時符合",
"Lot Origin": "來源",
"Create by item hint": "只列出有可用庫存的貨品。選來源(PP/PF/其他)即可列出;也可再輸入編號/名稱縮小。先勾選貨品,再於右側勾選批號/位置。",
"Create by item search hint": "可只選來源列出;或再輸入編號/名稱縮小。兩個關鍵字都填時要同時符合",
"Create by item selected count": "已選 {{count}} 項貨品",
"Create by item lot detail": "可選批號(已選 {{count}})",
"Create by item select item first": "請先在左側勾選貨品",
"No available lots for selected items": "所選貨品沒有可用批號",
"Lot busy in open stock take": "已在進行中的盤點輪次",
"Select all free lots": "全選可盤批號",
"Search item code or name": "搜尋貨品編號或名稱",
"Total selected items label": "已選貨品",
"Total selected lots label": "已選批號",
"items unit": "項",
"lots unit": "筆",
"Create Stock Take (Select Sections)": "建立盤點(選擇區域)",
"Total Item Kind Number": "貨品種類數量",
"View ReStockTake": "查看重新盤點",


Cargando…
Cancelar
Guardar