add promocode datafetching

promocode datagrid displays data
This commit is contained in:
nflnkr 2024-03-03 10:57:12 +03:00
parent 3485eca257
commit 0a2413a077
10 changed files with 614 additions and 495 deletions

@ -41,6 +41,7 @@
"reconnecting-eventsource": "^1.6.2", "reconnecting-eventsource": "^1.6.2",
"start-server-and-test": "^2.0.0", "start-server-and-test": "^2.0.0",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"swr": "^2.2.5",
"typescript": "^4.8.2", "typescript": "^4.8.2",
"use-debounce": "^9.0.4", "use-debounce": "^9.0.4",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",

@ -1,48 +0,0 @@
import { makeRequest } from "@frontend/kitui";
import { GetPromocodeListBody, PromocodeList, CreatePromocodeBody, Promocode } from "@root/model/promocodes";
import { parseAxiosError } from "@root/utils/parse-error";
const baseUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode";
export const getPromocodeList = async (
body: GetPromocodeListBody
): Promise<[PromocodeList | null, string?]> => {
try {
const promocodeListResponse = await makeRequest<
GetPromocodeListBody,
PromocodeList
>({
url: baseUrl + "/getList",
body,
useToken: false,
});
return [promocodeListResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении списка промокодов. ${error}`];
}
};
export const createPromocode = async (
body: CreatePromocodeBody
): Promise<[Promocode | null, string?]> => {
try {
const createPromocodeResponse = await makeRequest<
CreatePromocodeBody,
Promocode
>({
url: baseUrl + "/create",
body,
useToken: false,
});
return [createPromocodeResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка создания промокода. ${error}`];
}
};

