@@ -12,8 +12,8 @@ export interface PickOrderResult{ | |||
id: number, | |||
code: string, | |||
consoCode?: string, | |||
targetDate: string, | |||
completeDate?: string, | |||
targetDate: number[], | |||
completeDate?: number[], | |||
type: string, | |||
status: string, | |||
releasedBy: string, | |||
@@ -1,4 +1,6 @@ | |||
import dayjs, { ConfigType } from "dayjs"; | |||
import { Uom } from "../api/settings/uom"; | |||
import { ListIterateeCustom, every, isArray, isNaN, isNull, isUndefined, take } from "lodash"; | |||
export const manhourFormatter = new Intl.NumberFormat("en-HK", { | |||
minimumFractionDigits: 2, | |||
@@ -24,6 +26,29 @@ export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; | |||
export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | |||
export const arrayToDayjs = (arr: ConfigType | (number | undefined)[]) => { | |||
const isValidNumber = (item: ListIterateeCustom<number | undefined, boolean>): boolean => | |||
typeof item === "number" && !isNaN(item) && isFinite(item) | |||
let tempArr = arr; | |||
if (isArray(arr) && every(arr, isValidNumber) && arr.length >= 3) { | |||
// [year, month, day] | |||
tempArr = take(arr, 3) | |||
} | |||
return dayjs(tempArr as ConfigType) | |||
} | |||
export const arrayToDateString = (arr: ConfigType | (number | undefined)[]) => { | |||
return arrayToDayjs(arr).format(OUTPUT_DATE_FORMAT) | |||
} | |||
export const dateStringToDayjs = (date: string) => { | |||
// Format: YYYY/MM/DD | |||
return dayjs(date, OUTPUT_DATE_FORMAT) | |||
} | |||
export const stockInLineStatusMap: { [status: string]: number } = { | |||
"draft": 0, | |||
"pending": 1, | |||
@@ -5,7 +5,9 @@ import { useCallback, useMemo, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
import { groupBy, map, sortBy, sortedUniq, uniqBy, upperCase, upperFirst } from "lodash"; | |||
import { flatten, groupBy, intersectionWith, isEmpty, map, sortBy, sortedUniq, uniqBy, upperCase, upperFirst } from "lodash"; | |||
import { arrayToDateString, arrayToDayjs, dateStringToDayjs } from "@/app/utils/formatUtil"; | |||
import dayjs from "dayjs"; | |||
interface Props { | |||
pickOrders: PickOrderResult[]; | |||
@@ -32,20 +34,20 @@ const PickOrderSearch: React.FC<Props> = ({ | |||
label: t("Type"), paramName: "type", type: "autocomplete", | |||
options: sortBy( | |||
uniqBy(pickOrders.map((po) => ({ value: po.type, label: t(upperCase(po.type)) })), "value"), | |||
"label").map((po) => (po)) | |||
"label") | |||
}, | |||
{ | |||
label: t("Status"), paramName: "status", type: "autocomplete", | |||
options: sortBy( | |||
uniqBy(pickOrders.map((po) => ({ value: po.status, label: t(upperFirst(po.status)) })), "value"), | |||
"label").map((po) => (po)) | |||
"label") | |||
}, | |||
{ | |||
label: t("Items"), paramName: "items", type: "autocomplete", multiple: true, | |||
options: uniqBy(flatten(sortBy( | |||
pickOrders.map((po) => po.items ? po.items.map((item) => ({ value: item.name, label: item.name, group: item.type })): []), | |||
"label")), "value") | |||
}, | |||
// { | |||
// label: t("Items"), paramName: "items", type: "autocomplete", multiple: true, | |||
// options: sortBy( | |||
// uniqBy(pickOrders.map((po) => po.items?.map((item) => ({ value: item.name, label: item.name, group: item.type }))), "value"), | |||
// "label") | |||
// }, | |||
], [t]) | |||
const onReset = useCallback(() => { | |||
@@ -60,10 +62,16 @@ const PickOrderSearch: React.FC<Props> = ({ | |||
{ | |||
name: "consoCode", | |||
label: t("Consolidated Code"), | |||
renderCell: (params) => { | |||
return params.consoCode ?? "N/A" | |||
} | |||
}, | |||
{ | |||
name: "type", | |||
label: t("type"), | |||
renderCell: (params) => { | |||
return upperCase(params.type) | |||
} | |||
}, | |||
{ | |||
name: "items", | |||
@@ -72,6 +80,13 @@ const PickOrderSearch: React.FC<Props> = ({ | |||
return params.items?.map((i) => i.name).join(", ") | |||
} | |||
}, | |||
{ | |||
name: "targetDate", | |||
label: t("Target Date"), | |||
renderCell: (params) => { | |||
return arrayToDateString(params.targetDate) | |||
} | |||
}, | |||
{ | |||
name: "releasedBy", | |||
label: t("Released By"), | |||
@@ -79,6 +94,9 @@ const PickOrderSearch: React.FC<Props> = ({ | |||
{ | |||
name: "status", | |||
label: t("Status"), | |||
renderCell: (params) => { | |||
return upperFirst(params.status) | |||
} | |||
}, | |||
], [t]) | |||
@@ -89,8 +107,17 @@ const PickOrderSearch: React.FC<Props> = ({ | |||
onSearch={(query) => { | |||
setFilteredPickOrders( | |||
pickOrders.filter( | |||
(po) => | |||
po.code.toLowerCase().includes(query.code.toLowerCase()) | |||
(po) =>{ | |||
const poTargetDateStr = arrayToDayjs(po.targetDate) | |||
// console.log(intersectionWith(po.items?.map(item => item.name), query.items)) | |||
return po.code.toLowerCase().includes(query.code.toLowerCase()) | |||
&& (isEmpty(query.targetDate) || poTargetDateStr.isSame(query.targetDate) || poTargetDateStr.isAfter(query.targetDate)) | |||
&& (isEmpty(query.targetDateTo) || poTargetDateStr.isSame(query.targetDateTo) || poTargetDateStr.isBefore(query.targetDateTo)) | |||
&& (intersectionWith(["All"], query.items).length > 0 || intersectionWith(po.items?.map(item => item.name), query.items).length > 0) | |||
&& (query.status.toLowerCase() == "all" || po.status.toLowerCase().includes(query.status.toLowerCase())) | |||
&& (query.type.toLowerCase() == "all" || po.type.toLowerCase().includes(query.type.toLowerCase())) | |||
} | |||
) | |||
) | |||
}} | |||
@@ -50,7 +50,6 @@ interface MultiSelectCriterion<T extends string> extends BaseCriterion<T> { | |||
handleSelectionChange: (selectedOptions: T[]) => void; | |||
} | |||
// TODO: Add group | |||
interface AutocompleteOptions { | |||
value: string | number; | |||
label: string; | |||
@@ -83,7 +82,10 @@ export type Criterion<T extends string> = | |||
interface Props<T extends string> { | |||
criteria: Criterion<T>[]; | |||
onSearch: (inputs: Record<T, string>) => void; | |||
// TODO: may need to check the type is "autocomplete" and "multiple" = true, then allow string[]. | |||
// TODO: may need to check the type is "dateRange", then add T and `${T}To` in the same time. | |||
// onSearch: (inputs: Record<T | (Criterion<T>["type"] extends "dateRange" ? `${T}To` : never), string>) => void; | |||
onSearch: (inputs: Record<T | `${T}To`, string>) => void; | |||
onReset?: () => void; | |||
} | |||
@@ -93,14 +95,15 @@ function SearchBox<T extends string>({ | |||
onReset, | |||
}: Props<T>) { | |||
const { t } = useTranslation("common"); | |||
const defaultAll = { value: "All", label: t("All"), group: t("All") } | |||
const defaultAll: AutocompleteOptions = { value: "All", label: t("All"), group: t("All") } | |||
const defaultInputs = useMemo( | |||
() => | |||
criteria.reduce<Record<T, string>>( | |||
(acc, c) => { | |||
return { | |||
...acc, | |||
[c.paramName]: (c.type === "select" || c.type === "autocomplete" ? "All" : "") | |||
[c.paramName]: (c.type === "select" || (c.type === "autocomplete" && !Boolean(c.multiple)) ? "All" | |||
: (c.type === "autocomplete" && Boolean(c.multiple)) ? [defaultAll.value]: "") | |||
}; | |||
}, | |||
{} as Record<T, string>, | |||
@@ -168,7 +171,6 @@ function SearchBox<T extends string>({ | |||
<Typography variant="overline">{t("Search Criteria")}</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
{criteria.map((c) => { | |||
console.log(c.options) | |||
return ( | |||
<Grid key={c.paramName} item xs={6}> | |||
{c.type === "text" && ( | |||
@@ -216,7 +218,7 @@ function SearchBox<T extends string>({ | |||
noOptionsText={c.noOptionsText ?? t("No options")} | |||
disableClearable | |||
fullWidth | |||
value={c.multiple ? intersectionWith(c.options, inputs[c.paramName], (option, v) => { | |||
value={c.multiple ? intersectionWith([defaultAll, ...c.options], inputs[c.paramName], (option, v) => { | |||
return option.value === (v ?? "") | |||
}) | |||
: c.options.find((option) => option.value === inputs[c.paramName]) ?? defaultAll} | |||