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