@ -0,0 +1,67 @@
import { makeRequest } from "@frontend/kitui";
import { CreatePromocodeBody, GetPromocodeListBody, Promocode, PromocodeList } from "@root/model/promocodes";
import { parseAxiosError } from "@root/utils/parse-error";
const baseUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode";
const getPromocodeList = async (
body: GetPromocodeListBody
): Promise<Promocode[]> => {
try {
const promocodeListResponse = await makeRequest<
GetPromocodeListBody,
PromocodeList
>({
url: baseUrl + "/getList",
method: "POST",
body,
useToken: false,
});
return promocodeListResponse.items;
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка при получении списка промокодов. ${error}`);
}
};
const createPromocode = async (
body: CreatePromocodeBody
): Promise<Promocode> => {
try {
const createPromocodeResponse = await makeRequest<
CreatePromocodeBody,
Promocode
>({
url: baseUrl + "/create",
method: "POST",
body,
useToken: false,
});
return createPromocodeResponse;
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка создания промокода. ${error}`);
}
};
const deletePromocode = async (id: string): Promise<void> => {
try {
await makeRequest<never, never>({
url: `${baseUrl}/${id}`,
method: "DELETE",
useToken: false,
});
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка удаления промокода. ${error}`);
}
};
export const promocodeApi = {
getPromocodeList,
createPromocode,
deletePromocode,
};

69
src/api/promocode/swr.ts Normal file

@ -0,0 +1,69 @@
import { CreatePromocodeBody, Promocode } from "@root/model/promocodes";
import { enqueueSnackbar } from "notistack";
import useSwr, { mutate } from "swr";
import { promocodeApi } from "./requests";
export function usePromocodes() {
return useSwr(
"promocodes",
() => promocodeApi.getPromocodeList({
limit: 100,
filter: {
active: true,
},
page: 0,
}),
{
onError(err) {
console.log("Error fetching promocodes", err);
enqueueSnackbar(err.message, { variant: "error" });
},
focusThrottleInterval: 60e3,
}
);
}
export async function createPromocode(body: CreatePromocodeBody) {
try {
await mutate<Promocode[] | undefined, Promocode>(
"promocodes",
promocodeApi.createPromocode(body),
{
populateCache(result, currentData) {
if (!currentData) return;
return [...currentData, result];
},
revalidate: false,
}
);
} catch (error) {
console.log("Error creating promocode", error);
if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
}
}
export async function deletePromocode(id: string) {
try {
await mutate<Promocode[] | undefined, void>(
"promocodes",
promocodeApi.deletePromocode(id),
{
optimisticData(currentData, displayedData) {
if (!displayedData) return;
return displayedData.filter((item) => item.id !== id);
},
rollbackOnError: true,
populateCache(result, currentData) {
if (!currentData) return;
return currentData.filter((item) => item.id !== id);
},
}
);
} catch (error) {
console.log("Error deleting promocode", error);
if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
}
}

@ -28,6 +28,7 @@ export type GetPromocodeListBody = {
}; };
export type Promocode = CreatePromocodeBody & { export type Promocode = CreatePromocodeBody & {
id: string;
outdated: boolean; outdated: boolean;
offLimit: boolean; offLimit: boolean;
delete: boolean; delete: boolean;

@ -1,365 +1,368 @@
import { useEffect, useState } from "react";
import moment from "moment";
import { Formik, Field, Form } from "formik";
import { import {
Typography, Button,
TextField, FormControlLabel,
Button, MenuItem,
RadioGroup, Radio,
Radio, RadioGroup,
FormControlLabel, Select,
Select, TextField,
MenuItem, Typography,
} from "@mui/material"; } from "@mui/material";
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker"; import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { Field, Form, Formik } from "formik";
import moment from "moment";
import { useEffect, useState } from "react";
import { requestPrivileges } from "@root/services/privilegies.service"; import { requestPrivileges } from "@root/services/privilegies.service";
import { usePrivilegeStore } from "@root/stores/privilegesStore"; import { usePrivilegeStore } from "@root/stores/privilegesStore";
import { createPromocode } from "@root/api/promocode";
import { SERVICE_LIST } from "@root/model/privilege"; import { SERVICE_LIST } from "@root/model/privilege";
import theme from "@root/theme"; import theme from "@root/theme";
import type { ChangeEvent } from "react";
import type { TextFieldProps } from "@mui/material"; import type { TextFieldProps } from "@mui/material";
import { createPromocode } from "@root/api/promocode/swr";
import type { ChangeEvent } from "react";
type BonusType = "discount" | "privilege"; type BonusType = "discount" | "privilege";
type FormValues = { type FormValues = {
codeword: string; codeword: string;
description: string; description: string;
greetings: string; greetings: string;
dueTo: number; dueTo: number;
activationCount: number; activationCount: number;
privilegeId: string; privilegeId: string;
amount: number; amount: number;
layer: 1 | 2; layer: 1 | 2;
factor: number; factor: number;
target: string; target: string;
threshold: number; threshold: number;
}; };
type CustomTextFieldProps = {
name: string;
label: string;
required?: boolean;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
};
const CustomTextField = ({
name,
label,
required = false,
onChange,
}: CustomTextFieldProps) => (
<Field
name={name}
label={label}
required={required}
variant="filled"
color="secondary"
as={TextField}
onChange={onChange}
sx={{ width: "100%", marginTop: "15px" }}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
},
}}
InputLabelProps={{
style: { color: theme.palette.secondary.main },
}}
/>
);
const initialValues: FormValues = { const initialValues: FormValues = {
codeword: "", codeword: "",
description: "", description: "",
greetings: "", greetings: "",
dueTo: 0, dueTo: 0,
activationCount: 0, activationCount: 0,
privilegeId: "", privilegeId: "",
amount: 0, amount: 0,
layer: 1, layer: 1,
factor: 0, factor: 0,
target: "", target: "",
threshold: 0, threshold: 0,
}; };
export const CreatePromocodeForm = () => { export const CreatePromocodeForm = () => {
const [bonusType, setBonusType] = useState<BonusType>("discount"); const [bonusType, setBonusType] = useState<BonusType>("discount");
const { privileges } = usePrivilegeStore(); const { privileges } = usePrivilegeStore();
useEffect(() => { useEffect(() => {
requestPrivileges(); requestPrivileges();
}, []); }, []);
const submitForm = async (values: FormValues) => { const submitForm = (values: FormValues) => {
const body = { const body = {
...values, ...values,
threshold: values.layer === 1 ? values.threshold : values.threshold * 100, threshold: values.layer === 1 ? values.threshold : values.threshold * 100,
};
const factorFromDiscountValue = 1 - body.factor / 100;
return createPromocode({
codeword: body.codeword,
description: body.description,
greetings: body.greetings,
dueTo: body.dueTo,
activationCount: body.activationCount,
bonus: {
privilege: { privilegeID: body.privilegeId, amount: body.amount },
discount: {
layer: body.layer,
factor: factorFromDiscountValue,
target: body.target,
threshold: body.threshold,
},
},
});
}; };
await createPromocode({ return (
codeword: body.codeword, <Formik initialValues={initialValues} onSubmit={submitForm}>
description: body.description, {({ values, handleChange, handleBlur, setFieldValue }) => (
greetings: body.greetings, <Form
dueTo: body.dueTo, style={{
activationCount: body.activationCount, width: "100%",
bonus: { maxWidth: "600px",
privilege: { privilegeID: body.privilegeId, amount: body.amount }, padding: "0 10px",
discount: { }}
layer: body.layer, >
factor: body.factor, <CustomTextField
target: body.target, name="codeword"
threshold: body.threshold, label="Кодовое слово"
}, required
}, onChange={handleChange}
}); />
}; <CustomTextField
name="description"
return ( label="Описание"
<Formik initialValues={initialValues} onSubmit={submitForm}> required
{({ values, handleChange, handleBlur, setFieldValue }) => ( onChange={handleChange}
<Form />
style={{ <CustomTextField
width: "100%", name="greetings"
maxWidth: "600px", label="Приветственное сообщение"
padding: "0 10px", required
}} onChange={handleChange}
> />
<CustomTextField <Typography
name="codeword" variant="h4"
label="Кодовое слово" sx={{
required height: "40px",
onChange={handleChange} fontWeight: "normal",
/> marginTop: "15px",
<CustomTextField color: theme.palette.secondary.main,
name="description" }}
label="Описание" >
required Время существования промокода
onChange={handleChange} </Typography>
/> <Field
<CustomTextField name="dueTo"
name="greetings" as={DesktopDatePicker}
label="Приветственное сообщение" inputFormat="DD/MM/YYYY"
required value={values.dueTo ? new Date(Number(values.dueTo) * 1000) : null}
onChange={handleChange} onChange={(date: Date | null) => {
/> if (date) {
<Typography setFieldValue("dueTo", moment(date).unix() || null);
variant="h4" }
sx={{ }}
height: "40px", renderInput={(params: TextFieldProps) => <TextField {...params} />}
fontWeight: "normal", InputProps={{
marginTop: "15px", sx: {
color: theme.palette.secondary.main, height: "40px",
}} color: theme.palette.secondary.main,
> border: "1px solid",
Время существования промокода borderColor: theme.palette.secondary.main,
</Typography> "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
<Field },
name="dueTo" }}
as={DesktopDatePicker} />
inputFormat="DD/MM/YYYY" <CustomTextField
value={values.dueTo ? new Date(Number(values.dueTo) * 1000) : null} name="activationCount"
onChange={(date: Date | null) => { label="Количество активаций промокода"
if (date) { onChange={({ target }) =>
setFieldValue("dueTo", moment(date).unix() || null); setFieldValue(
} "activationCount",
}} Number(target.value.replace(/\D/g, ""))
renderInput={(params: TextFieldProps) => <TextField {...params} />} )
InputProps={{ }
sx: { />
height: "40px", <RadioGroup
color: theme.palette.secondary.main, row
border: "1px solid", name="bonusType"
borderColor: theme.palette.secondary.main, value={bonusType}
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, sx={{ marginTop: "15px" }}
}, onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
}} setBonusType(target.value as BonusType);
/> }}
<CustomTextField onBlur={handleBlur}
name="activationCount" >
label="Количество активаций промокода" <FormControlLabel
onChange={({ target }) => value="discount"
setFieldValue( control={<Radio color="secondary" />}
"activationCount", label="Скидка"
Number(target.value.replace(/\D/g, "")) />
) <FormControlLabel
} value="privilege"
/> control={<Radio color="secondary" />}
<RadioGroup label="Привилегия"
row />
name="bonusType" </RadioGroup>
value={bonusType} {bonusType === "discount" && (
sx={{ marginTop: "15px" }} <>
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { <RadioGroup
setBonusType(target.value as BonusType); row
}} name="layer"
onBlur={handleBlur} value={values.layer}
> sx={{ marginTop: "15px" }}
<FormControlLabel onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
value="discount" setFieldValue("target", "");
control={<Radio color="secondary" />} setFieldValue("layer", Number(target.value));
label="Скидка" }}
/> onBlur={handleBlur}
<FormControlLabel >
value="privilege" <FormControlLabel
control={<Radio color="secondary" />} value="1"
label="Привилегия" control={<Radio color="secondary" />}
/> label="Привилегия"
</RadioGroup> />
{bonusType === "discount" && ( <FormControlLabel
<> value="2"
<RadioGroup control={<Radio color="secondary" />}
row label="Сервис"
name="layer" />
value={values.layer} </RadioGroup>
sx={{ marginTop: "15px" }} <CustomTextField
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { name="factor"
setFieldValue("target", ""); label="Процент скидки"
setFieldValue("layer", Number(target.value)); required
}} onChange={({ target }) => {
onBlur={handleBlur} setFieldValue(
> "factor",
<FormControlLabel Number(target.value.replace(/\D/g, ""))
value="1" );
control={<Radio color="secondary" />} }}
label="Привилегия" />
/> <Typography
<FormControlLabel variant="h4"
value="2" sx={{
control={<Radio color="secondary" />} height: "40px",
label="Сервис" fontWeight: "normal",
/> marginTop: "15px",
</RadioGroup> padding: "0 12px",
<CustomTextField color: theme.palette.secondary.main,
name="factor" }}
label="Процент скидки" >
required {values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"}
onChange={({ target }) => </Typography>
setFieldValue( <Field
"factor", name="target"
Number(target.value.replace(/\D/g, "")) as={Select}
) label={values.layer === 1 ? "Привилегия" : "Сервис"}
} sx={{
/> width: "100%",
<Typography border: "2px solid",
variant="h4" color: theme.palette.secondary.main,
sx={{ borderColor: theme.palette.secondary.main,
height: "40px", "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
fontWeight: "normal", border: "none",
marginTop: "15px", },
padding: "0 12px", ".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
color: theme.palette.secondary.main, }}
}} children={
> values.layer === 1
{values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"} ? privileges.map(({ name, privilegeId }) => (
</Typography> <MenuItem key={privilegeId} value={privilegeId}>
<Field {name}
name="target" </MenuItem>
as={Select} ))
label={values.layer === 1 ? "Привилегия" : "Сервис"} : SERVICE_LIST.map(({ displayName, serviceKey }) => (
sx={{ <MenuItem key={serviceKey} value={serviceKey}>
width: "100%", {displayName}
border: "2px solid", </MenuItem>
color: theme.palette.secondary.main, ))
borderColor: theme.palette.secondary.main, }
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { />
border: "none", <CustomTextField
}, name="threshold"
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main }, label="При каком значении применяется скидка"
}} onChange={({ target }) =>
children={ setFieldValue(
values.layer === 1 "threshold",
? privileges.map(({ name, privilegeId }) => ( Number(target.value.replace(/\D/g, ""))
<MenuItem key={privilegeId} value={privilegeId}> )
{name} }
</MenuItem> />
)) </>
: SERVICE_LIST.map(({ displayName, serviceKey }) => ( )}
<MenuItem key={serviceKey} value={serviceKey}> {bonusType === "privilege" && (
{displayName} <>
</MenuItem> <Typography
)) variant="h4"
} sx={{
/> height: "40px",
<CustomTextField fontWeight: "normal",
name="threshold" marginTop: "15px",
label="При каком значении применяется скидка" padding: "0 12px",
onChange={({ target }) => color: theme.palette.secondary.main,
setFieldValue( }}
"threshold", >
Number(target.value.replace(/\D/g, "")) Выбор привилегии
) </Typography>
} <Field
/> name="privilegeId"
</> as={Select}
)} label="Привилегия"
{bonusType === "privilege" && ( sx={{
<> width: "100%",
<Typography border: "2px solid",
variant="h4" color: theme.palette.secondary.main,
sx={{ borderColor: theme.palette.secondary.main,
height: "40px", "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
fontWeight: "normal", border: "none",
marginTop: "15px", },
padding: "0 12px", ".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
color: theme.palette.secondary.main, }}
}} children={privileges.map(({ name, privilegeId }) => (
> <MenuItem key={privilegeId} value={privilegeId}>
Выбор привилегии {name}
</Typography> </MenuItem>
<Field ))}
name="privilegeId" />
as={Select} <CustomTextField
label="Привилегия" name="amount"
sx={{ label="Количество"
width: "100%", required
border: "2px solid", onChange={({ target }) =>
color: theme.palette.secondary.main, setFieldValue(
borderColor: theme.palette.secondary.main, "amount",
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { Number(target.value.replace(/\D/g, ""))
border: "none", )
}, }
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main }, />
}} </>
children={privileges.map(({ name, privilegeId }) => ( )}
<MenuItem key={privilegeId} value={privilegeId}> <Button
{name} variant="contained"
</MenuItem> sx={{
))} display: "block",
/> padding: "10px",
<CustomTextField margin: "15px auto 0",
name="amount" fontWeight: "normal",
label="Количество" fontSize: "18px",
required backgroundColor: theme.palette.menu.main,
onChange={({ target }) => "&:hover": { backgroundColor: theme.palette.grayMedium.main },
setFieldValue( }}
"amount", type="submit"
Number(target.value.replace(/\D/g, "")) >
) Cоздать
} </Button>
/> </Form>
</> )}
)} </Formik>
<Button );
variant="contained"
sx={{
display: "block",
padding: "10px",
margin: "15px auto 0",
fontWeight: "normal",
fontSize: "18px",
backgroundColor: theme.palette.menu.main,
"&:hover": { backgroundColor: theme.palette.grayMedium.main },
}}
type="submit"
>
Cоздать
</Button>
</Form>
)}
</Formik>
);
}; };
type CustomTextFieldProps = {
name: string;
label: string;
required?: boolean;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
};
const CustomTextField = ({
name,
label,
required = false,
onChange,
}: CustomTextFieldProps) => (
<Field
name={name}
label={label}
required={required}
variant="filled"
color="secondary"
as={TextField}
onChange={onChange}
sx={{ width: "100%", marginTop: "15px" }}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
},
}}
InputLabelProps={{
style: { color: theme.palette.secondary.main },
}}
/>
);

@ -1,56 +1,93 @@
import { Box } from "@mui/material"; import { Box, IconButton } from "@mui/material";
import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid";
import { Promocode } from "@root/model/promocodes";
import { usePromocodeStore } from "@root/stores/promocodes"; import DeleteIcon from '@mui/icons-material/Delete';
import theme from "@root/theme"; import theme from "@root/theme";
import { deletePromocode } from "@root/api/promocode/swr";
const columns: GridColDef[] = [ const columns: GridColDef<Promocode, string | number>[] = [
{ field: "id", headerName: "ID", width: 30, sortable: false }, {
{ field: "id",
field: "name", headerName: "ID",
headerName: "Название промокода", width: 30,
width: 200, sortable: false,
sortable: false, valueGetter: ({ row }) => row.id,
}, },
{ field: "endless", headerName: "Бесконечный", width: 120, sortable: false }, {
{ field: "from", headerName: "От", width: 120, sortable: false }, field: "codeword",
{ field: "dueTo", headerName: "До", width: 120, sortable: false }, headerName: "Кодовое слово",
{ width: 160,
field: "privileges", sortable: false,
headerName: "Привилегии", valueGetter: ({ row }) => row.codeword,
width: 210, },
sortable: false, {
}, field: "factor",
headerName: "Коэф. скидки",
width: 140,
sortable: false,
valueGetter: ({ row }) => row.bonus.discount.factor,
},
{
field: "activationCount",
headerName: "Кол-во активаций",
width: 140,
sortable: false,
valueGetter: ({ row }) => row.activationCount,
},
{
field: "dueTo",
headerName: "Истекает",
width: 160,
sortable: false,
valueGetter: ({ row }) => new Date(row.dueTo * 1000).toLocaleString(),
},
{
field: "delete",
headerName: "",
width: 60,
renderCell: ({ row }) => {
return (
<IconButton onClick={() => deletePromocode(row.id)}>
<DeleteIcon />
</IconButton>
);
},
},
]; ];
export const PromocodesList = () => { type Props = {
const { promocodes } = usePromocodeStore(); promocodes: Promocode[];
};
return (
<Box style={{ width: "80%", marginTop: "55px" }}> export const PromocodesList = ({ promocodes }: Props) => {
<Box style={{ height: 400 }}>
<DataGrid return (
checkboxSelection={true} <Box style={{ width: "80%", marginTop: "55px" }}>
rows={promocodes} <Box style={{ height: 400 }}>
columns={columns} <DataGrid
sx={{ disableSelectionOnClick={true}
color: theme.palette.secondary.main, checkboxSelection={true}
"& .MuiDataGrid-iconSeparator": { display: "none" }, rows={promocodes}
"& .css-levciy-MuiTablePagination-displayedRows": { columns={columns}
color: theme.palette.secondary.main, sx={{
}, minHeight: "1000px",
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, color: theme.palette.secondary.main,
"& .MuiTablePagination-selectLabel": { "& .MuiDataGrid-iconSeparator": { display: "none" },
color: theme.palette.secondary.main, "& .css-levciy-MuiTablePagination-displayedRows": {
}, color: theme.palette.secondary.main,
"& .MuiInputBase-root": { color: theme.palette.secondary.main }, },
"& .MuiButton-text": { color: theme.palette.secondary.main }, "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
}} "& .MuiTablePagination-selectLabel": {
components={{ Toolbar: GridToolbar }} color: theme.palette.secondary.main,
onSelectionModelChange={(ids) => console.log("datagrid select")} },
/> "& .MuiInputBase-root": { color: theme.palette.secondary.main },
</Box> "& .MuiButton-text": { color: theme.palette.secondary.main },
</Box> }}
); components={{ Toolbar: GridToolbar }}
onSelectionModelChange={(ids) => console.log("datagrid select")}
/>
</Box>
</Box>
);
}; };

@ -1,30 +1,39 @@
import { Typography } from "@mui/material"; import { CircularProgress, Typography } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { PromocodesList } from "./PromocodesList";
import { CreatePromocodeForm } from "./CreatePromocodeForm"; import { CreatePromocodeForm } from "./CreatePromocodeForm";
import { PromocodesList } from "./PromocodesList";
import { usePromocodes } from "@root/api/promocode/swr";
import theme from "@root/theme"; import theme from "@root/theme";
export const PromocodeManagement = () => ( export const PromocodeManagement = () => {
<LocalizationProvider dateAdapter={AdapterDayjs}> const { data, error, isLoading } = usePromocodes();
<Typography
variant="subtitle1" if (error) return <Typography>Ошибка загрузки промокодов</Typography>;
sx={{ if (isLoading) return <CircularProgress />;
width: "90%", if (!data) return null;
height: "60px",
display: "flex", return (
flexDirection: "column", <LocalizationProvider dateAdapter={AdapterDayjs}>
justifyContent: "center", <Typography
alignItems: "center", variant="subtitle1"
textTransform: "uppercase", sx={{
color: theme.palette.secondary.main, width: "90%",
}} height: "60px",
> display: "flex",
Создание промокода flexDirection: "column",
</Typography> justifyContent: "center",
<CreatePromocodeForm /> alignItems: "center",
<PromocodesList /> textTransform: "uppercase",
</LocalizationProvider> color: theme.palette.secondary.main,
); }}
>
Создание промокода
</Typography>
<CreatePromocodeForm />
<PromocodesList promocodes={data} />
</LocalizationProvider>
);
};

@ -1,33 +0,0 @@
import { Promocode } from "@root/model/cart";
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
interface PromocodeStore {
promocodes: Promocode[];
addPromocodes: (newPromocodes: Promocode[]) => void;
deletePromocodes: (promocodeIdsToDelete: string[]) => void;
}
export const usePromocodeStore = create<PromocodeStore>()(
devtools(
// persist(
(set, get) => ({
promocodes: [],
addPromocodes: newPromocodes => set(state => (
{ promocodes: [...state.promocodes, ...newPromocodes] }
)),
deletePromocodes: promocodeIdsToDelete => set(state => (
{ promocodes: state.promocodes.filter(promocode => !promocodeIdsToDelete.includes(promocode.id)) }
)),
}),
// {
// name: "promocodes",
// getStorage: () => localStorage,
// }
// ),
{
name: "Promocode store"
}
)
);

@ -4327,6 +4327,11 @@ cli-truncate@^2.1.0:
slice-ansi "^3.0.0" slice-ansi "^3.0.0"
string-width "^4.2.0" string-width "^4.2.0"
client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
cliui@^7.0.2: cliui@^7.0.2:
version "7.0.4" version "7.0.4"
resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz"
@ -11801,6 +11806,14 @@ svgo@^2.7.0:
picocolors "^1.0.0" picocolors "^1.0.0"
stable "^0.1.8" stable "^0.1.8"
swr@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b"
integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==
dependencies:
client-only "^0.0.1"
use-sync-external-store "^1.2.0"
symbol-tree@^3.2.4: symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz"
@ -12360,7 +12373,7 @@ use-debounce@^9.0.4:
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85" resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85"
integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ== integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==
use-sync-external-store@1.2.0: use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==