| @@ -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} | |||