diff --git a/src/app/api/pickOrder/index.ts b/src/app/api/pickOrder/index.ts index 45ad8d6..6958b98 100644 --- a/src/app/api/pickOrder/index.ts +++ b/src/app/api/pickOrder/index.ts @@ -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, diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 044d6b6..a882bbc 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -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): 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, diff --git a/src/components/PickOrderSearch/PickOrderSearch.tsx b/src/components/PickOrderSearch/PickOrderSearch.tsx index b825bfd..61509d4 100644 --- a/src/components/PickOrderSearch/PickOrderSearch.tsx +++ b/src/components/PickOrderSearch/PickOrderSearch.tsx @@ -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 = ({ 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 = ({ { 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 = ({ 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 = ({ { name: "status", label: t("Status"), + renderCell: (params) => { + return upperFirst(params.status) + } }, ], [t]) @@ -89,8 +107,17 @@ const PickOrderSearch: React.FC = ({ 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())) + } ) ) }} diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 2aea17a..e35c994 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -50,7 +50,6 @@ interface MultiSelectCriterion extends BaseCriterion { handleSelectionChange: (selectedOptions: T[]) => void; } -// TODO: Add group interface AutocompleteOptions { value: string | number; label: string; @@ -83,7 +82,10 @@ export type Criterion = interface Props { criteria: Criterion[]; - onSearch: (inputs: Record) => 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["type"] extends "dateRange" ? `${T}To` : never), string>) => void; + onSearch: (inputs: Record) => void; onReset?: () => void; } @@ -93,14 +95,15 @@ function SearchBox({ onReset, }: Props) { 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>( (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, @@ -168,7 +171,6 @@ function SearchBox({ {t("Search Criteria")} {criteria.map((c) => { - console.log(c.options) return ( {c.type === "text" && ( @@ -216,7 +218,7 @@ function SearchBox({ 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}