Просмотр исходного кода

[search box] update multi-select for search box logic

create_edit_user
jason.lam 3 месяцев назад
Родитель
Сommit
9bed480dc9
7 измененных файлов: 308 добавлений и 208 удалений
  1. +2
    -0
      package.json
  2. +4
    -2
      src/app/(main)/settings/rss/page.tsx
  3. +40
    -0
      src/components/RoughScheduleSetting/RoughScheduleLoading.tsx
  4. +159
    -165
      src/components/RoughScheduleSetting/RoughScheduleSetting.tsx
  5. +11
    -39
      src/components/RoughScheduleSetting/RoughScheduleSettingWrapper.tsx
  6. +67
    -0
      src/components/SearchBox/MultiSelect.tsx
  7. +25
    -2
      src/components/SearchBox/SearchBox.tsx

+ 2
- 0
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",


+ 4
- 2
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")}
</Button> */}
</Stack>
<Suspense fallback={<ItemsSearch.Loading />}>
<ItemsSearch />
<Suspense fallback={<RoughScheduleLoading.Loading />}>
<RoughScheduleSetting />
</Suspense>
</>
);


+ 40
- 0
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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default RoughScheduleLoading;

+ 159
- 165
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<CreateItemInputs> | undefined;
qcChecks: ItemQc[]
items: ItemsResult[];
};
type SearchQuery = Partial<Omit<ItemsResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const CreateItem: React.FC<Props> = ({
isEditMode,
// type,
defaultValues,
qcChecks
}) => {
// console.log(type)
const apiRef = useGridApiRef();
const params = useSearchParams()
console.log(params.get("id"))
const [serverError, setServerError] = useState<String>("");
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const RSSOverview: React.FC<Props> = ({ items }) => {
const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(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<CreateItemInputs>({
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<SubmitHandler<CreateItemInputs & {}>>(
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<SearchParamNames>[] = useMemo(
() => {
var searchCriteria: Criterion<SearchParamNames>[] = [
{ 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<SubmitErrorHandler<CreateItemInputs>>(
(errors) => {},
[]
const onDetailClick = useCallback(
(item: ItemsResult) => {
router.push(`/settings/items/edit?id=${item.id}`);
},
[router]
);

const onDeleteClick = useCallback(
(item: ItemsResult) => {},
[router]
);

const columns = useMemo<Column<ItemsResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onDetailClick,
buttonIcon: <EditNote />,
},
{
name: "fgName",
label: "Finished Goods Name",
},
{
name: "excludeDate",
label: t("Exclude Date"),
},
{
name: "action",
label: t(""),
buttonIcon: <GridDeleteIcon />,
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<ItemsResult[]>(`${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 (
<>
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Grid>
<Typography mb={2} variant="h4">
{t(`${mode} ${title}`)}
</Typography>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Product / Material Details")} iconPosition="end"/>
<Tab label={t("Qc items")} iconPosition="end" />
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <ProductDetails />}
{tabIndex === 1 && <QcDetails apiRef={apiRef} />}
{/* {type === TypeEnum.MATERIAL && <MaterialDetails />} */}
{/* {type === TypeEnum.BYPRODUCT && <ByProductDetails />} */}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
name="submit"
variant="contained"
startIcon={<Check />}
type="submit"
// disabled={submitDisabled}
>
{isEditMode ? t("Save") as String : t("Confirm") as String}
</Button>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel") as String}
</Button>
</Stack>
</Stack>
</FormProvider>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilterObj({
...query
})
}}
onReset={onReset}
/>
<SearchResults<ItemsResult>
items={filteredItems}
columns={columns}
setPagingController={setPagingController}
pagingController={pagingController}
isAutoPaging={false}
/>
</>
);
};
export default CreateItem;

export default RSSOverview;

+ 11
- 39
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<JSX.Element> = async ({ id }) => {
var result
var defaultValues: Partial<CreateItemInputs> | undefined
const RoughScheduleSettingWrapper: ({}: {}) => Promise<JSX.Element> = 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 (
<CreateItem
isEditMode={Boolean(id)}
defaultValues={defaultValues}
qcChecks={qcChecks || []}
/>
);
var result = await fetchAllItems()
return <RSSOverview items={result} />;
};
RoughScheduleSettingWrapper.Loading = CreateItemLoading;

RoughScheduleSettingWrapper.Loading = RoughScheduleLoading;

export default RoughScheduleSettingWrapper;

+ 67
- 0
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<MultiSelectProps> = ({ label, options, selectedValues, onChange, isReset }) => {
const [displayValues, setDisplayValues] = useState<number[]>(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 (
<FormControl fullWidth>
<InputLabel>{label}</InputLabel>
<Select
multiple
value={displayValues}
onChange={handleChange}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
{(selected as number[]).map((value) => (
<Chip
key={value}
label={options.find(item => item.value == value).label}
sx={{ margin: 0.5 }}
/>
))}
</Box>
)}
>
{options.map((option) => (
<MenuItem key={option.value} value={option.value}>
<input
type="checkbox"
checked={displayValues.indexOf(option.value) > -1}
readOnly
/>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
};

export default MultiSelect;

+ 25
- 2
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<T extends string> {
label: string;
label2?: string;
paramName: T;
paramName2?: T;
options?: T[];
filterObj?: T;
handleSelectionChange: (selectedOptions: T[]) => void;
}

interface TextCriterion<T extends string> extends BaseCriterion<T> {
@@ -35,7 +39,14 @@ interface TextCriterion<T extends string> extends BaseCriterion<T> {

interface SelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: string[];
options: T[];
}

interface MultiSelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: T[];
selectedOptions: T[];
handleSelectionChange: (selectedOptions: T[]) => void;
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
@@ -45,7 +56,8 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>;
| DateRangeCriterion<T>
| MultiSelectCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
@@ -70,6 +82,7 @@ function SearchBox<T extends string>({
[criteria],
);
const [inputs, setInputs] = useState(defaultInputs);
const [isReset, setIsReset] = useState(false);

const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
@@ -104,6 +117,7 @@ function SearchBox<T extends string>({
const handleReset = () => {
setInputs(defaultInputs);
onReset?.();
setIsReset(!isReset);
};

const handleSearch = () => {
@@ -126,6 +140,15 @@ function SearchBox<T extends string>({
value={inputs[c.paramName]}
/>
)}
{c.type === "multi-select" && (
<MultiSelect
label={c.label}
options={c?.options}
selectedValues={c.filterObj?.[c.paramName] ?? []}
onChange={c.handleSelectionChange}
isReset={isReset}
/>
)}
{c.type === "select" && (
<FormControl fullWidth>
<InputLabel>{c.label}</InputLabel>


Загрузка…
Отмена
Сохранить