|
|
|
@@ -4,53 +4,302 @@ import SearchBox, { Criterion } from "../SearchBox"; |
|
|
|
import { useCallback, useMemo, useState } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import SearchResults, { Column } from "../SearchResults/index"; |
|
|
|
import { StockIssueResult } from "./action"; |
|
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
|
import { |
|
|
|
batchSubmitBadItem, |
|
|
|
batchSubmitExpiryItem, |
|
|
|
batchSubmitMissItem, |
|
|
|
ExpiryItemResult, |
|
|
|
StockIssueLists, |
|
|
|
StockIssueResult, |
|
|
|
submitBadItem, |
|
|
|
submitExpiryItem, |
|
|
|
submitMissItem, |
|
|
|
} from "@/app/api/stockIssue/actions"; |
|
|
|
import { Box, Button, Tab, Tabs } from "@mui/material"; |
|
|
|
import { useSession } from "next-auth/react"; |
|
|
|
|
|
|
|
interface Props { |
|
|
|
dataList: StockIssueResult[]; |
|
|
|
dataList: StockIssueLists; |
|
|
|
} |
|
|
|
|
|
|
|
type SearchQuery = { |
|
|
|
lotNo: string; |
|
|
|
}; |
|
|
|
type SearchQuery = Partial<Omit<StockIssueResult, "id">>; |
|
|
|
type SearchParamNames = keyof SearchQuery; |
|
|
|
|
|
|
|
const SearchPage: React.FC<Props> = ({dataList}) => { |
|
|
|
const { t } = useTranslation("user"); |
|
|
|
const [filteredList, setFilteredList] = useState(dataList); |
|
|
|
|
|
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( |
|
|
|
() => [ |
|
|
|
{ |
|
|
|
label: t("Lot No."), |
|
|
|
paramName: "lotNo", |
|
|
|
type: "text", |
|
|
|
}, |
|
|
|
], |
|
|
|
[t], |
|
|
|
const SearchPage: React.FC<Props> = ({ dataList }) => { |
|
|
|
const { t } = useTranslation("inventory"); |
|
|
|
const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss"); |
|
|
|
const [search, setSearch] = useState<SearchQuery>({ lotNo: "" }); |
|
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
|
const [missItems, setMissItems] = useState<StockIssueResult[]>( |
|
|
|
dataList.missItems, |
|
|
|
); |
|
|
|
const [badItems, setBadItems] = useState<StockIssueResult[]>( |
|
|
|
dataList.badItems, |
|
|
|
); |
|
|
|
const [expiryItems, setExpiryItems] = useState<ExpiryItemResult[]>( |
|
|
|
dataList.expiryItems, |
|
|
|
); |
|
|
|
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]); |
|
|
|
const [submittingIds, setSubmittingIds] = useState<Set<number>>(new Set()); |
|
|
|
const [batchSubmitting, setBatchSubmitting] = useState(false); |
|
|
|
|
|
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( |
|
|
|
() => [ |
|
|
|
{ |
|
|
|
label: t("Lot No."), |
|
|
|
paramName: "lotNo", |
|
|
|
type: "text", |
|
|
|
}, |
|
|
|
], |
|
|
|
[t], |
|
|
|
); |
|
|
|
|
|
|
|
const filterBySearch = useCallback( |
|
|
|
<T extends { lotNo: string | null }>(items: T[]): T[] => { |
|
|
|
if (!search.lotNo) return items; |
|
|
|
const keyword = search.lotNo.toLowerCase(); |
|
|
|
return items.filter( |
|
|
|
(i) => i.lotNo && i.lotNo.toLowerCase().includes(keyword), |
|
|
|
); |
|
|
|
}, |
|
|
|
[search.lotNo], |
|
|
|
); |
|
|
|
|
|
|
|
const handleSubmitSingle = useCallback( |
|
|
|
async (id: number) => { |
|
|
|
if (!currentUserId) { |
|
|
|
alert(t("User ID is required")); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const columns = useMemo<Column<StockIssueResult>[]>( |
|
|
|
setSubmittingIds((prev) => new Set(prev).add(id)); |
|
|
|
try { |
|
|
|
if (tab === "miss") { |
|
|
|
await submitMissItem(id, currentUserId); |
|
|
|
setMissItems((prev) => prev.filter((i) => i.id !== id)); |
|
|
|
} else if (tab === "bad") { |
|
|
|
await submitBadItem(id, currentUserId); |
|
|
|
setBadItems((prev) => prev.filter((i) => i.id !== id)); |
|
|
|
} else { |
|
|
|
await submitExpiryItem(id, currentUserId); |
|
|
|
setExpiryItems((prev) => prev.filter((i) => i.id !== id)); |
|
|
|
} |
|
|
|
// Remove from selectedIds if it was selected |
|
|
|
setSelectedIds((prev) => prev.filter((selectedId) => selectedId !== id)); |
|
|
|
} catch (error) { |
|
|
|
console.error("Failed to submit item:", error); |
|
|
|
alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); |
|
|
|
} finally { |
|
|
|
setSubmittingIds((prev) => { |
|
|
|
const newSet = new Set(prev); |
|
|
|
newSet.delete(id); |
|
|
|
return newSet; |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
[tab, currentUserId, t], |
|
|
|
); |
|
|
|
|
|
|
|
const handleSubmitSelected = useCallback(async () => { |
|
|
|
if (!currentUserId) return; |
|
|
|
|
|
|
|
// Get all IDs from the current tab's filtered items |
|
|
|
let allIds: number[] = []; |
|
|
|
if (tab === "miss") { |
|
|
|
const items = filterBySearch(missItems); |
|
|
|
allIds = items.map((item) => item.id); |
|
|
|
} else if (tab === "bad") { |
|
|
|
const items = filterBySearch(badItems); |
|
|
|
allIds = items.map((item) => item.id); |
|
|
|
} else { |
|
|
|
const items = filterBySearch(expiryItems); |
|
|
|
allIds = items.map((item) => item.id); |
|
|
|
} |
|
|
|
|
|
|
|
if (allIds.length === 0) return; |
|
|
|
|
|
|
|
setBatchSubmitting(true); |
|
|
|
try { |
|
|
|
if (tab === "miss") { |
|
|
|
await batchSubmitMissItem(allIds, currentUserId); |
|
|
|
setMissItems((prev) => prev.filter((i) => !allIds.includes(i.id))); |
|
|
|
} else if (tab === "bad") { |
|
|
|
await batchSubmitBadItem(allIds, currentUserId); |
|
|
|
setBadItems((prev) => prev.filter((i) => !allIds.includes(i.id))); |
|
|
|
} else { |
|
|
|
await batchSubmitExpiryItem(allIds, currentUserId); |
|
|
|
setExpiryItems((prev) => prev.filter((i) => !allIds.includes(i.id))); |
|
|
|
} |
|
|
|
|
|
|
|
setSelectedIds([]); |
|
|
|
} catch (error) { |
|
|
|
console.error("Failed to submit selected items:", error); |
|
|
|
alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); |
|
|
|
} finally { |
|
|
|
setBatchSubmitting(false); |
|
|
|
} |
|
|
|
}, [tab, currentUserId, missItems, badItems, expiryItems, filterBySearch]); |
|
|
|
|
|
|
|
const missColumns = useMemo<Column<StockIssueResult>[]>( |
|
|
|
() => [ |
|
|
|
{ name: "itemCode", label: t("Item Code") }, |
|
|
|
{ name: "itemDescription", label: t("Item") }, |
|
|
|
{ name: "lotNo", label: t("Lot No.") }, |
|
|
|
{ name: "storeLocation", label: t("Location") }, |
|
|
|
{ name: "badItemQty", label: t("Defective Qty") } |
|
|
|
{ name: "missQty", label: t("Miss Qty") }, |
|
|
|
{ |
|
|
|
name: "id", |
|
|
|
label: t("Action"), |
|
|
|
renderCell: (item) => ( |
|
|
|
<Button |
|
|
|
size="small" |
|
|
|
variant="contained" |
|
|
|
color="primary" |
|
|
|
onClick={() => handleSubmitSingle(item.id)} |
|
|
|
disabled={submittingIds.has(item.id) || !currentUserId} |
|
|
|
> |
|
|
|
{submittingIds.has(item.id) ? t("Processing...") : t("Looked")} |
|
|
|
</Button> |
|
|
|
), |
|
|
|
}, |
|
|
|
], |
|
|
|
[t], |
|
|
|
[t, handleSubmitSingle, submittingIds, currentUserId], |
|
|
|
); |
|
|
|
|
|
|
|
const badColumns = useMemo<Column<StockIssueResult>[]>( |
|
|
|
() => [ |
|
|
|
{ name: "itemCode", label: t("Item Code") }, |
|
|
|
{ name: "itemDescription", label: t("Item") }, |
|
|
|
{ name: "lotNo", label: t("Lot No.") }, |
|
|
|
{ name: "storeLocation", label: t("Location") }, |
|
|
|
{ name: "badItemQty", label: t("Defective Qty") }, |
|
|
|
{ |
|
|
|
name: "id", |
|
|
|
label: t("Action"), |
|
|
|
renderCell: (item) => ( |
|
|
|
<Button |
|
|
|
size="small" |
|
|
|
variant="contained" |
|
|
|
color="primary" |
|
|
|
onClick={() => handleSubmitSingle(item.id)} |
|
|
|
disabled={submittingIds.has(item.id) || !currentUserId} |
|
|
|
> |
|
|
|
{submittingIds.has(item.id) ? t("Disposing...") : t("Disposed")} |
|
|
|
</Button> |
|
|
|
), |
|
|
|
}, |
|
|
|
], |
|
|
|
[t, handleSubmitSingle, submittingIds, currentUserId], |
|
|
|
); |
|
|
|
|
|
|
|
const expiryColumns = useMemo<Column<ExpiryItemResult>[]>( |
|
|
|
() => [ |
|
|
|
{ name: "itemCode", label: t("Item Code") }, |
|
|
|
{ name: "itemDescription", label: t("Item") }, |
|
|
|
{ name: "lotNo", label: t("Lot No.") }, |
|
|
|
{ name: "storeLocation", label: t("Location") }, |
|
|
|
{ name: "expiryDate", label: t("Expiry Date") }, |
|
|
|
{ name: "remainingQty", label: t("Remaining Qty") }, |
|
|
|
{ |
|
|
|
name: "id", |
|
|
|
label: t("Action"), |
|
|
|
renderCell: (item) => ( |
|
|
|
<Button |
|
|
|
size="small" |
|
|
|
variant="contained" |
|
|
|
color="primary" |
|
|
|
onClick={() => handleSubmitSingle(item.id)} |
|
|
|
disabled={submittingIds.has(item.id) || !currentUserId} |
|
|
|
> |
|
|
|
{submittingIds.has(item.id) ? t("Disposing...") : t("Disposed")} |
|
|
|
</Button> |
|
|
|
), |
|
|
|
}, |
|
|
|
], |
|
|
|
[t, handleSubmitSingle, submittingIds, currentUserId], |
|
|
|
); |
|
|
|
|
|
|
|
const handleSearch = useCallback((query: Record<SearchParamNames, string>) => { |
|
|
|
setSearch(query); |
|
|
|
}, []); |
|
|
|
|
|
|
|
const handleTabChange = useCallback( |
|
|
|
(_: React.SyntheticEvent, value: string) => { |
|
|
|
setTab(value as "miss" | "bad" | "expiry"); |
|
|
|
setSelectedIds([]); |
|
|
|
}, |
|
|
|
[], |
|
|
|
); |
|
|
|
|
|
|
|
const renderCurrentTab = () => { |
|
|
|
if (tab === "miss") { |
|
|
|
const items = filterBySearch(missItems); |
|
|
|
return ( |
|
|
|
<SearchResults<StockIssueResult> |
|
|
|
items={items} |
|
|
|
columns={missColumns} |
|
|
|
pagingController={{ pageNum: 1, pageSize: 10 }} |
|
|
|
checkboxIds={selectedIds} |
|
|
|
setCheckboxIds={setSelectedIds} |
|
|
|
/> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
if (tab === "bad") { |
|
|
|
const items = filterBySearch(badItems); |
|
|
|
return ( |
|
|
|
<SearchResults<StockIssueResult> |
|
|
|
items={items} |
|
|
|
columns={badColumns} |
|
|
|
pagingController={{ pageNum: 1, pageSize: 10 }} |
|
|
|
checkboxIds={selectedIds} |
|
|
|
setCheckboxIds={setSelectedIds} |
|
|
|
/> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const items = filterBySearch(expiryItems); |
|
|
|
return ( |
|
|
|
<SearchResults<ExpiryItemResult> |
|
|
|
items={items} |
|
|
|
columns={expiryColumns} |
|
|
|
pagingController={{ pageNum: 1, pageSize: 10 }} |
|
|
|
checkboxIds={selectedIds} |
|
|
|
setCheckboxIds={setSelectedIds} |
|
|
|
/> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
return ( |
|
|
|
<> |
|
|
|
<SearchBox |
|
|
|
<Box> |
|
|
|
<Tabs value={tab} onChange={handleTabChange} sx={{ mb: 2 }}> |
|
|
|
<Tab value="miss" label={t("Miss Item")} /> |
|
|
|
<Tab value="bad" label={t("Bad Item")} /> |
|
|
|
<Tab value="expiry" label={t("Expiry Item")} /> |
|
|
|
</Tabs> |
|
|
|
|
|
|
|
<SearchBox<SearchParamNames> |
|
|
|
criteria={searchCriteria} |
|
|
|
onSearch={(query) => { |
|
|
|
}} |
|
|
|
/> |
|
|
|
<SearchResults<StockIssueResult> |
|
|
|
items={filteredList} |
|
|
|
columns={columns} |
|
|
|
pagingController={{ pageNum: 1, pageSize: 10 }} |
|
|
|
onSearch={handleSearch} |
|
|
|
/> |
|
|
|
</> |
|
|
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="primary" |
|
|
|
onClick={handleSubmitSelected} |
|
|
|
disabled={batchSubmitting || !currentUserId} |
|
|
|
> |
|
|
|
{batchSubmitting ? tab === "miss" ? t("Processing...") : tab === "bad" ? t("Disposing...") : t("Disposing...") : t("Batch Disposed All")} |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
|
|
|
|
{renderCurrentTab()} |
|
|
|
</Box> |
|
|
|
); |
|
|
|
}; |
|
|
|
|