|
|
|
@@ -22,12 +22,15 @@ import { |
|
|
|
InputLabel, |
|
|
|
Select, |
|
|
|
MenuItem, |
|
|
|
Autocomplete, |
|
|
|
Badge, |
|
|
|
IconButton, |
|
|
|
Tooltip, |
|
|
|
Drawer, |
|
|
|
Divider, |
|
|
|
Tab, |
|
|
|
Tabs, |
|
|
|
ToggleButton, |
|
|
|
ToggleButtonGroup, |
|
|
|
} from "@mui/material"; |
|
|
|
import SearchIcon from "@mui/icons-material/Search"; |
|
|
|
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined"; |
|
|
|
@@ -44,8 +47,8 @@ import { |
|
|
|
getLatestStockTakeRoundMeta, |
|
|
|
} from "@/app/api/stockTake/actions"; |
|
|
|
import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; |
|
|
|
import { fetchMissingStockTakeSectionIssues } from "@/app/api/warehouse/client"; |
|
|
|
import type { MissingStockTakeSectionIssueItem, StockTakeSectionInfo } from "@/app/api/warehouse"; |
|
|
|
import { fetchMissingStockTakeSectionIssues, fetchWarehouseListClient } from "@/app/api/warehouse/client"; |
|
|
|
import type { MissingStockTakeSectionIssueItem, StockTakeSectionInfo, WarehouseResult } from "@/app/api/warehouse"; |
|
|
|
import dayjs, { type Dayjs } from "dayjs"; |
|
|
|
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; |
|
|
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; |
|
|
|
@@ -57,11 +60,100 @@ import { |
|
|
|
import { AUTH } from "@/authorities"; |
|
|
|
|
|
|
|
const FLOOR_GROUP_UNSET = "__UNSET__"; |
|
|
|
/** 與 controllQRcodescanWarehouse 選倉相同:樓層 → 倉庫 → 倉位 */ |
|
|
|
const LOCATION_ALL = "ALL"; |
|
|
|
const LOCATION_BUTTONS_PER_ROW = 15; |
|
|
|
const LOCATION_BUTTON_WIDTH = 80; |
|
|
|
const LOCATION_NONE_SECTION = "__NONE__"; |
|
|
|
|
|
|
|
function chunkList<T>(items: T[], size: number): T[][] { |
|
|
|
const rows: T[][] = []; |
|
|
|
for (let i = 0; i < items.length; i += size) { |
|
|
|
rows.push(items.slice(i, i + size)); |
|
|
|
} |
|
|
|
return rows; |
|
|
|
} |
|
|
|
|
|
|
|
function withAllOption(options: string[]): string[] { |
|
|
|
return options.length > 1 ? [LOCATION_ALL, ...options] : options; |
|
|
|
} |
|
|
|
|
|
|
|
function compareLocationLabel(a: string, b: string): number { |
|
|
|
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); |
|
|
|
} |
|
|
|
|
|
|
|
function matchesPickerCardFilters( |
|
|
|
session: AllPickedStockTakeListReponse, |
|
|
|
filters: PickerCardListFilters, |
|
|
|
): boolean { |
|
|
|
if (filters.sectionDescription && filters.sectionDescription !== "All") { |
|
|
|
if (session.stockTakeSectionDescription !== filters.sectionDescription) return false; |
|
|
|
} |
|
|
|
const sectionQuery = filters.stockTakeSession?.trim() ?? ""; |
|
|
|
if (sectionQuery) { |
|
|
|
const parts = sectionQuery.split(",").map((part) => part.trim()).filter(Boolean); |
|
|
|
const name = (session.stockTakeSession || "").toLowerCase(); |
|
|
|
const hit = parts.some((part) => { |
|
|
|
const key = part.toLowerCase(); |
|
|
|
return name === key || name.includes(key); |
|
|
|
}); |
|
|
|
if (!hit) return false; |
|
|
|
} |
|
|
|
if (filters.status && filters.status !== "All") { |
|
|
|
const normalized = filters.status.trim().toLowerCase(); |
|
|
|
const accepted = normalized === "stocktaking" |
|
|
|
? ["stocktaking", "processing", "in_progress"] |
|
|
|
: [normalized]; |
|
|
|
if (!accepted.includes((session.status || "").trim().toLowerCase())) return false; |
|
|
|
} |
|
|
|
const area = filters.area?.trim() ?? ""; |
|
|
|
if (area && !(session.warehouseArea || "").toLowerCase().includes(area.toLowerCase())) return false; |
|
|
|
if (filters.storeId && filters.storeId !== "All") { |
|
|
|
const floor = filters.storeId.trim().toLowerCase(); |
|
|
|
const itemFloor = (session.storeId || "").toLowerCase(); |
|
|
|
if (itemFloor !== floor && !itemFloor.includes(floor)) return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
/** 移動超過此距離才視為框選(Windows 橡皮筋),否則視為單擊 */ |
|
|
|
const MARQUEE_DRAG_THRESHOLD_PX = 4; |
|
|
|
/** 左側樓層列表超過此數量才啟用捲動 */ |
|
|
|
const FLOOR_LIST_SCROLL_THRESHOLD = 8; |
|
|
|
|
|
|
|
const SEARCH_FIELD_HEIGHT = 56; |
|
|
|
/** 下拉最多先顯示約 8 列,其餘在選單內捲動 */ |
|
|
|
const SEARCH_MENU_MAX_HEIGHT = 320; |
|
|
|
const searchMenuProps = { |
|
|
|
PaperProps: { |
|
|
|
sx: { maxHeight: SEARCH_MENU_MAX_HEIGHT }, |
|
|
|
}, |
|
|
|
} as const; |
|
|
|
|
|
|
|
const searchFieldSx = { |
|
|
|
"& .MuiOutlinedInput-root": { |
|
|
|
height: SEARCH_FIELD_HEIGHT, |
|
|
|
minHeight: SEARCH_FIELD_HEIGHT, |
|
|
|
boxSizing: "border-box", |
|
|
|
alignItems: "center", |
|
|
|
}, |
|
|
|
"& .MuiSelect-select": { |
|
|
|
height: `${SEARCH_FIELD_HEIGHT}px !important`, |
|
|
|
minHeight: "0 !important", |
|
|
|
display: "flex", |
|
|
|
alignItems: "center", |
|
|
|
boxSizing: "border-box", |
|
|
|
py: "0 !important", |
|
|
|
}, |
|
|
|
} as const; |
|
|
|
|
|
|
|
function selectedSectionCodes(value: string): string[] { |
|
|
|
if (!value.trim() || value.trim() === LOCATION_NONE_SECTION) return []; |
|
|
|
return value |
|
|
|
.split(",") |
|
|
|
.map((part) => part.trim()) |
|
|
|
.filter((part) => part && part !== LOCATION_NONE_SECTION); |
|
|
|
} |
|
|
|
|
|
|
|
const createDialogCompactInputSx = { |
|
|
|
mt: 0, |
|
|
|
mb: 0, |
|
|
|
@@ -105,16 +197,81 @@ interface PickerCardListProps { |
|
|
|
appliedFilters: PickerCardListFilters; |
|
|
|
onSearchFiltersChange: (next: PickerCardListFilters) => void; |
|
|
|
onAppliedFiltersChange: (next: PickerCardListFilters) => void; |
|
|
|
/** 樓層/倉庫/倉位,由父層保存,從明細返回時仍顯示 */ |
|
|
|
locationPick: PickerLocationPick; |
|
|
|
onLocationPickChange: (next: PickerLocationPick) => void; |
|
|
|
} |
|
|
|
|
|
|
|
export type PickerSearchKind = "location" | "section"; |
|
|
|
|
|
|
|
export type PickerCardListFilters = { |
|
|
|
sectionDescription: string; |
|
|
|
stockTakeSession: string; |
|
|
|
status: string; |
|
|
|
area: string; |
|
|
|
storeId: string; |
|
|
|
searchKind: PickerSearchKind; |
|
|
|
}; |
|
|
|
|
|
|
|
function locationQueryFromPick( |
|
|
|
pick: PickerLocationPick, |
|
|
|
warehouses: WarehouseResult[], |
|
|
|
): { stockTakeSession: string; storeId: string } { |
|
|
|
const useSections = |
|
|
|
(pick.warehouse !== "" && pick.warehouse !== LOCATION_ALL) || |
|
|
|
(pick.area !== "" && pick.area !== LOCATION_ALL) || |
|
|
|
(pick.slot !== "" && pick.slot !== LOCATION_ALL); |
|
|
|
const sections = new Set<string>(); |
|
|
|
if (useSections) { |
|
|
|
warehouses.forEach((w) => { |
|
|
|
const section = w.stockTakeSection?.trim(); |
|
|
|
if (!section) return; |
|
|
|
if (pick.storeId && pick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== pick.storeId) return; |
|
|
|
if (pick.warehouse && pick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== pick.warehouse) return; |
|
|
|
if (pick.area && pick.area !== LOCATION_ALL && (w.area ?? "").trim() !== pick.area) return; |
|
|
|
if (pick.slot && pick.slot !== LOCATION_ALL && (w.slot ?? "").trim() !== pick.slot) return; |
|
|
|
sections.add(section); |
|
|
|
}); |
|
|
|
} |
|
|
|
return { |
|
|
|
stockTakeSession: useSections |
|
|
|
? Array.from(sections).sort(compareLocationLabel).join(",") || LOCATION_NONE_SECTION |
|
|
|
: "", |
|
|
|
storeId: !pick.storeId || pick.storeId === LOCATION_ALL || useSections ? "All" : pick.storeId, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
function appliedPickerFilters( |
|
|
|
filters: PickerCardListFilters, |
|
|
|
pick: PickerLocationPick, |
|
|
|
warehouses: WarehouseResult[], |
|
|
|
): PickerCardListFilters { |
|
|
|
const kind = filters.searchKind ?? "location"; |
|
|
|
if (kind === "section") { |
|
|
|
return { |
|
|
|
sectionDescription: filters.sectionDescription || "All", |
|
|
|
stockTakeSession: filters.stockTakeSession || "", |
|
|
|
status: filters.status || "All", |
|
|
|
area: "", |
|
|
|
storeId: "All", |
|
|
|
searchKind: "section", |
|
|
|
}; |
|
|
|
} |
|
|
|
const location = locationQueryFromPick(pick, warehouses); |
|
|
|
return { |
|
|
|
sectionDescription: "All", |
|
|
|
stockTakeSession: location.stockTakeSession, |
|
|
|
status: filters.status || "All", |
|
|
|
area: "", |
|
|
|
storeId: location.storeId, |
|
|
|
searchKind: "location", |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
export type PickerLocationPick = { storeId: string; warehouse: string; area: string; slot: string }; |
|
|
|
|
|
|
|
export const EMPTY_PICKER_LOCATION: PickerLocationPick = { storeId: "", warehouse: "", area: "", slot: "" }; |
|
|
|
|
|
|
|
const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
page, |
|
|
|
pageSize, |
|
|
|
@@ -125,14 +282,16 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
appliedFilters, |
|
|
|
onSearchFiltersChange, |
|
|
|
onAppliedFiltersChange, |
|
|
|
locationPick, |
|
|
|
onLocationPickChange, |
|
|
|
}) => { |
|
|
|
const { t } = useTranslation(["stockTake", "common"]); |
|
|
|
const router = useRouter(); |
|
|
|
dayjs.extend(duration); |
|
|
|
|
|
|
|
const [loading, setLoading] = useState(false); |
|
|
|
const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]); |
|
|
|
const [total, setTotal] = useState(0); |
|
|
|
const [loading, setLoading] = useState(true); |
|
|
|
/** 這一輪全部卡片,篩選在前端做,點選不再重打 API */ |
|
|
|
const [roundRecords, setRoundRecords] = useState<AllPickedStockTakeListReponse[]>([]); |
|
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
|
const abilities = session?.abilities ?? session?.user?.abilities ?? []; |
|
|
|
const canManageStockTake = abilities.some((a) => a.trim() === AUTH.ADMIN); |
|
|
|
@@ -171,22 +330,20 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
} | null>(null); |
|
|
|
const [marqueeRect, setMarqueeRect] = useState<MarqueeRect | null>(null); |
|
|
|
const [marqueePreviewIds, setMarqueePreviewIds] = useState<Set<string>>(() => new Set()); |
|
|
|
const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]); |
|
|
|
const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]); |
|
|
|
const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]); |
|
|
|
const [locationWarehouses, setLocationWarehouses] = useState<WarehouseResult[]>([]); |
|
|
|
/** 由選倉寫入的盤點區域字串;使用者改過區域欄後就不再覆寫 */ |
|
|
|
const locationOwnedSectionRef = useRef<string | null>( |
|
|
|
(locationPick.warehouse && locationPick.warehouse !== LOCATION_ALL) || |
|
|
|
(locationPick.area && locationPick.area !== LOCATION_ALL) || |
|
|
|
(locationPick.slot && locationPick.slot !== LOCATION_ALL) |
|
|
|
? searchFilters.stockTakeSession |
|
|
|
: null, |
|
|
|
); |
|
|
|
|
|
|
|
const statusOptions = ["pending", "stockTaking", "approving", "completed"]; |
|
|
|
|
|
|
|
const handleSearch = () => { |
|
|
|
onAppliedFiltersChange({ |
|
|
|
sectionDescription: searchFilters.sectionDescription || "All", |
|
|
|
stockTakeSession: searchFilters.stockTakeSession || "", |
|
|
|
status: searchFilters.status || "All", |
|
|
|
area: searchFilters.area || "", |
|
|
|
storeId: searchFilters.storeId || "All", |
|
|
|
}); |
|
|
|
onListPageChange(0); |
|
|
|
}; |
|
|
|
const searchKind = searchFilters.searchKind ?? "location"; |
|
|
|
|
|
|
|
const handleResetSearch = () => { |
|
|
|
const resetFilters: PickerCardListFilters = { |
|
|
|
@@ -195,34 +352,47 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
status: "All", |
|
|
|
area: "", |
|
|
|
storeId: "All", |
|
|
|
searchKind, |
|
|
|
}; |
|
|
|
onSearchFiltersChange(resetFilters); |
|
|
|
onAppliedFiltersChange(resetFilters); |
|
|
|
onLocationPickChange(EMPTY_PICKER_LOCATION); |
|
|
|
locationOwnedSectionRef.current = null; |
|
|
|
onListPageChange(0); |
|
|
|
}; |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
let cancelled = false; |
|
|
|
setLoading(true); |
|
|
|
getStockTakeRecordsPaged(page, pageSize, { |
|
|
|
sectionDescription: appliedFilters.sectionDescription, |
|
|
|
stockTakeSections: appliedFilters.stockTakeSession, |
|
|
|
status: appliedFilters.status, |
|
|
|
area: appliedFilters.area, |
|
|
|
storeId: appliedFilters.storeId, |
|
|
|
fetchWarehouseListClient() |
|
|
|
.then((rows) => { |
|
|
|
if (!cancelled) setLocationWarehouses(Array.isArray(rows) ? rows : []); |
|
|
|
}) |
|
|
|
.catch((e) => { |
|
|
|
console.error("Failed to load warehouses for stock take location filter:", e); |
|
|
|
if (!cancelled) setLocationWarehouses([]); |
|
|
|
}); |
|
|
|
return () => { |
|
|
|
cancelled = true; |
|
|
|
}; |
|
|
|
}, []); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
let cancelled = false; |
|
|
|
getStockTakeRecordsPaged(0, 10000, { |
|
|
|
sectionDescription: "All", |
|
|
|
stockTakeSections: "", |
|
|
|
status: "All", |
|
|
|
area: "", |
|
|
|
storeId: "All", |
|
|
|
onlyLatestRound: true, |
|
|
|
}) |
|
|
|
.then((res) => { |
|
|
|
if (cancelled) return; |
|
|
|
setStockTakeSessions(Array.isArray(res.records) ? res.records : []); |
|
|
|
setTotal(res.total || 0); |
|
|
|
setRoundRecords(Array.isArray(res.records) ? res.records : []); |
|
|
|
}) |
|
|
|
.catch((e) => { |
|
|
|
console.error(e); |
|
|
|
if (!cancelled) { |
|
|
|
setStockTakeSessions([]); |
|
|
|
setTotal(0); |
|
|
|
} |
|
|
|
console.error("Failed to load this round's stock take sections:", e); |
|
|
|
if (!cancelled) setRoundRecords([]); |
|
|
|
}) |
|
|
|
.finally(() => { |
|
|
|
if (!cancelled) setLoading(false); |
|
|
|
@@ -230,7 +400,196 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
return () => { |
|
|
|
cancelled = true; |
|
|
|
}; |
|
|
|
}, [page, pageSize, appliedFilters, listRefreshNonce]); |
|
|
|
}, [listRefreshNonce]); |
|
|
|
|
|
|
|
const roundSectionCodes = useMemo( |
|
|
|
() => |
|
|
|
Array.from( |
|
|
|
new Set( |
|
|
|
roundRecords |
|
|
|
.map((row) => row.stockTakeSession?.trim()) |
|
|
|
.filter((code): code is string => Boolean(code)), |
|
|
|
), |
|
|
|
), |
|
|
|
[roundRecords], |
|
|
|
); |
|
|
|
|
|
|
|
const filteredSessions = useMemo( |
|
|
|
() => roundRecords.filter((session) => matchesPickerCardFilters(session, appliedFilters)), |
|
|
|
[appliedFilters, roundRecords], |
|
|
|
); |
|
|
|
const total = filteredSessions.length; |
|
|
|
const stockTakeSessions = useMemo( |
|
|
|
() => filteredSessions.slice(page * pageSize, page * pageSize + pageSize), |
|
|
|
[filteredSessions, page, pageSize], |
|
|
|
); |
|
|
|
|
|
|
|
const stockTakeLocationWarehouses = useMemo(() => { |
|
|
|
if (roundSectionCodes.length === 0) return []; |
|
|
|
const inRound = new Set(roundSectionCodes); |
|
|
|
return locationWarehouses.filter((w) => { |
|
|
|
const section = w.stockTakeSection?.trim(); |
|
|
|
return Boolean(section && inRound.has(section)); |
|
|
|
}); |
|
|
|
}, [locationWarehouses, roundSectionCodes]); |
|
|
|
|
|
|
|
const locationFloorOptions = useMemo(() => { |
|
|
|
const set = new Set<string>(); |
|
|
|
stockTakeLocationWarehouses.forEach((w) => { |
|
|
|
const floor = w.store_id?.trim(); |
|
|
|
if (floor) set.add(floor); |
|
|
|
}); |
|
|
|
return withAllOption(Array.from(set).sort(compareLocationLabel)); |
|
|
|
}, [stockTakeLocationWarehouses]); |
|
|
|
|
|
|
|
const locationWarehouseOptions = useMemo(() => { |
|
|
|
if (!locationPick.storeId) return []; |
|
|
|
const set = new Set<string>(); |
|
|
|
stockTakeLocationWarehouses.forEach((w) => { |
|
|
|
if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; |
|
|
|
const zone = w.warehouse?.trim(); |
|
|
|
if (zone) set.add(zone); |
|
|
|
}); |
|
|
|
return withAllOption(Array.from(set).sort(compareLocationLabel)); |
|
|
|
}, [stockTakeLocationWarehouses, locationPick.storeId]); |
|
|
|
|
|
|
|
const locationAreaOptions = useMemo(() => { |
|
|
|
if (!locationPick.storeId || !locationPick.warehouse) return []; |
|
|
|
const set = new Set<string>(); |
|
|
|
stockTakeLocationWarehouses.forEach((w) => { |
|
|
|
if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; |
|
|
|
if (locationPick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== locationPick.warehouse) return; |
|
|
|
const area = w.area?.trim(); |
|
|
|
if (area) set.add(area); |
|
|
|
}); |
|
|
|
return withAllOption(Array.from(set).sort(compareLocationLabel)); |
|
|
|
}, [stockTakeLocationWarehouses, locationPick.storeId, locationPick.warehouse]); |
|
|
|
|
|
|
|
const locationSlotOptions = useMemo(() => { |
|
|
|
if (!locationPick.storeId || !locationPick.warehouse || !locationPick.area) return []; |
|
|
|
const set = new Set<string>(); |
|
|
|
stockTakeLocationWarehouses.forEach((w) => { |
|
|
|
if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; |
|
|
|
if (locationPick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== locationPick.warehouse) return; |
|
|
|
if (locationPick.area !== LOCATION_ALL && (w.area ?? "").trim() !== locationPick.area) return; |
|
|
|
const slot = w.slot?.trim(); |
|
|
|
if (slot) set.add(slot); |
|
|
|
}); |
|
|
|
return withAllOption(Array.from(set).sort(compareLocationLabel)); |
|
|
|
}, [stockTakeLocationWarehouses, locationPick.storeId, locationPick.warehouse, locationPick.area]); |
|
|
|
|
|
|
|
const locationWarehouseRows = useMemo( |
|
|
|
() => chunkList(locationWarehouseOptions, LOCATION_BUTTONS_PER_ROW), |
|
|
|
[locationWarehouseOptions], |
|
|
|
); |
|
|
|
const locationAreaRows = useMemo( |
|
|
|
() => chunkList(locationAreaOptions, LOCATION_BUTTONS_PER_ROW), |
|
|
|
[locationAreaOptions], |
|
|
|
); |
|
|
|
const locationSlotRows = useMemo( |
|
|
|
() => chunkList(locationSlotOptions, LOCATION_BUTTONS_PER_ROW), |
|
|
|
[locationSlotOptions], |
|
|
|
); |
|
|
|
|
|
|
|
const applyFilters = useCallback((next: PickerCardListFilters) => { |
|
|
|
const withKind = { ...next, searchKind: next.searchKind ?? "location" }; |
|
|
|
onSearchFiltersChange(withKind); |
|
|
|
onAppliedFiltersChange(appliedPickerFilters(withKind, locationPick, stockTakeLocationWarehouses)); |
|
|
|
onListPageChange(0); |
|
|
|
}, [locationPick, onAppliedFiltersChange, onListPageChange, onSearchFiltersChange, stockTakeLocationWarehouses]); |
|
|
|
|
|
|
|
/** 依盤點區域搜索時用這一輪全部區域;依位置搜索時下拉不顯示 */ |
|
|
|
const searchSectionCodes = useMemo(() => { |
|
|
|
if (searchKind === "section" || !locationPick.storeId) return [...roundSectionCodes].sort(compareLocationLabel); |
|
|
|
const codes = new Set<string>(); |
|
|
|
stockTakeLocationWarehouses.forEach((w) => { |
|
|
|
const section = w.stockTakeSection?.trim(); |
|
|
|
if (!section) return; |
|
|
|
if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; |
|
|
|
if (locationPick.warehouse && locationPick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== locationPick.warehouse) return; |
|
|
|
if (locationPick.area && locationPick.area !== LOCATION_ALL && (w.area ?? "").trim() !== locationPick.area) return; |
|
|
|
if (locationPick.slot && locationPick.slot !== LOCATION_ALL && (w.slot ?? "").trim() !== locationPick.slot) return; |
|
|
|
codes.add(section); |
|
|
|
}); |
|
|
|
return Array.from(codes).sort(compareLocationLabel); |
|
|
|
}, [locationPick, roundSectionCodes, searchKind, stockTakeLocationWarehouses]); |
|
|
|
|
|
|
|
const sectionDescriptionByCode = useMemo(() => { |
|
|
|
const map = new Map<string, string>(); |
|
|
|
stockTakeSectionRows.forEach((row) => { |
|
|
|
const section = row.stockTakeSection?.trim(); |
|
|
|
const text = row.stockTakeSectionDescription?.trim(); |
|
|
|
if (section && text) map.set(section, text); |
|
|
|
}); |
|
|
|
roundRecords.forEach((session) => { |
|
|
|
const section = session.stockTakeSession?.trim(); |
|
|
|
const text = session.stockTakeSectionDescription?.trim(); |
|
|
|
if (section && text && !map.has(section)) map.set(section, text); |
|
|
|
}); |
|
|
|
return map; |
|
|
|
}, [stockTakeSectionRows, roundRecords]); |
|
|
|
|
|
|
|
const searchSectionDescriptionOptions = useMemo(() => { |
|
|
|
const picked = selectedSectionCodes(searchFilters.stockTakeSession).filter((code) => |
|
|
|
searchSectionCodes.includes(code) |
|
|
|
); |
|
|
|
const source = picked.length > 0 ? picked : searchSectionCodes; |
|
|
|
const desc = new Set<string>(); |
|
|
|
source.forEach((code) => { |
|
|
|
const text = sectionDescriptionByCode.get(code); |
|
|
|
if (text) desc.add(text); |
|
|
|
}); |
|
|
|
return Array.from(desc).sort((a, b) => a.localeCompare(b)); |
|
|
|
}, [searchFilters.stockTakeSession, searchSectionCodes, sectionDescriptionByCode]); |
|
|
|
|
|
|
|
const sectionCodeMenuOptions = useMemo(() => { |
|
|
|
const descFilter = searchFilters.sectionDescription !== "All" ? searchFilters.sectionDescription : ""; |
|
|
|
if (!descFilter) return searchSectionCodes; |
|
|
|
return searchSectionCodes.filter((code) => sectionDescriptionByCode.get(code) === descFilter); |
|
|
|
}, [searchFilters.sectionDescription, searchSectionCodes, sectionDescriptionByCode]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (searchKind !== "section") return; |
|
|
|
const current = searchFilters.sectionDescription; |
|
|
|
if (!current || current === "All") return; |
|
|
|
if (roundSectionCodes.length === 0) return; |
|
|
|
if (searchSectionDescriptionOptions.includes(current)) return; |
|
|
|
applyFilters({ ...searchFilters, sectionDescription: "All", searchKind: "section" }); |
|
|
|
}, [ |
|
|
|
applyFilters, |
|
|
|
roundSectionCodes.length, |
|
|
|
searchFilters, |
|
|
|
searchKind, |
|
|
|
searchSectionDescriptionOptions, |
|
|
|
]); |
|
|
|
|
|
|
|
const applyLocationPick = useCallback( |
|
|
|
(pick: PickerLocationPick) => { |
|
|
|
const location = locationQueryFromPick(pick, stockTakeLocationWarehouses); |
|
|
|
locationOwnedSectionRef.current = location.stockTakeSession || null; |
|
|
|
const next: PickerCardListFilters = { |
|
|
|
...searchFilters, |
|
|
|
searchKind: "location", |
|
|
|
}; |
|
|
|
onSearchFiltersChange(next); |
|
|
|
onAppliedFiltersChange(appliedPickerFilters(next, pick, stockTakeLocationWarehouses)); |
|
|
|
onListPageChange(0); |
|
|
|
}, |
|
|
|
[onAppliedFiltersChange, onListPageChange, onSearchFiltersChange, searchFilters, stockTakeLocationWarehouses], |
|
|
|
); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (searchKind !== "location") return; |
|
|
|
if (!locationPick.storeId && !locationPick.warehouse && !locationPick.area && !locationPick.slot) return; |
|
|
|
if (roundSectionCodes.length === 0 || locationWarehouses.length === 0) return; |
|
|
|
const floorOk = !locationPick.storeId || locationFloorOptions.includes(locationPick.storeId); |
|
|
|
const warehouseOk = !locationPick.warehouse || locationWarehouseOptions.includes(locationPick.warehouse); |
|
|
|
const areaOk = !locationPick.area || locationAreaOptions.includes(locationPick.area); |
|
|
|
const slotOk = !locationPick.slot || locationSlotOptions.includes(locationPick.slot); |
|
|
|
if (floorOk && warehouseOk && areaOk && slotOk) return; |
|
|
|
onLocationPickChange(EMPTY_PICKER_LOCATION); |
|
|
|
applyLocationPick(EMPTY_PICKER_LOCATION); |
|
|
|
}, [applyLocationPick, locationAreaOptions, locationFloorOptions, locationPick, locationSlotOptions, locationWarehouseOptions, locationWarehouses.length, onLocationPickChange, roundSectionCodes.length]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
let cancelled = false; |
|
|
|
@@ -315,7 +674,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
const sid = row.storeId?.trim(); |
|
|
|
if (sid) sectionToFloor.set(sec, sid); |
|
|
|
}); |
|
|
|
stockTakeSessions.forEach((session) => { |
|
|
|
roundRecords.forEach((session) => { |
|
|
|
const sec = session.stockTakeSession?.trim(); |
|
|
|
if (!sec) return; |
|
|
|
const sid = session.storeId?.trim(); |
|
|
|
@@ -344,7 +703,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
out.set(key, Array.from(set).sort((a, b) => a.localeCompare(b))); |
|
|
|
}); |
|
|
|
return out; |
|
|
|
}, [stockTakeSectionRows, stockTakeSessions, stockTakeSectionOptions]); |
|
|
|
}, [stockTakeSectionRows, roundRecords, stockTakeSectionOptions]); |
|
|
|
|
|
|
|
const sortedFloorKeys = useMemo(() => { |
|
|
|
const keys = Array.from(sectionsByStore.keys()); |
|
|
|
@@ -393,14 +752,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
const getCreateDialogSectionMeta = useCallback( |
|
|
|
(section: string) => { |
|
|
|
const row = stockTakeSectionRows.find((r) => r.stockTakeSection?.trim() === section); |
|
|
|
const fromList = stockTakeSessions.find((s) => s.stockTakeSession?.trim() === section); |
|
|
|
const fromList = roundRecords.find((s) => s.stockTakeSession?.trim() === section); |
|
|
|
const desc = row?.stockTakeSectionDescription?.trim() || fromList?.stockTakeSectionDescription?.trim() || ""; |
|
|
|
const area = row?.warehouseArea?.trim() || fromList?.warehouseArea?.trim() || ""; |
|
|
|
// const store = row?.storeId?.trim() || fromList?.storeId?.trim() || ""; |
|
|
|
const parts = [desc, area].filter((v) => Boolean(v && v.trim())); |
|
|
|
return parts.length ? parts.join(" / ") : ""; |
|
|
|
}, |
|
|
|
[stockTakeSectionRows, stockTakeSessions], |
|
|
|
[stockTakeSectionRows, roundRecords], |
|
|
|
); |
|
|
|
|
|
|
|
const activeCreateFloorKey = useMemo(() => { |
|
|
|
@@ -603,20 +962,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
fetchStockTakeSections() |
|
|
|
.then((sections) => { |
|
|
|
setStockTakeSectionRows(Array.isArray(sections) ? sections : []); |
|
|
|
const descSet = new Set<string>(); |
|
|
|
const sectionSet = new Set<string>(); |
|
|
|
const storeIdSet = new Set<string>(["2F", "4F"]); |
|
|
|
sections.forEach((s) => { |
|
|
|
const section = s.stockTakeSection?.trim(); |
|
|
|
if (section) sectionSet.add(section); |
|
|
|
const desc = s.stockTakeSectionDescription?.trim(); |
|
|
|
if (desc) descSet.add(desc); |
|
|
|
const storeId = s.storeId?.trim(); |
|
|
|
if (storeId) storeIdSet.add(storeId); |
|
|
|
}); |
|
|
|
setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b))); |
|
|
|
setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b))); |
|
|
|
setStoreIdOptions(Array.from(storeIdSet).sort((a, b) => a.localeCompare(b))); |
|
|
|
}) |
|
|
|
.catch((e) => { |
|
|
|
console.error("Failed to load section descriptions for filter:", e); |
|
|
|
@@ -628,23 +979,13 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
useEffect(() => { |
|
|
|
setStockTakeSectionOptions((prev) => { |
|
|
|
const sectionSet = new Set<string>(prev); |
|
|
|
stockTakeSessions.forEach((item) => { |
|
|
|
roundRecords.forEach((item) => { |
|
|
|
const section = item.stockTakeSession?.trim(); |
|
|
|
if (section) sectionSet.add(section); |
|
|
|
}); |
|
|
|
return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); |
|
|
|
}); |
|
|
|
}, [stockTakeSessions]); |
|
|
|
useEffect(() => { |
|
|
|
setStoreIdOptions((prev) => { |
|
|
|
const storeIdSet = new Set<string>([...prev, "2F", "4F"]); |
|
|
|
stockTakeSessions.forEach((item) => { |
|
|
|
const storeId = item.storeId?.trim(); |
|
|
|
if (storeId) storeIdSet.add(storeId); |
|
|
|
}); |
|
|
|
return Array.from(storeIdSet).sort((a, b) => a.localeCompare(b)); |
|
|
|
}); |
|
|
|
}, [stockTakeSessions]); |
|
|
|
}, [roundRecords]); |
|
|
|
const getStatusColor = (status: string) => { |
|
|
|
const statusLower = status.toLowerCase(); |
|
|
|
if (statusLower === "completed") return "success"; |
|
|
|
@@ -726,23 +1067,59 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
<Typography variant="overline" sx={{ display: "block", mb: 1 }}> |
|
|
|
{t("Search Criteria")} |
|
|
|
</Typography> |
|
|
|
<Grid container spacing={2}> |
|
|
|
<Grid item xs={12} md={4}> |
|
|
|
<FormControl fullWidth> |
|
|
|
<Grid container spacing={2} alignItems="flex-start"> |
|
|
|
<Grid item xs={12}> |
|
|
|
<Tabs |
|
|
|
value={searchKind} |
|
|
|
onChange={(_, value: PickerSearchKind) => applyFilters({ ...searchFilters, searchKind: value })} |
|
|
|
sx={{ minHeight: 36 }} |
|
|
|
> |
|
|
|
<Tab |
|
|
|
value="location" |
|
|
|
label={t("Search by location")} |
|
|
|
sx={{ minHeight: 36, py: 0.5, textTransform: "none" }} |
|
|
|
/> |
|
|
|
<Tab |
|
|
|
value="section" |
|
|
|
label={t("Search by stock take section")} |
|
|
|
sx={{ minHeight: 36, py: 0.5, textTransform: "none" }} |
|
|
|
/> |
|
|
|
</Tabs> |
|
|
|
</Grid> |
|
|
|
{searchKind === "section" && ( |
|
|
|
<Grid item xs={12} md={4} sx={{ order: 1 }}> |
|
|
|
<FormControl fullWidth size="small" sx={searchFieldSx}> |
|
|
|
<InputLabel>{t("Stock Take Section")}</InputLabel> |
|
|
|
<Select |
|
|
|
size="small" |
|
|
|
value={searchFilters.sectionDescription} |
|
|
|
value={ |
|
|
|
searchFilters.sectionDescription === "All" || |
|
|
|
searchSectionDescriptionOptions.includes(searchFilters.sectionDescription) |
|
|
|
? searchFilters.sectionDescription |
|
|
|
: "All" |
|
|
|
} |
|
|
|
MenuProps={searchMenuProps} |
|
|
|
label={t("Stock Take Section")} |
|
|
|
onChange={(e) => |
|
|
|
onSearchFiltersChange({ |
|
|
|
onChange={(e) => { |
|
|
|
const nextDesc = e.target.value; |
|
|
|
const allowed = new Set( |
|
|
|
nextDesc === "All" |
|
|
|
? searchSectionCodes |
|
|
|
: searchSectionCodes.filter((code) => sectionDescriptionByCode.get(code) === nextDesc) |
|
|
|
); |
|
|
|
const kept = selectedSectionCodes(searchFilters.stockTakeSession).filter((code) => allowed.has(code)); |
|
|
|
const nextSession = kept.join(","); |
|
|
|
if (nextSession !== (searchFilters.stockTakeSession || "")) { |
|
|
|
locationOwnedSectionRef.current = null; |
|
|
|
} |
|
|
|
applyFilters({ |
|
|
|
...searchFilters, |
|
|
|
sectionDescription: e.target.value, |
|
|
|
}) |
|
|
|
} |
|
|
|
sectionDescription: nextDesc, |
|
|
|
stockTakeSession: nextSession, |
|
|
|
}); |
|
|
|
}} |
|
|
|
> |
|
|
|
<MenuItem value="All">{t("All")}</MenuItem> |
|
|
|
{sectionDescriptionOptions.map((desc) => ( |
|
|
|
{searchSectionDescriptionOptions.map((desc) => ( |
|
|
|
<MenuItem key={desc} value={desc}> |
|
|
|
{desc} |
|
|
|
</MenuItem> |
|
|
|
@@ -750,40 +1127,60 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={12} md={4}> |
|
|
|
<Autocomplete |
|
|
|
freeSolo |
|
|
|
options={stockTakeSectionOptions} |
|
|
|
value={searchFilters.stockTakeSession} |
|
|
|
onChange={(_, newValue) => |
|
|
|
onSearchFiltersChange({ |
|
|
|
...searchFilters, |
|
|
|
stockTakeSession: newValue || "", |
|
|
|
}) |
|
|
|
} |
|
|
|
onInputChange={(_, newValue) => |
|
|
|
onSearchFiltersChange({ |
|
|
|
...searchFilters, |
|
|
|
stockTakeSession: newValue, |
|
|
|
}) |
|
|
|
} |
|
|
|
renderInput={(params) => ( |
|
|
|
<TextField |
|
|
|
{...params} |
|
|
|
fullWidth |
|
|
|
label={t("Stock Take Section (can use , to search multiple sections)")} |
|
|
|
/> |
|
|
|
)} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{searchKind === "section" && ( |
|
|
|
<Grid item xs={12} md={4} sx={{ order: 1 }}> |
|
|
|
<FormControl fullWidth size="small" sx={searchFieldSx}> |
|
|
|
<InputLabel>{t("Stock Take Section code")}</InputLabel> |
|
|
|
<Select |
|
|
|
multiple |
|
|
|
MenuProps={searchMenuProps} |
|
|
|
value={selectedSectionCodes(searchFilters.stockTakeSession).filter((code) => |
|
|
|
sectionCodeMenuOptions.includes(code) |
|
|
|
)} |
|
|
|
label={t("Stock Take Section code")} |
|
|
|
onChange={(e) => { |
|
|
|
const raw = e.target.value; |
|
|
|
const picked = (typeof raw === "string" ? raw.split(",") : raw) |
|
|
|
.map((part) => part.trim()) |
|
|
|
.filter(Boolean); |
|
|
|
if (locationOwnedSectionRef.current != null) locationOwnedSectionRef.current = null; |
|
|
|
const descriptions = new Set<string>(); |
|
|
|
picked.forEach((code) => { |
|
|
|
const text = sectionDescriptionByCode.get(code); |
|
|
|
if (text) descriptions.add(text); |
|
|
|
}); |
|
|
|
const nextDesc = |
|
|
|
searchFilters.sectionDescription !== "All" && |
|
|
|
picked.length > 0 && |
|
|
|
!descriptions.has(searchFilters.sectionDescription) |
|
|
|
? "All" |
|
|
|
: searchFilters.sectionDescription; |
|
|
|
applyFilters({ |
|
|
|
...searchFilters, |
|
|
|
sectionDescription: nextDesc, |
|
|
|
stockTakeSession: picked.join(","), |
|
|
|
}); |
|
|
|
}} |
|
|
|
renderValue={(selected) => (selected as string[]).join(", ")} |
|
|
|
> |
|
|
|
{sectionCodeMenuOptions.map((code) => ( |
|
|
|
<MenuItem key={code} value={code}> |
|
|
|
{code} |
|
|
|
</MenuItem> |
|
|
|
))} |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={12} md={4}> |
|
|
|
<FormControl fullWidth> |
|
|
|
)} |
|
|
|
<Grid item xs={12} md={4} sx={{ order: 2 }}> |
|
|
|
<FormControl fullWidth size="small" sx={searchFieldSx}> |
|
|
|
<InputLabel>{t("Status")}</InputLabel> |
|
|
|
<Select |
|
|
|
value={searchFilters.status} |
|
|
|
label={t("Status")} |
|
|
|
onChange={(e) => |
|
|
|
onSearchFiltersChange({ |
|
|
|
applyFilters({ |
|
|
|
...searchFilters, |
|
|
|
status: e.target.value, |
|
|
|
}) |
|
|
|
@@ -798,49 +1195,175 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={12} md={6}> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
label={t("Area")} |
|
|
|
value={searchFilters.area} |
|
|
|
onChange={(e) => |
|
|
|
onSearchFiltersChange({ |
|
|
|
...searchFilters, |
|
|
|
area: e.target.value, |
|
|
|
}) |
|
|
|
} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={12} md={6}> |
|
|
|
<FormControl fullWidth> |
|
|
|
<InputLabel>{t("Store ID")}</InputLabel> |
|
|
|
<Select |
|
|
|
value={searchFilters.storeId} |
|
|
|
label={t("Store ID")} |
|
|
|
onChange={(e) => |
|
|
|
onSearchFiltersChange({ |
|
|
|
...searchFilters, |
|
|
|
storeId: e.target.value, |
|
|
|
}) |
|
|
|
} |
|
|
|
> |
|
|
|
<MenuItem value="All">{t("All")}</MenuItem> |
|
|
|
{storeIdOptions.map((storeId) => ( |
|
|
|
<MenuItem key={storeId} value={storeId}> |
|
|
|
{storeId} |
|
|
|
</MenuItem> |
|
|
|
))} |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
{searchKind === "location" && ( |
|
|
|
<Grid item xs={12} sx={{ order: 1 }}> |
|
|
|
<Stack spacing={1.25}> |
|
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}> |
|
|
|
<Typography variant="body2" sx={{ minWidth: 72, fontWeight: 600 }}> |
|
|
|
{t("Store ID")} |
|
|
|
</Typography> |
|
|
|
<ToggleButtonGroup |
|
|
|
exclusive |
|
|
|
size="small" |
|
|
|
value={locationPick.storeId || null} |
|
|
|
onChange={(_, next: string | null) => { |
|
|
|
const pick = { storeId: next ?? "", warehouse: "", area: "", slot: "" }; |
|
|
|
onLocationPickChange(pick); |
|
|
|
applyLocationPick(pick); |
|
|
|
}} |
|
|
|
> |
|
|
|
{locationFloorOptions.map((floor) => ( |
|
|
|
<ToggleButton key={floor} value={floor} sx={{ px: 1.5, textTransform: "none" }}> |
|
|
|
{floor === LOCATION_ALL ? t("All") : floor} |
|
|
|
</ToggleButton> |
|
|
|
))} |
|
|
|
</ToggleButtonGroup> |
|
|
|
</Box> |
|
|
|
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1.5, flexWrap: "wrap" }}> |
|
|
|
<Typography variant="body2" sx={{ minWidth: 72, fontWeight: 600, mt: 0.75 }}> |
|
|
|
{t("Warehouse")} |
|
|
|
</Typography> |
|
|
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75, opacity: locationPick.storeId ? 1 : 0.45 }}> |
|
|
|
{locationWarehouseRows.map((row, rowIndex) => ( |
|
|
|
<ToggleButtonGroup |
|
|
|
key={row.join("-") || rowIndex} |
|
|
|
exclusive |
|
|
|
size="small" |
|
|
|
disabled={!locationPick.storeId} |
|
|
|
value={locationPick.storeId ? locationPick.warehouse || null : null} |
|
|
|
onChange={(_, next: string | null) => { |
|
|
|
if (next == null) return; |
|
|
|
const pick = { ...locationPick, warehouse: next, area: "", slot: "" }; |
|
|
|
onLocationPickChange(pick); |
|
|
|
applyLocationPick(pick); |
|
|
|
}} |
|
|
|
sx={{ |
|
|
|
"& .MuiToggleButtonGroup-grouped": { |
|
|
|
width: LOCATION_BUTTON_WIDTH, |
|
|
|
minWidth: LOCATION_BUTTON_WIDTH, |
|
|
|
maxWidth: LOCATION_BUTTON_WIDTH, |
|
|
|
px: 0, |
|
|
|
}, |
|
|
|
}} |
|
|
|
> |
|
|
|
{row.map((zone) => ( |
|
|
|
<ToggleButton key={zone} value={zone} sx={{ textTransform: "none" }}> |
|
|
|
{zone === LOCATION_ALL ? t("All") : zone} |
|
|
|
</ToggleButton> |
|
|
|
))} |
|
|
|
</ToggleButtonGroup> |
|
|
|
))} |
|
|
|
</Box> |
|
|
|
{!locationPick.storeId ? ( |
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.75 }}> |
|
|
|
{t("Select floor first")} |
|
|
|
</Typography> |
|
|
|
) : null} |
|
|
|
</Box> |
|
|
|
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1.5, flexWrap: "wrap" }}> |
|
|
|
<Typography variant="body2" sx={{ minWidth: 72, fontWeight: 600, mt: 0.75 }}> |
|
|
|
{t("Area")} |
|
|
|
</Typography> |
|
|
|
<Box |
|
|
|
sx={{ |
|
|
|
display: "flex", |
|
|
|
flexDirection: "column", |
|
|
|
gap: 0.75, |
|
|
|
opacity: locationPick.storeId && locationPick.warehouse ? 1 : 0.45, |
|
|
|
}} |
|
|
|
> |
|
|
|
{locationAreaRows.map((row, rowIndex) => ( |
|
|
|
<ToggleButtonGroup |
|
|
|
key={row.join("-") || rowIndex} |
|
|
|
exclusive |
|
|
|
size="small" |
|
|
|
disabled={!locationPick.storeId || !locationPick.warehouse} |
|
|
|
value={locationPick.warehouse ? locationPick.area || null : null} |
|
|
|
onChange={(_, next: string | null) => { |
|
|
|
if (next == null) return; |
|
|
|
const pick = { ...locationPick, area: next, slot: "" }; |
|
|
|
onLocationPickChange(pick); |
|
|
|
applyLocationPick(pick); |
|
|
|
}} |
|
|
|
sx={{ |
|
|
|
"& .MuiToggleButtonGroup-grouped": { |
|
|
|
width: LOCATION_BUTTON_WIDTH, |
|
|
|
minWidth: LOCATION_BUTTON_WIDTH, |
|
|
|
maxWidth: LOCATION_BUTTON_WIDTH, |
|
|
|
px: 0, |
|
|
|
}, |
|
|
|
}} |
|
|
|
> |
|
|
|
{row.map((area) => ( |
|
|
|
<ToggleButton key={area} value={area} sx={{ textTransform: "none" }}> |
|
|
|
{area === LOCATION_ALL ? t("All") : area} |
|
|
|
</ToggleButton> |
|
|
|
))} |
|
|
|
</ToggleButtonGroup> |
|
|
|
))} |
|
|
|
</Box> |
|
|
|
{!locationPick.warehouse ? ( |
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.75 }}> |
|
|
|
{locationPick.storeId ? t("Select warehouse first") : t("Select floor first")} |
|
|
|
</Typography> |
|
|
|
) : null} |
|
|
|
</Box> |
|
|
|
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1.5, flexWrap: "wrap" }}> |
|
|
|
<Typography variant="body2" sx={{ minWidth: 72, fontWeight: 600, mt: 0.75 }}> |
|
|
|
{t("Slot")} |
|
|
|
</Typography> |
|
|
|
<Box |
|
|
|
sx={{ |
|
|
|
display: "flex", |
|
|
|
flexDirection: "column", |
|
|
|
gap: 0.75, |
|
|
|
opacity: locationPick.storeId && locationPick.warehouse && locationPick.area ? 1 : 0.45, |
|
|
|
}} |
|
|
|
> |
|
|
|
{locationSlotRows.map((row, rowIndex) => ( |
|
|
|
<ToggleButtonGroup |
|
|
|
key={row.join("-") || rowIndex} |
|
|
|
exclusive |
|
|
|
size="small" |
|
|
|
disabled={!locationPick.storeId || !locationPick.warehouse || !locationPick.area} |
|
|
|
value={locationPick.area ? locationPick.slot || null : null} |
|
|
|
onChange={(_, next: string | null) => { |
|
|
|
if (next == null) return; |
|
|
|
const pick = { ...locationPick, slot: next }; |
|
|
|
onLocationPickChange(pick); |
|
|
|
applyLocationPick(pick); |
|
|
|
}} |
|
|
|
sx={{ |
|
|
|
"& .MuiToggleButtonGroup-grouped": { |
|
|
|
width: LOCATION_BUTTON_WIDTH, |
|
|
|
minWidth: LOCATION_BUTTON_WIDTH, |
|
|
|
maxWidth: LOCATION_BUTTON_WIDTH, |
|
|
|
px: 0, |
|
|
|
}, |
|
|
|
}} |
|
|
|
> |
|
|
|
{row.map((slot) => ( |
|
|
|
<ToggleButton key={slot} value={slot} sx={{ textTransform: "none" }}> |
|
|
|
{slot === LOCATION_ALL ? t("All") : slot} |
|
|
|
</ToggleButton> |
|
|
|
))} |
|
|
|
</ToggleButtonGroup> |
|
|
|
))} |
|
|
|
</Box> |
|
|
|
{!locationPick.area ? ( |
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.75 }}> |
|
|
|
{locationPick.warehouse ? t("Select area first") : locationPick.storeId ? t("Select warehouse first") : t("Select floor first")} |
|
|
|
</Typography> |
|
|
|
) : null} |
|
|
|
</Box> |
|
|
|
</Stack> |
|
|
|
</Grid> |
|
|
|
)} |
|
|
|
</Grid> |
|
|
|
<CardActions sx={{ px: 0, pt: 2, gap: 1 }}> |
|
|
|
<Button variant="outlined" onClick={handleResetSearch}> |
|
|
|
{t("Reset")} |
|
|
|
</Button> |
|
|
|
<Button variant="contained" onClick={handleSearch}> |
|
|
|
{t("Search")} |
|
|
|
</Button> |
|
|
|
</CardActions> |
|
|
|
</CardContent> |
|
|
|
</Card> |
|
|
|
|