diff --git a/package.json b/package.json index 7780a2b..a8e487c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@mui/x-date-pickers": "^6.18.7", "@unly/universal-language-detector": "^2.0.3", "apexcharts": "^3.45.2", + "axios": "^1.9.0", "dayjs": "^1.11.10", "i18next": "^23.7.11", "i18next-resources-to-backend": "^1.2.0", @@ -28,6 +29,7 @@ "next": "14.0.4", "next-auth": "^4.24.5", "next-pwa": "^5.6.0", + "qs": "^6.14.0", "react": "^18", "react-apexcharts": "^1.4.1", "react-dom": "^18", diff --git a/src/app/(main)/settings/rss/page.tsx b/src/app/(main)/settings/rss/page.tsx index 2bf97dd..d4af0d4 100644 --- a/src/app/(main)/settings/rss/page.tsx +++ b/src/app/(main)/settings/rss/page.tsx @@ -8,6 +8,8 @@ import Typography from "@mui/material/Typography"; import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; +import RoughScheduleLoading from "@/components/RoughScheduleSetting/RoughScheduleLoading"; +import RoughScheduleSetting from "@/components/RoughScheduleSetting/RoughScheduleSetting"; export const metadata: Metadata = { title: "Rough Schedule Setting", @@ -38,8 +40,8 @@ const roughScheduleSetting: React.FC = async () => { {t("Create product")} */} - }> - + }> + ); diff --git a/src/components/RoughScheduleSetting/RoughScheduleLoading.tsx b/src/components/RoughScheduleSetting/RoughScheduleLoading.tsx new file mode 100644 index 0000000..62db673 --- /dev/null +++ b/src/components/RoughScheduleSetting/RoughScheduleLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const RoughScheduleLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default RoughScheduleLoading; diff --git a/src/components/RoughScheduleSetting/RoughScheduleSetting.tsx b/src/components/RoughScheduleSetting/RoughScheduleSetting.tsx index 2416dca..c42b687 100644 --- a/src/components/RoughScheduleSetting/RoughScheduleSetting.tsx +++ b/src/components/RoughScheduleSetting/RoughScheduleSetting.tsx @@ -1,191 +1,185 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import React, {useCallback, useEffect, useMemo, useState} from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { ItemsResult} from "@/app/api/settings/item"; +import SearchResults, { Column } from "../SearchResults"; +import { EditNote } from "@mui/icons-material"; import { useRouter, useSearchParams } from "next/navigation"; - -import { - FormProvider, - SubmitErrorHandler, - SubmitHandler, - useForm, -} from "react-hook-form"; -import { deleteDialog } from "../Swal/CustomAlerts"; -import { Box, Button, Grid, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; -import { Check, Close, EditNote } from "@mui/icons-material"; -import { useGridApiRef } from "@mui/x-data-grid"; -import {CreateItemInputs, saveItem} from "@/app/api/settings/item/actions"; -import {saveItemQcChecks} from "@/app/api/settings/qcCheck/actions"; -import {useTranslation} from "react-i18next/index"; -import {ItemQc} from "@/app/api/settings/item"; +import { GridDeleteIcon } from "@mui/x-data-grid"; +import { TypeEnum } from "@/app/utils/typeEnum"; +import axios from "axios"; +import {BASE_API_URL, NEXT_PUBLIC_API_URL} from "@/config/api"; +import { useTranslation } from "react-i18next"; +import axiosInstance from "@/app/(main)/axios/axiosInstance"; +import Qs from 'qs'; // Make sure to import Qs type Props = { - isEditMode: boolean; - // type: TypeEnum; - defaultValues: Partial | undefined; - qcChecks: ItemQc[] + items: ItemsResult[]; }; +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; -const CreateItem: React.FC = ({ - isEditMode, - // type, - defaultValues, - qcChecks - }) => { - // console.log(type) - const apiRef = useGridApiRef(); - const params = useSearchParams() - console.log(params.get("id")) - const [serverError, setServerError] = useState(""); - const [tabIndex, setTabIndex] = useState(0); - const { t } = useTranslation(); +const RSSOverview: React.FC = ({ items }) => { + const [filteredItems, setFilteredItems] = useState(items ?? []); + const { t } = useTranslation("items"); const router = useRouter(); - const title = "Product / Material" + const [filterObj, setFilterObj] = useState({}); + const [tempSelectedValue, setTempSelectedValue] = useState({}); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + totalCount: 0, + }) + const [mode, redirPath] = useMemo(() => { // var typeId = TypeEnum.CONSUMABLE_ID var title = ""; - var mode = ""; + var mode = "Search"; var redirPath = ""; - // if (type === TypeEnum.MATERIAL) { - // typeId = TypeEnum.MATERIAL_ID - // title = "Material"; - // redirPath = "/settings/material"; - // } - // if (type === TypeEnum.PRODUCT) { - // typeId = TypeEnum.PRODUCT_ID - title = "Rough Schedule"; + title = "Product"; redirPath = "/settings/rss"; - // } - // if (type === TypeEnum.BYPRODUCT) { - // typeId = TypeEnum.BYPRODUCT_ID - // title = "By-Product"; - // redirPath = "/settings/byProduct"; - // } - if (isEditMode) { - mode = "Edit"; - } else { - mode = "Create"; - } return [mode, redirPath]; - }, [isEditMode]); - // console.log(typeId) - const formProps = useForm({ - defaultValues: defaultValues ? defaultValues : { - }, - }); - const errors = formProps.formState.errors; + }, []); - const handleCancel = () => { - router.replace(`/settings/product`); + const handleSelectionChange = (selectedValues: string[]) => { + setTempSelectedValue({ + ...tempSelectedValue, + excludeDate: selectedValues, + }); }; - const onSubmit = useCallback>( - async (data, event) => { - let hasErrors = false; - console.log(errors) - // console.log(apiRef.current.getCellValue(2, "lowerLimit")) - // apiRef.current. - try { - if (hasErrors) { - setServerError(t("An error has occurred. Please try again later.") as String); - return false; - } - console.log("data posted"); - console.log(data); - const qcCheck = data.qcChecks.length > 0 ? data.qcChecks.filter((q) => data.qcChecks_active.includes(q.id!!)).map((qc) => { - return { - qcItemId: qc.id, - instruction: qc.instruction, - lowerLimit: qc.lowerLimit, - upperLimit: qc.upperLimit, - itemId: parseInt(params.get("id")!.toString()) - } - }) : [] - - const test = data.qcChecks.filter((q) => data.qcChecks_active.includes(q.id!!)) - // TODO: - // 1. check field ( directly modify col def / check here ) - // 2. set error change tab index - console.log(test) - console.log(qcCheck) - // return - // do api - console.log("asdad") - var responseI = await saveItem(data); - console.log("asdad") - var responseQ = await saveItemQcChecks(qcCheck) - if (responseI && responseQ) { - if (!Boolean(responseI.id)) { - formProps.setError(responseI.errorPosition!! as keyof CreateItemInputs, { - message: responseI.message!!, - type: "required", - }); - } else if (!Boolean(responseQ.id)) { - - } else if (Boolean(responseI.id) && Boolean(responseQ.id)) { - router.replace(redirPath); - } - } - } catch (e) { - // backend error - setServerError(t("An error has occurred. Please try again later.")); - console.log(e); - } + + const dayOptions = [ + {label: "Mon", value: 1}, + {label: "Tue", value: 2}, + {label: "Wed", value: 3}, + {label: "Thu", value: 4}, + {label: "Fri", value: 5}, + {label: "Sat", value: 6}, + {label: "Sun", value: 7}, + ]; + + const searchCriteria: Criterion[] = useMemo( + () => { + var searchCriteria: Criterion[] = [ + { label: t("Finished Goods Name"), paramName: "fgName", type: "text" }, + { + label: t("Exclude Date"), + paramName: "excludeDate", + type: "multi-select", + options: dayOptions, + selectedValues: filterObj, + handleSelectionChange: handleSelectionChange, + }, + ] + return searchCriteria }, - [apiRef, router, t] + [t, items] ); - // multiple tabs - const onSubmitError = useCallback>( - (errors) => {}, - [] + const onDetailClick = useCallback( + (item: ItemsResult) => { + router.push(`/settings/items/edit?id=${item.id}`); + }, + [router] + ); + + const onDeleteClick = useCallback( + (item: ItemsResult) => {}, + [router] ); + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onDetailClick, + buttonIcon: , + }, + { + name: "fgName", + label: "Finished Goods Name", + }, + { + name: "excludeDate", + label: t("Exclude Date"), + }, + { + name: "action", + label: t(""), + buttonIcon: , + onClick: onDeleteClick, + }, + ], + [filteredItems] + ); + + useEffect(() => { + refetchData(filterObj); + + }, [filterObj, pagingController.pageNum, pagingController.pageSize]); + + const refetchData = async (filterObj: SearchQuery) => { + + const authHeader = axiosInstance.defaults.headers['Authorization']; + if (!authHeader) { + return; // Exit the function if the token is not set + } + + const params ={ + pageNum: pagingController.pageNum, + pageSize: pagingController.pageSize, + ...filterObj, + ...tempSelectedValue, + } + + try { + const response = await axiosInstance.get(`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { + params, + paramsSerializer: (params) => { + return Qs.stringify(params, { arrayFormat: 'repeat' }); + }, + }); + setFilteredItems(response.data.records); + setPagingController({ + ...pagingController, + totalCount: response.data.total + }) + return response; // Return the data from the response + } catch (error) { + console.error('Error fetching items:', error); + throw error; // Rethrow the error for further handling + } + }; + + const onReset = useCallback(() => { + //setFilteredItems(items ?? []); + setFilterObj({}); + setTempSelectedValue({}); + refetchData(); + }, [items]); + return ( <> - - - - - {t(`${mode} ${title}`)} - - - - - - - {serverError && ( - - {serverError} - - )} - {tabIndex === 0 && } - {tabIndex === 1 && } - {/* {type === TypeEnum.MATERIAL && } */} - {/* {type === TypeEnum.BYPRODUCT && } */} - - - - - - + { + setFilterObj({ + ...query + }) + }} + onReset={onReset} + /> + + items={filteredItems} + columns={columns} + setPagingController={setPagingController} + pagingController={pagingController} + isAutoPaging={false} + /> ); }; -export default CreateItem; + +export default RSSOverview; diff --git a/src/components/RoughScheduleSetting/RoughScheduleSettingWrapper.tsx b/src/components/RoughScheduleSetting/RoughScheduleSettingWrapper.tsx index 93a7bd6..2dd5145 100644 --- a/src/components/RoughScheduleSetting/RoughScheduleSettingWrapper.tsx +++ b/src/components/RoughScheduleSetting/RoughScheduleSettingWrapper.tsx @@ -1,51 +1,23 @@ -import {CreateItemInputs} from "@/app/api/settings/item/actions"; -import CreateItemLoading from "../CreateItem/CreateItemLoading"; -import {fetchItem} from "@/app/api/settings/item"; -import CreateItem from "@/components/RoughScheduleSetting/RoughScheduleSetting"; +import { fetchAllItems, } from "@/app/api/settings/item"; +import {RoughScheduleLoading} from "./RoughScheduleLoading"; +import RSSOverview from "@/components/RoughScheduleSetting/RoughScheduleSetting"; interface SubComponents { - Loading: typeof CreateItemLoading; + Loading: typeof RoughScheduleLoading; } type Props = { - id?: number // type: TypeEnum; }; -const RoughScheduleSettingWrapper: ({id}: { id: any }) => Promise = async ({ id }) => { - var result - var defaultValues: Partial | undefined +const RoughScheduleSettingWrapper: ({}: {}) => Promise = async ({ + // type, + }) => { // console.log(type) - var qcChecks - if (id) { - result = await fetchItem(id); - const item = result.item - qcChecks = result.qcChecks - const activeRows = qcChecks.filter(it => it.isActive).map(i => i.id) - console.log(qcChecks) - defaultValues = { - type: item?.type, - id: item?.id, - code: item?.code, - name: item?.name, - description: item?.description, - remarks: item?.remarks, - shelfLife: item?.shelfLife, - countryOfOrigin: item?.countryOfOrigin, - maxQty: item?.maxQty, - qcChecks: qcChecks, - qcChecks_active: activeRows - }; - } - - return ( - - ); + var result = await fetchAllItems() + return ; }; -RoughScheduleSettingWrapper.Loading = CreateItemLoading; + +RoughScheduleSettingWrapper.Loading = RoughScheduleLoading; export default RoughScheduleSettingWrapper; diff --git a/src/components/SearchBox/MultiSelect.tsx b/src/components/SearchBox/MultiSelect.tsx new file mode 100644 index 0000000..ba71493 --- /dev/null +++ b/src/components/SearchBox/MultiSelect.tsx @@ -0,0 +1,67 @@ +import React, {useEffect, useState} from 'react'; +import { FormControl, InputLabel, Select, MenuItem, Chip, Box } from '@mui/material'; + +interface Option { + value: number; + label: string; +} + +interface MultiSelectProps { + label: string; + options: Option[]; + selectedValues: number[]; + onChange: (values: number[]) => void; +} + +const MultiSelect: React.FC = ({ label, options, selectedValues, onChange, isReset }) => { + const [displayValues, setDisplayValues] = useState(selectedValues); + + const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { + const value = event.target.value as number[]; + console.log("[debug] value", value); + + // Update display values state + setDisplayValues(value); + // Update selected values in parent component + onChange(value); + }; + + useEffect(()=>{ + setDisplayValues([]); + }, [isReset]) + + return ( + + {label} + -1} + readOnly + /> + {option.label} + + ))} + + + ); +}; + +export default MultiSelect; \ No newline at end of file diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 56c9b13..b1a5512 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -21,12 +21,16 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { Box } from "@mui/material"; +import MultiSelect from "@/components/SearchBox/MultiSelect"; interface BaseCriterion { label: string; label2?: string; paramName: T; paramName2?: T; + options?: T[]; + filterObj?: T; + handleSelectionChange: (selectedOptions: T[]) => void; } interface TextCriterion extends BaseCriterion { @@ -35,7 +39,14 @@ interface TextCriterion extends BaseCriterion { interface SelectCriterion extends BaseCriterion { type: "select"; - options: string[]; + options: T[]; +} + +interface MultiSelectCriterion extends BaseCriterion { + type: "select"; + options: T[]; + selectedOptions: T[]; + handleSelectionChange: (selectedOptions: T[]) => void; } interface DateRangeCriterion extends BaseCriterion { @@ -45,7 +56,8 @@ interface DateRangeCriterion extends BaseCriterion { export type Criterion = | TextCriterion | SelectCriterion - | DateRangeCriterion; + | DateRangeCriterion + | MultiSelectCriterion; interface Props { criteria: Criterion[]; @@ -70,6 +82,7 @@ function SearchBox({ [criteria], ); const [inputs, setInputs] = useState(defaultInputs); + const [isReset, setIsReset] = useState(false); const makeInputChangeHandler = useCallback( (paramName: T): React.ChangeEventHandler => { @@ -104,6 +117,7 @@ function SearchBox({ const handleReset = () => { setInputs(defaultInputs); onReset?.(); + setIsReset(!isReset); }; const handleSearch = () => { @@ -126,6 +140,15 @@ function SearchBox({ value={inputs[c.paramName]} /> )} + {c.type === "multi-select" && ( + + )} {c.type === "select" && ( {c.label}