@@ -12,8 +12,8 @@ export interface PickOrderResult{ | |||||
id: number, | id: number, | ||||
code: string, | code: string, | ||||
consoCode?: string, | consoCode?: string, | ||||
targetDate: string, | |||||
completeDate?: string, | |||||
targetDate: number[], | |||||
completeDate?: number[], | |||||
type: string, | type: string, | ||||
status: string, | status: string, | ||||
releasedBy: string, | releasedBy: string, | ||||
@@ -1,4 +1,6 @@ | |||||
import dayjs, { ConfigType } from "dayjs"; | |||||
import { Uom } from "../api/settings/uom"; | import { Uom } from "../api/settings/uom"; | ||||
import { ListIterateeCustom, every, isArray, isNaN, isNull, isUndefined, take } from "lodash"; | |||||
export const manhourFormatter = new Intl.NumberFormat("en-HK", { | export const manhourFormatter = new Intl.NumberFormat("en-HK", { | ||||
minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
@@ -24,6 +26,29 @@ export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; | |||||
export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | 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 } = { | export const stockInLineStatusMap: { [status: string]: number } = { | ||||
"draft": 0, | "draft": 0, | ||||
"pending": 1, | "pending": 1, | ||||
@@ -5,7 +5,9 @@ import { useCallback, useMemo, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import SearchResults, { Column } from "../SearchResults"; | 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 { | interface Props { | ||||
pickOrders: PickOrderResult[]; | pickOrders: PickOrderResult[]; | ||||
@@ -32,20 +34,20 @@ const PickOrderSearch: React.FC<Props> = ({ | |||||
label: t("Type"), paramName: "type", type: "autocomplete", | label: t("Type"), paramName: "type", type: "autocomplete", | ||||
options: sortBy( | options: sortBy( | ||||
uniqBy(pickOrders.map((po) => ({ value: po.type, label: t(upperCase(po.type)) })), "value"), | 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", | label: t("Status"), paramName: "status", type: "autocomplete", | ||||
options: sortBy( | options: sortBy( | ||||
uniqBy(pickOrders.map((po) => ({ value: po.status, label: t(upperFirst(po.status)) })), "value"), | 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]) | ], [t]) | ||||
const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
@@ -60,10 +62,16 @@ const PickOrderSearch: React.FC<Props> = ({ | |||||
{ | { | ||||
name: "consoCode", | name: "consoCode", | ||||
label: t("Consolidated Code"), | label: t("Consolidated Code"), | ||||
renderCell: (params) => { | |||||
return params.consoCode ?? "N/A" | |||||
} | |||||
}, | }, | ||||
{ | { | ||||
name: "type", | name: "type", | ||||
label: t("type"), | label: t("type"), | ||||
renderCell: (params) => { | |||||
return upperCase(params.type) | |||||
} | |||||
}, | }, | ||||
{ | { | ||||
name: "items", | name: "items", | ||||
@@ -72,6 +80,13 @@ const PickOrderSearch: React.FC<Props> = ({ | |||||
return params.items?.map((i) => i.name).join(", ") | return params.items?.map((i) => i.name).join(", ") | ||||
} | } | ||||
}, | }, | ||||
{ | |||||
name: "targetDate", | |||||
label: t("Target Date"), | |||||
renderCell: (params) => { | |||||
return arrayToDateString(params.targetDate) | |||||
} | |||||
}, | |||||
{ | { | ||||
name: "releasedBy", | name: "releasedBy", | ||||
label: t("Released By"), | label: t("Released By"), | ||||
@@ -79,6 +94,9 @@ const PickOrderSearch: React.FC<Props> = ({ | |||||
{ | { | ||||
name: "status", | name: "status", | ||||
label: t("Status"), | label: t("Status"), | ||||
renderCell: (params) => { | |||||
return upperFirst(params.status) | |||||
} | |||||
}, | }, | ||||
], [t]) | ], [t]) | ||||
@@ -89,8 +107,17 @@ const PickOrderSearch: React.FC<Props> = ({ | |||||
onSearch={(query) => { | onSearch={(query) => { | ||||
setFilteredPickOrders( | setFilteredPickOrders( | ||||
pickOrders.filter( | 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; | handleSelectionChange: (selectedOptions: T[]) => void; | ||||
} | } | ||||
// TODO: Add group | |||||
interface AutocompleteOptions { | interface AutocompleteOptions { | ||||
value: string | number; | value: string | number; | ||||
label: string; | label: string; | ||||
@@ -83,7 +82,10 @@ export type Criterion<T extends string> = | |||||
interface Props<T extends string> { | interface Props<T extends string> { | ||||
criteria: Criterion<T>[]; | 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; | onReset?: () => void; | ||||
} | } | ||||
@@ -93,14 +95,15 @@ function SearchBox<T extends string>({ | |||||
onReset, | onReset, | ||||
}: Props<T>) { | }: Props<T>) { | ||||
const { t } = useTranslation("common"); | 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( | const defaultInputs = useMemo( | ||||
() => | () => | ||||
criteria.reduce<Record<T, string>>( | criteria.reduce<Record<T, string>>( | ||||
(acc, c) => { | (acc, c) => { | ||||
return { | return { | ||||
...acc, | ...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>, | {} as Record<T, string>, | ||||
@@ -168,7 +171,6 @@ function SearchBox<T extends string>({ | |||||
<Typography variant="overline">{t("Search Criteria")}</Typography> | <Typography variant="overline">{t("Search Criteria")}</Typography> | ||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
{criteria.map((c) => { | {criteria.map((c) => { | ||||
console.log(c.options) | |||||
return ( | return ( | ||||
<Grid key={c.paramName} item xs={6}> | <Grid key={c.paramName} item xs={6}> | ||||
{c.type === "text" && ( | {c.type === "text" && ( | ||||
@@ -216,7 +218,7 @@ function SearchBox<T extends string>({ | |||||
noOptionsText={c.noOptionsText ?? t("No options")} | noOptionsText={c.noOptionsText ?? t("No options")} | ||||
disableClearable | disableClearable | ||||
fullWidth | 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 ?? "") | return option.value === (v ?? "") | ||||
}) | }) | ||||
: c.options.find((option) => option.value === inputs[c.paramName]) ?? defaultAll} | : c.options.find((option) => option.value === inputs[c.paramName]) ?? defaultAll} | ||||