diff --git a/package.json b/package.json index 52b3c11..38c4ac5 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.4.0", "craco": "^0.0.3", "cypress": "^12.17.2", + "date-fns": "^3.3.1", "dayjs": "^1.11.5", "formik": "^2.2.9", "immer": "^10.0.2", @@ -41,6 +42,7 @@ "reconnecting-eventsource": "^1.6.2", "start-server-and-test": "^2.0.0", "styled-components": "^5.3.5", + "swr": "^2.2.5", "typescript": "^4.8.2", "use-debounce": "^9.0.4", "web-vitals": "^2.1.4", diff --git a/src/api/history/requests.ts b/src/api/history/requests.ts new file mode 100644 index 0000000..fdc638f --- /dev/null +++ b/src/api/history/requests.ts @@ -0,0 +1,50 @@ +import { makeRequest } from "@frontend/kitui"; + +import { parseAxiosError } from "@root/utils/parse-error"; + +type RawDetail = { + Key: string; + Value: number | string | RawDetail[]; +}; + +type History = { + id: string; + userId: string; + comment: string; + key: string; + rawDetails: RawDetail[]; + isDeleted: boolean; + createdAt: string; + updatedAt: string; +}; + +type HistoryResponse = { + records: History[]; + totalPages: number; +}; + +const baseUrl = process.env.REACT_APP_DOMAIN + "/customer"; + +const getUserHistory = async ( + accountId: string, + page: number +): Promise<[HistoryResponse | null, string?]> => { + try { + const historyResponse = await makeRequest({ + method: "GET", + url: + baseUrl + + `/history?page=${page}&limit=${100}&accountID=${accountId}&type=payCart`, + }); + + return [historyResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при получении пользователей. ${error}`]; + } +}; + +export const historyApi = { + getUserHistory, +}; diff --git a/src/api/history/swr.ts b/src/api/history/swr.ts new file mode 100644 index 0000000..489d37b --- /dev/null +++ b/src/api/history/swr.ts @@ -0,0 +1,43 @@ +import { useState } from "react"; +import useSWRInfinite from "swr/infinite"; +import { enqueueSnackbar } from "notistack"; + +import { historyApi } from "./requests"; + +export function useHistory(accountId: string) { + const [currentPage, setCurrentPage] = useState(1); + + const swrResponse = useSWRInfinite( + () => `history-${currentPage}`, + async () => { + const [historyResponse, error] = await historyApi.getUserHistory( + accountId, + currentPage + ); + + if (error) { + throw new Error(error); + } + + if (!historyResponse) { + throw new Error("Empty history data"); + } + + if (currentPage < historyResponse.totalPages) { + setCurrentPage((page) => page + 1); + } + + return historyResponse; + }, + { + onError(err) { + console.log("Error fetching users", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); + + return swrResponse; +} diff --git a/src/api/promocode/requests.ts b/src/api/promocode/requests.ts new file mode 100644 index 0000000..f28ee26 --- /dev/null +++ b/src/api/promocode/requests.ts @@ -0,0 +1,63 @@ +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) => { + try { + const promocodeListResponse = await makeRequest< + GetPromocodeListBody, + PromocodeList + >({ + url: baseUrl + "/getList", + method: "POST", + body, + useToken: false, + }); + + return promocodeListResponse; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка при получении списка промокодов. ${error}`); + } +}; + +const createPromocode = async (body: CreatePromocodeBody) => { + 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 => { + try { + await makeRequest({ + url: `${baseUrl}/${id}`, + method: "DELETE", + useToken: false, + }); + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка удаления промокода. ${error}`); + } +}; + +export const promocodeApi = { + getPromocodeList, + createPromocode, + deletePromocode, +}; diff --git a/src/api/promocode/swr.ts b/src/api/promocode/swr.ts new file mode 100644 index 0000000..064d41a --- /dev/null +++ b/src/api/promocode/swr.ts @@ -0,0 +1,81 @@ +import { CreatePromocodeBody, PromocodeList } from "@root/model/promocodes"; +import { enqueueSnackbar } from "notistack"; +import { useCallback, useRef } from "react"; +import useSwr, { mutate } from "swr"; +import { promocodeApi } from "./requests"; + + +export function usePromocodes(page: number, pageSize: number) { + const promocodesCountRef = useRef(0); + const swrResponse = useSwr( + ["promocodes", page, pageSize], + async (key) => { + const result = await promocodeApi.getPromocodeList({ + limit: key[2], + filter: { + active: true, + }, + page: key[1], + }); + + promocodesCountRef.current = result.count; + return result; + }, + { + onError(err) { + console.log("Error fetching promocodes", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); + + const createPromocode = useCallback(async function (body: CreatePromocodeBody) { + try { + await promocodeApi.createPromocode(body); + mutate(["promocodes", page, pageSize]); + } catch (error) { + console.log("Error creating promocode", error); + if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" }); + } + }, [page, pageSize]); + + const deletePromocode = useCallback(async function (id: string) { + try { + await mutate( + ["promocodes", page, pageSize], + promocodeApi.deletePromocode(id), + { + optimisticData(currentData, displayedData) { + if (!displayedData) return; + + return { + count: displayedData.count - 1, + items: displayedData.items.filter((item) => item.id !== id), + }; + }, + rollbackOnError: true, + populateCache(result, currentData) { + if (!currentData) return; + + return { + count: currentData.count - 1, + items: currentData.items.filter((item) => item.id !== id), + }; + }, + } + ); + } catch (error) { + console.log("Error deleting promocode", error); + if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" }); + } + }, [page, pageSize]); + + return { + ...swrResponse, + createPromocode, + deletePromocode, + promocodesCount: promocodesCountRef.current, + }; +} diff --git a/src/api/user.ts b/src/api/user.ts deleted file mode 100644 index aa6be3b..0000000 --- a/src/api/user.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { makeRequest } from "@frontend/kitui"; - -import { parseAxiosError } from "@root/utils/parse-error"; - -import type { UserType } from "@root/api/roles"; - -type RegisteredUsersResponse = { - tatalPages: number; - users: UserType[]; -}; - -const baseUrl = process.env.REACT_APP_DOMAIN + "/user"; - -export const getUserInfo = async ( - id: string -): Promise<[UserType | null, string?]> => { - try { - const userInfoResponse = await makeRequest({ - url: `${baseUrl}/${id}`, - method: "GET", - useToken: true, - }); - - return [userInfoResponse]; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - - return [null, `Ошибка получения информации о пользователе. ${error}`]; - } -}; - -export const getRegisteredUsers = async (): Promise< - [RegisteredUsersResponse | null, string?] -> => { - try { - const registeredUsersResponse = await makeRequest< - never, - RegisteredUsersResponse - >({ - method: "get", - url: baseUrl + "/", - }); - - return [registeredUsersResponse]; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - - return [null, `Ошибка при получении пользователей. ${error}`]; - } -}; - -export const getManagersList = async (): Promise< - [RegisteredUsersResponse | null, string?] -> => { - try { - const managersListResponse = await makeRequest< - never, - RegisteredUsersResponse - >({ - method: "get", - url: baseUrl + "/", - }); - - return [managersListResponse]; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - - return [null, `Ошибка при получении менеджеров. ${error}`]; - } -}; diff --git a/src/api/user/requests.ts b/src/api/user/requests.ts new file mode 100644 index 0000000..4ae9590 --- /dev/null +++ b/src/api/user/requests.ts @@ -0,0 +1,89 @@ +import { makeRequest } from "@frontend/kitui"; + +import { parseAxiosError } from "@root/utils/parse-error"; + +import type { UserType } from "@root/api/roles"; + +export type UsersListResponse = { + totalPages: number; + users: UserType[]; +}; + +const baseUrl = process.env.REACT_APP_DOMAIN + "/user"; + +const getUserInfo = async (id: string): Promise<[UserType | null, string?]> => { + try { + const userInfoResponse = await makeRequest({ + url: `${baseUrl}/${id}`, + method: "GET", + useToken: true, + }); + + return [userInfoResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка получения информации о пользователе. ${error}`]; + } +}; + +const getUserList = async ( + page = 1, + limit = 10 +): Promise<[UsersListResponse | null, string?]> => { + try { + const userResponse = await makeRequest({ + method: "get", + url: baseUrl + `/?page=${page}&limit=${limit}`, + }); + + return [userResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при получении пользователей. ${error}`]; + } +}; + +const getManagerList = async ( + page = 1, + limit = 10 +): Promise<[UsersListResponse | null, string?]> => { + try { + const managerResponse = await makeRequest({ + method: "get", + url: baseUrl + `/?page=${page}&limit=${limit}`, + }); + + return [managerResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при получении менеджеров. ${error}`]; + } +}; + +const getAdminList = async ( + page = 1, + limit = 10 +): Promise<[UsersListResponse | null, string?]> => { + try { + const adminResponse = await makeRequest({ + method: "get", + url: baseUrl + `/?page=${page}&limit=${limit}`, + }); + + return [adminResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при получении админов. ${error}`]; + } +}; + +export const userApi = { + getUserInfo, + getUserList, + getManagerList, + getAdminList, +}; diff --git a/src/api/user/swr.ts b/src/api/user/swr.ts new file mode 100644 index 0000000..23dc84d --- /dev/null +++ b/src/api/user/swr.ts @@ -0,0 +1,104 @@ +import { useRef } from "react"; +import useSwr from "swr"; +import { enqueueSnackbar } from "notistack"; + +import { userApi } from "./requests"; + +export function useAdmins(page: number, pageSize: number) { + const adminPagesRef = useRef(0); + + const swrResponse = useSwr( + ["admin", page, pageSize], + async ([_, page, pageSize]) => { + const [adminResponse, error] = await userApi.getManagerList( + page, + pageSize + ); + + if (error) { + throw new Error(error); + } + + adminPagesRef.current = adminResponse?.totalPages || 1; + return adminResponse; + }, + { + onError(err) { + console.log("Error fetching users", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); + + return { + ...swrResponse, + adminPages: adminPagesRef.current, + }; +} + +export function useManagers(page: number, pageSize: number) { + const managerPagesRef = useRef(0); + + const swrResponse = useSwr( + ["manager", page, pageSize], + async ([_, page, pageSize]) => { + const [managerResponse, error] = await userApi.getManagerList( + page, + pageSize + ); + + if (error) { + throw new Error(error); + } + + managerPagesRef.current = managerResponse?.totalPages || 1; + return managerResponse; + }, + { + onError(err) { + console.log("Error fetching users", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); + + return { + ...swrResponse, + managerPages: managerPagesRef.current, + }; +} + +export function useUsers(page: number, pageSize: number) { + const userPagesRef = useRef(0); + + const swrResponse = useSwr( + ["users", page, pageSize], + async ([_, page, pageSize]) => { + const [userResponse, error] = await userApi.getUserList(page, pageSize); + + if (error) { + throw new Error(error); + } + + userPagesRef.current = userResponse?.totalPages || 1; + return userResponse; + }, + { + onError(err) { + console.log("Error fetching users", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); + + return { + ...swrResponse, + userPagesCount: userPagesRef.current, + }; +} diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..da5e7a7 --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/index.tsx b/src/index.tsx index 99f5b6d..0223de9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,9 +20,10 @@ import Users from "@pages/dashboard/Content/Users"; import Entities from "@pages/dashboard/Content/Entities"; import Tariffs from "@pages/dashboard/Content/Tariffs"; import DiscountManagement from "@root/pages/dashboard/Content/DiscountManagement/DiscountManagement"; -import PromocodeManagement from "@pages/dashboard/Content/PromocodeManagement"; +import { PromocodeManagement } from "@root/pages/dashboard/Content/PromocodeManagement"; import { SettingRoles } from "@pages/Setting/SettingRoles"; import Support from "@pages/dashboard/Content/Support/Support"; +import ChatImageNewWindow from "@pages/dashboard/Content/Support/ChatImageNewWindow"; import theme from "./theme"; import "./index.css"; @@ -104,9 +105,14 @@ root.render( } /> {componentsArray.map((element) => ( - + ))} + } /> } /> diff --git a/src/kitUI/Cart/Cart.tsx b/src/kitUI/Cart/Cart.tsx index fc3fc52..1dcb5c5 100644 --- a/src/kitUI/Cart/Cart.tsx +++ b/src/kitUI/Cart/Cart.tsx @@ -11,7 +11,7 @@ import { Alert, Checkbox, FormControlLabel, - useTheme + useTheme, useMediaQuery } from "@mui/material"; import Input from "@kitUI/input"; import { useState } from "react"; @@ -29,6 +29,7 @@ import { currencyFormatter } from "@root/utils/currencyFormatter"; export default function Cart() { const theme = useTheme(); + const mobile = useMediaQuery(theme.breakpoints.down(400)); let discounts = useDiscountStore(state => state.discounts); const cartData = useCartStore((store) => store.cartData); const tariffs = useTariffStore(state => state.tariffs); @@ -84,6 +85,7 @@ export default function Cart() { alignItems: "center", justifyContent: "space-between", gap: "20px", + flexDirection: mobile ? "column" : undefined }} > { const theme = useTheme(); const navigate = useNavigate(); - + const isMobile = useMediaQuery(theme.breakpoints.down(600)); const initialValues: Values = { email: "", password: "", @@ -99,6 +99,7 @@ const SigninForm = () => { "> *": { marginTop: "15px", }, + padding: isMobile ? "0 16px" : undefined }} > diff --git a/src/pages/Setting/CardPrivilegie.tsx b/src/pages/Setting/CardPrivilegie.tsx index 5cf5dd9..9fe3aad 100644 --- a/src/pages/Setting/CardPrivilegie.tsx +++ b/src/pages/Setting/CardPrivilegie.tsx @@ -1,6 +1,6 @@ import { KeyboardEvent, useRef, useState } from "react"; import { enqueueSnackbar } from "notistack"; -import { Box, IconButton, TextField, Tooltip, Typography } from "@mui/material"; +import {Box, IconButton, TextField, Tooltip, Typography, useMediaQuery, useTheme} from "@mui/material"; import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined"; import { PrivilegeWithAmount } from "@frontend/kitui"; import { putPrivilege } from "@root/api/privilegies"; @@ -16,6 +16,8 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => { const [inputOpen, setInputOpen] = useState(false); const [inputValue, setInputValue] = useState(""); const priceRef = useRef(null); + const theme = useTheme(); + const mobile = useMediaQuery(theme.breakpoints.down(600)); const translationType = { count: "за единицу", @@ -24,6 +26,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => { }; const putPrivileges = async () => { + const [_, putedPrivilegeError] = await putPrivilege({ name: privilege.name, privilegeId: privilege.privilegeId, @@ -79,7 +82,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => { }} > - + { - + {inputOpen ? ( { onChange={(event) => setInputValue(event.target.value)} sx={{ alignItems: "center", - width: "400px", + maxWidth: "400px", + width: "100%", + marginLeft: "5px", "& .MuiInputBase-root": { backgroundColor: "#F2F3F7", height: "48px", diff --git a/src/pages/Setting/FormCreateRoles.tsx b/src/pages/Setting/FormCreateRoles.tsx index 2621fec..900a556 100644 --- a/src/pages/Setting/FormCreateRoles.tsx +++ b/src/pages/Setting/FormCreateRoles.tsx @@ -1,14 +1,14 @@ import { useState } from "react"; import { - Button, - Checkbox, - FormControl, - ListItemText, - MenuItem, - Select, - SelectChangeEvent, - TextField, + Button, + Checkbox, + FormControl, + ListItemText, + MenuItem, + Select, + SelectChangeEvent, + TextField, useMediaQuery, useTheme, } from "@mui/material"; import { MOCK_DATA_USERS } from "@root/api/roles"; @@ -24,7 +24,8 @@ const MenuProps = { export default function CreateForm() { const [personName, setPersonName] = useState([]); - + const theme = useTheme(); + const mobile = useMediaQuery(theme.breakpoints.down(600)); const handleChange = (event: SelectChangeEvent) => { const { target: { value }, @@ -39,7 +40,8 @@ export default function CreateForm() { fullWidth sx={{ alignItems: "center", - width: "400px", + maxWidth: "400px", + width: "100%", "& .MuiInputBase-root": { backgroundColor: "#F2F3F7", height: "48px", @@ -54,8 +56,8 @@ export default function CreateForm() { }, }} /> - - + + - Шаблонизатор - Опросник - Аналитика сокращателя - АБ тесты - - - - - - - - - - Работает, если заплатите 100500 денег - - - - - Вы должны будете продать душу дьяволу - - - -
-
- - - Дата действия: - - - - С - - { if (e) { setValue1(e); } }} - renderInput={(params) => } - InputProps={{ - sx: { - height: "40px", - color: theme.palette.secondary.main, - border: "1px solid", - borderColor: theme.palette.secondary.main, - "& .MuiSvgIcon-root": { color: theme.palette.secondary.main } - } - }} - /> - - по - - { if (e) { setValue2(e); } }} - renderInput={(params) => } - InputProps={{ - sx: { - height: "40px", - color: theme.palette.secondary.main, - border: "1px solid", - borderColor: theme.palette.secondary.main, - "& .MuiSvgIcon-root": { color: theme.palette.secondary.main } - } - }} - /> - - - - - - toggleCheckbox()} /> - - - Бессрочно - - - - - - - -
- - - - console.log("datagrid select")} - /> - - - - - - ); -}; - - -export default PromocodeManagement; diff --git a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx new file mode 100644 index 0000000..9ce1870 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx @@ -0,0 +1,372 @@ +import { + Button, + FormControlLabel, + MenuItem, + Radio, + RadioGroup, + Select, + TextField, + Typography, +} from "@mui/material"; +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 { usePrivilegeStore } from "@root/stores/privilegesStore"; + +import { SERVICE_LIST } from "@root/model/privilege"; +import theme from "@root/theme"; + +import type { TextFieldProps } from "@mui/material"; +import { CreatePromocodeBody } from "@root/model/promocodes"; +import type { ChangeEvent } from "react"; + +type BonusType = "discount" | "privilege"; + +type FormValues = { + codeword: string; + description: string; + greetings: string; + dueTo: number; + activationCount: number; + privilegeId: string; + amount: number; + layer: 1 | 2; + factor: number; + target: string; + threshold: number; +}; + +const initialValues: FormValues = { + codeword: "", + description: "", + greetings: "", + dueTo: 0, + activationCount: 0, + privilegeId: "", + amount: 0, + layer: 1, + factor: 0, + target: "", + threshold: 0, +}; + +type Props = { + createPromocode: (body: CreatePromocodeBody) => Promise; +}; + +export const CreatePromocodeForm = ({ createPromocode }: Props) => { + const [bonusType, setBonusType] = useState("discount"); + const { privileges } = usePrivilegeStore(); + + useEffect(() => { + requestPrivileges(); + }, []); + + const submitForm = (values: FormValues) => { + const body = { + ...values, + 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, + }, + }, + }); + }; + + return ( + + {({ values, handleChange, handleBlur, setFieldValue }) => ( +
+ + + + + Время существования промокода + + { + if (date) { + setFieldValue("dueTo", moment(date).unix() || null); + } + }} + renderInput={(params: TextFieldProps) => } + InputProps={{ + sx: { + height: "40px", + color: theme.palette.secondary.main, + border: "1px solid", + borderColor: theme.palette.secondary.main, + "& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, + }, + }} + /> + + setFieldValue( + "activationCount", + Number(target.value.replace(/\D/g, "")) + ) + } + /> + ) => { + setBonusType(target.value as BonusType); + }} + onBlur={handleBlur} + > + } + label="Скидка" + /> + } + label="Привилегия" + /> + + {bonusType === "discount" && ( + <> + ) => { + setFieldValue("target", ""); + setFieldValue("layer", Number(target.value)); + }} + onBlur={handleBlur} + > + } + label="Привилегия" + /> + } + label="Сервис" + /> + + { + setFieldValue( + "factor", + Number(target.value.replace(/\D/g, "")) + ); + }} + /> + + {values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"} + + ( + + {name} + + )) + : SERVICE_LIST.map(({ displayName, serviceKey }) => ( + + {displayName} + + )) + } + /> + + setFieldValue( + "threshold", + Number(target.value.replace(/\D/g, "")) + ) + } + /> + + )} + {bonusType === "privilege" && ( + <> + + Выбор привилегии + + ( + + {name} + + ))} + /> + + setFieldValue( + "amount", + Number(target.value.replace(/\D/g, "")) + ) + } + /> + + )} + + + )} +
+ ); +}; + +type CustomTextFieldProps = { + name: string; + label: string; + required?: boolean; + onChange: (event: ChangeEvent) => void; +}; + +const CustomTextField = ({ + name, + label, + required = false, + onChange, +}: CustomTextFieldProps) => ( + +); diff --git a/src/pages/dashboard/Content/PromocodeManagement/index.tsx b/src/pages/dashboard/Content/PromocodeManagement/index.tsx new file mode 100644 index 0000000..bdd7be5 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/index.tsx @@ -0,0 +1,78 @@ +import { Box, Typography, useTheme } from "@mui/material"; +import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { usePromocodes } from "@root/api/promocode/swr"; +import { fadeIn } from "@root/utils/style/keyframes"; +import { useState } from "react"; +import { CreatePromocodeForm } from "./CreatePromocodeForm"; +import { usePromocodeGridColDef } from "./usePromocodeGridColDef"; + + +export const PromocodeManagement = () => { + const theme = useTheme(); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + const { data, error, isValidating, promocodesCount, deletePromocode, createPromocode } = usePromocodes(page, pageSize); + const columns = usePromocodeGridColDef(deletePromocode); + + if (error) return Ошибка загрузки промокодов; + + return ( + + + Создание промокода + + + + + + + ); +}; diff --git a/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx b/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx new file mode 100644 index 0000000..a897d00 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx @@ -0,0 +1,59 @@ +import DeleteIcon from '@mui/icons-material/Delete'; +import { IconButton } from "@mui/material"; +import { GridColDef } from "@mui/x-data-grid"; +import { Promocode } from "@root/model/promocodes"; +import { useMemo } from "react"; + +export function usePromocodeGridColDef(deletePromocode: (id: string) => void) { + return useMemo[]>(() => [ + { + field: "id", + headerName: "ID", + width: 30, + sortable: false, + valueGetter: ({ row }) => row.id, + }, + { + field: "codeword", + headerName: "Кодовое слово", + width: 160, + sortable: false, + valueGetter: ({ row }) => row.codeword, + }, + { + field: "factor", + headerName: "Коэф. скидки", + width: 120, + 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 }) => row.dueTo * 1000, + valueFormatter: ({ value }) => new Date(value).toLocaleString(), + }, + { + field: "delete", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => { + return ( + deletePromocode(params.row.id)}> + + + ); + }, + }, + ], [deletePromocode]); +} diff --git a/src/pages/dashboard/Content/ServiceUsersDG.tsx b/src/pages/dashboard/Content/ServiceUsersDG.tsx index 9f0c9cd..2aca18d 100644 --- a/src/pages/dashboard/Content/ServiceUsersDG.tsx +++ b/src/pages/dashboard/Content/ServiceUsersDG.tsx @@ -42,11 +42,21 @@ const columns: GridColDef[] = [ interface Props { handleSelectionChange: (selectionModel: GridSelectionModel) => void; users: UserType[]; + page: number; + setPage: (page: number) => void; + pageSize: number; + pagesCount: number; + onPageSizeChange?: (count: number) => void; } export default function ServiceUsersDG({ handleSelectionChange, users = [], + page, + setPage, + pageSize = 10, + pagesCount = 1, + onPageSizeChange, }: Props) { const navigate = useNavigate(); @@ -60,6 +70,13 @@ export default function ServiceUsersDG({ rows={users} columns={columns} components={{ Toolbar: GridToolbar }} + rowCount={pageSize * pagesCount} + rowsPerPageOptions={[10, 25, 50, 100]} + paginationMode="server" + page={page} + pageSize={pageSize} + onPageChange={setPage} + onPageSizeChange={onPageSizeChange} onSelectionModelChange={handleSelectionChange} onCellClick={({ row }, event) => { event.stopPropagation(); diff --git a/src/pages/dashboard/Content/Support/Chat/Chat.tsx b/src/pages/dashboard/Content/Support/Chat/Chat.tsx index ce6b471..8907b0a 100644 --- a/src/pages/dashboard/Content/Support/Chat/Chat.tsx +++ b/src/pages/dashboard/Content/Support/Chat/Chat.tsx @@ -9,8 +9,39 @@ import { TicketMessage } from "@root/model/ticket"; import { sendTicketMessage } from "@root/api/tickets"; import { enqueueSnackbar } from "notistack"; import { useTicketStore } from "@root/stores/tickets"; -import { getMessageFromFetchError, throttle, useEventListener, useSSESubscription, useTicketMessages, useToken } from "@frontend/kitui"; +import { getMessageFromFetchError, makeRequest, throttle, useEventListener, useSSESubscription, useTicketMessages, useToken } from "@frontend/kitui"; +import ChatImage from "./ChatImage"; +import ChatDocument from "./ChatDocument"; +import ChatVideo from "./ChatVideo"; +import ChatMessage from "./ChatMessage"; +import { ACCEPT_SEND_MEDIA_TYPES_MAP, MAX_FILE_SIZE, MAX_PHOTO_SIZE, MAX_VIDEO_SIZE } from "./fileUpload"; +const tooLarge = "Файл слишком большой" +const checkAcceptableMediaType = (file: File) => { + if (file === null) return "" + + const segments = file?.name.split('.'); + const extension = segments[segments.length - 1]; + const type = extension.toLowerCase(); + + console.log(type) + switch (type) { + case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find(name => name === type): + if (file.size > MAX_FILE_SIZE) return tooLarge + return "" + + case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find(name => name === type): + if (file.size > MAX_PHOTO_SIZE) return tooLarge + return "" + + case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find(name => name === type): + if (file.size > MAX_VIDEO_SIZE) return tooLarge + return "" + + default: + return "Не удалось отправить файл. Недопустимый тип" + } + } export default function Chat() { const token = useToken(); @@ -26,6 +57,8 @@ export default function Chat() { const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll); const fetchState = useMessageStore(state => state.ticketMessagesFetchState); const lastMessageId = useMessageStore(state => state.lastMessageId); + const fileInputRef = useRef(null); + const [disableFileButton, setDisableFileButton] = useState(false); const ticket = tickets.find(ticket => ticket.id === ticketId); @@ -107,7 +140,47 @@ export default function Chat() { setMessageField(""); } - function handleAddAttachment() { } + const sendFile = async (file: File) => { + if (file === undefined) return true; + + // const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP.some( + // fileType => file.name.toLowerCase().endsWith(fileType) + // ); + // console.log(file.name.toLowerCase().endsWith(".png")) + // if (!isFileTypeAccepted) return setModalWarningType("errorType"); + let data; + + const ticketId = ticket?.id + if (ticketId !== undefined) { + try { + const body = new FormData(); + + body.append(file.name, file); + body.append("ticket", ticketId); + await makeRequest({ + url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles", + body: body, + method: "POST", + }); + } catch (error: any) { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) enqueueSnackbar(errorMessage); + } + return true; + } + }; + const sendFileHC = async (file: File) => { + console.log(file) + const check = checkAcceptableMediaType(file) + if (check.length > 0) { + enqueueSnackbar(check) + return + } + setDisableFileButton(true) + await sendFile(file) + setDisableFileButton(false) + console.log(disableFileButton) + }; function handleTextfieldKeyPress(e: KeyboardEvent) { if (e.key === "Enter" && !e.shiftKey) { @@ -145,9 +218,66 @@ export default function Chat() { colorScheme: "dark", }} > - {ticket && messages.map(message => - - )} + {ticket && + messages.map((message) => { + const isFileVideo = () => { + if (message.files) { + return (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) => + message.files[0].toLowerCase().endsWith(fileType), + )) + } + }; + const isFileImage = () => { + if (message.files) { + return (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) => + message.files[0].toLowerCase().endsWith(fileType), + )) + } + }; + const isFileDocument = () => { + if (message.files) { + return (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) => + message.files[0].toLowerCase().endsWith(fileType), + )) + } + }; + if (message.files !== null && message.files.length > 0 && isFileImage()) { + return + } + if (message.files !== null && message.files.length > 0 && isFileVideo()) { + return + } + if (message.files !== null && message.files.length > 0 && isFileDocument()) { + return + } + return + + }) + }
{ticket && { + console.log(disableFileButton) + if (!disableFileButton) fileInputRef.current?.click() + }} sx={{ height: "45px", width: "45px", p: 0, }} > + { + if (e.target.files?.[0]) sendFileHC(e.target.files?.[0]); + }} + style={{ display: "none" }} + type="file" + /> diff --git a/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx b/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx new file mode 100644 index 0000000..f180a67 --- /dev/null +++ b/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx @@ -0,0 +1,61 @@ +import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; +import DownloadIcon from '@mui/icons-material/Download'; + +interface Props { + unAuthenticated?: boolean; + isSelf: boolean; + file: string; + createdAt: string; +} + +export default function ChatDocument({ + unAuthenticated = false, + isSelf, + file, + createdAt, +}: Props) { + const theme = useTheme(); + + const date = new Date(createdAt); + + return ( + + + {new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + + + + + ); +} diff --git a/src/pages/dashboard/Content/Support/Chat/ChatImage.tsx b/src/pages/dashboard/Content/Support/Chat/ChatImage.tsx new file mode 100644 index 0000000..8842d48 --- /dev/null +++ b/src/pages/dashboard/Content/Support/Chat/ChatImage.tsx @@ -0,0 +1,67 @@ +import { + Box, + ButtonBase, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +interface Props { + unAuthenticated?: boolean; + isSelf: boolean; + file: string; + createdAt: string; +} + +export default function ChatImage({ + unAuthenticated = false, + isSelf, + file, + createdAt, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + + + return ( + + + {new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + + + + + ); +} diff --git a/src/pages/dashboard/Content/Support/Chat/ChatMessage.tsx b/src/pages/dashboard/Content/Support/Chat/ChatMessage.tsx new file mode 100644 index 0000000..0c944a0 --- /dev/null +++ b/src/pages/dashboard/Content/Support/Chat/ChatMessage.tsx @@ -0,0 +1,52 @@ +import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; + +interface Props { + unAuthenticated?: boolean; + isSelf: boolean; + text: string; + createdAt: string; +} + +export default function ChatMessage({ + unAuthenticated = false, + isSelf, + text, + createdAt, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + + + return ( + + + {new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + {text} + + + + ); +} diff --git a/src/pages/dashboard/Content/Support/Chat/ChatVideo.tsx b/src/pages/dashboard/Content/Support/Chat/ChatVideo.tsx new file mode 100644 index 0000000..5fb281f --- /dev/null +++ b/src/pages/dashboard/Content/Support/Chat/ChatVideo.tsx @@ -0,0 +1,68 @@ +import { + Box, + ButtonBase, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; + +interface Props { + unAuthenticated?: boolean; + isSelf: boolean; + file: string; + createdAt: string; +} + +export default function ChatImage({ + unAuthenticated = false, + isSelf, + file, + createdAt, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + + + return ( + + + {new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + + + + + ); +} diff --git a/src/pages/dashboard/Content/Support/Chat/fileUpload.ts b/src/pages/dashboard/Content/Support/Chat/fileUpload.ts new file mode 100644 index 0000000..19410e1 --- /dev/null +++ b/src/pages/dashboard/Content/Support/Chat/fileUpload.ts @@ -0,0 +1,9 @@ +export const MAX_FILE_SIZE = 10485760; +export const MAX_PHOTO_SIZE = 5242880; +export const MAX_VIDEO_SIZE = 52428800; + +export const ACCEPT_SEND_MEDIA_TYPES_MAP = { + picture: ["jpg", "png"], + video: ["mp4"], + document: ["doc", "docx", "pdf", "txt", "xlsx", "csv"], + } as const; \ No newline at end of file diff --git a/src/pages/dashboard/Content/Support/ChatImageNewWindow.tsx b/src/pages/dashboard/Content/Support/ChatImageNewWindow.tsx new file mode 100644 index 0000000..3adc7e4 --- /dev/null +++ b/src/pages/dashboard/Content/Support/ChatImageNewWindow.tsx @@ -0,0 +1,20 @@ +import { Box } from "@mui/material"; +import { useLocation } from "react-router-dom"; + +export default function ChatImageNewWindow() { + const location = useLocation(); + console.log(location); + const srcImage = location.pathname.split("image/")[1]; + return ( + <> + + + ); +} diff --git a/src/pages/dashboard/Content/Support/Collapse.tsx b/src/pages/dashboard/Content/Support/Collapse.tsx index 5aa9870..e6e6962 100644 --- a/src/pages/dashboard/Content/Support/Collapse.tsx +++ b/src/pages/dashboard/Content/Support/Collapse.tsx @@ -2,52 +2,52 @@ import { ReactNode, useState } from "react"; import { Box, Typography, useTheme } from "@mui/material"; import ExpandIcon from "./ExpandIcon"; - interface Props { - headerText: string; - children: ReactNode; + headerText: string; + children: (callback: () => void) => ReactNode; } export default function Collapse({ headerText, children }: Props) { - const theme = useTheme(); - const [isExpanded, setIsExpanded] = useState(false); + const theme = useTheme(); + const [isExpanded, setIsExpanded] = useState(false); - return ( + return ( + + setIsExpanded((prev) => !prev)} + sx={{ + height: "72px", + p: "16px", + backgroundColor: theme.palette.menu.main, + borderRadius: "12px", + + display: "flex", + justifyContent: "space-between", + alignItems: "center", + cursor: "pointer", + userSelect: "none", + }} + > + {headerText} + + + {isExpanded && ( - setIsExpanded(prev => !prev)} - sx={{ - height: "72px", - p: "16px", - backgroundColor: theme.palette.menu.main, - borderRadius: "12px", - - display: "flex", - justifyContent: "space-between", - alignItems: "center", - cursor: "pointer", - userSelect: "none", - }} - > - {headerText} - - - {isExpanded && - - {children} - - } - - - ); -} \ No newline at end of file + {children(() => setIsExpanded(false))} + + )} + + ); +} diff --git a/src/pages/dashboard/Content/Support/Support.tsx b/src/pages/dashboard/Content/Support/Support.tsx index ab376c1..55735aa 100644 --- a/src/pages/dashboard/Content/Support/Support.tsx +++ b/src/pages/dashboard/Content/Support/Support.tsx @@ -1,61 +1,103 @@ +import { useState, useEffect } from "react"; import { Box, useMediaQuery, useTheme } from "@mui/material"; import Chat from "./Chat/Chat"; import Collapse from "./Collapse"; import TicketList from "./TicketList/TicketList"; import { Ticket } from "@root/model/ticket"; -import { clearTickets, setTicketsFetchState, updateTickets, useTicketStore } from "@root/stores/tickets"; +import { + clearTickets, + setTicketsFetchState, + updateTickets, + useTicketStore, +} from "@root/stores/tickets"; import { enqueueSnackbar } from "notistack"; import { clearMessageState } from "@root/stores/messages"; -import { getMessageFromFetchError, useSSESubscription, useTicketsFetcher, useToken } from "@frontend/kitui"; +import { + getMessageFromFetchError, + useSSESubscription, + useTicketsFetcher, + useToken, +} from "@frontend/kitui"; +import ModalUser from "@root/pages/dashboard/ModalUser"; export default function Support() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); - const ticketApiPage = useTicketStore((state) => state.apiPage); - const token = useToken(); + const [openUserModal, setOpenUserModal] = useState(false); + const [activeUserId, setActiveUserId] = useState(""); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); + const ticketApiPage = useTicketStore((state) => state.apiPage); + const token = useToken(); - useTicketsFetcher({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", - ticketsPerPage, - ticketApiPage, - onSuccess: result => { - if (result.data) updateTickets(result.data); - }, - onError: (error: Error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }, - onFetchStateChange: setTicketsFetchState, - }); + useTicketsFetcher({ + url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", + ticketsPerPage, + ticketApiPage, + onSuccess: (result) => { + if (result.data) updateTickets(result.data); + }, + onError: (error: Error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, + onFetchStateChange: setTicketsFetchState, + }); - useSSESubscription({ - enabled: Boolean(token), - url: process.env.REACT_APP_DOMAIN + `/heruvym/subscribe?Authorization=${token}`, - onNewData: updateTickets, - onDisconnect: () => { - clearMessageState(); - clearTickets(); - }, - marker: "ticket" - }); + useSSESubscription({ + enabled: Boolean(token), + url: + process.env.REACT_APP_DOMAIN + + `/heruvym/subscribe?Authorization=${token}`, + onNewData: updateTickets, + onDisconnect: () => { + clearMessageState(); + clearTickets(); + }, + marker: "ticket", + }); - return ( - - {!upMd && ( - - - - )} - - {upMd && } - - ); + useEffect(() => { + if (!openUserModal) { + setActiveUserId(""); + } + }, [openUserModal]); + + useEffect(() => { + if (activeUserId) { + setOpenUserModal(true); + + return; + } + + setOpenUserModal(false); + }, [activeUserId]); + + return ( + + {!upMd && ( + + {(closeCollapse) => ( + + )} + + )} + + {upMd && } + setOpenUserModal(false)} + userId={activeUserId} + /> + + ); } diff --git a/src/pages/dashboard/Content/Support/TicketList/TicketItem.tsx b/src/pages/dashboard/Content/Support/TicketList/TicketItem.tsx index b0366f8..82a8456 100644 --- a/src/pages/dashboard/Content/Support/TicketList/TicketItem.tsx +++ b/src/pages/dashboard/Content/Support/TicketList/TicketItem.tsx @@ -1,91 +1,108 @@ import CircleIcon from "@mui/icons-material/Circle"; -import { Box, Card, CardActionArea, CardContent, CardHeader, Divider, Typography, useTheme } from "@mui/material"; +import { + Box, + Card, + CardActionArea, + CardContent, + CardHeader, + Divider, + Typography, + useTheme, +} from "@mui/material"; import { green } from "@mui/material/colors"; import { Ticket } from "@root/model/ticket"; import { useNavigate, useParams } from "react-router-dom"; - const flexCenterSx = { - textAlign: "center", - display: "flex", - justifyContent: "center", - alignItems: "center", - padding: "10px", + textAlign: "center", + display: "flex", + justifyContent: "center", + alignItems: "center", + padding: "10px", }; interface Props { - ticket: Ticket; + ticket: Ticket; + setActiveUserId: (userId: string) => void; } -export default function TicketItem({ ticket }: Props) { - const theme = useTheme(); - const navigate = useNavigate(); - const ticketId = useParams().ticketId; +export default function TicketItem({ ticket, setActiveUserId }: Props) { + const theme = useTheme(); + const navigate = useNavigate(); + const ticketId = useParams().ticketId; - const isUnread = ticket.user === ticket.top_message.user_id; - const isSelected = ticket.id === ticketId; + const isUnread = ticket.user === ticket.top_message.user_id; + const isSelected = ticket.id === ticketId; - const unreadSx = { - border: "1px solid", - borderColor: theme.palette.golden.main, - backgroundColor: theme.palette.goldenMedium.main - }; + const unreadSx = { + border: "1px solid", + borderColor: theme.palette.golden.main, + backgroundColor: theme.palette.goldenMedium.main, + }; - const selectedSx = { - border: `2px solid ${theme.palette.secondary.main}`, - }; + const selectedSx = { + border: `2px solid ${theme.palette.secondary.main}`, + }; - function handleCardClick() { - navigate(`/support/${ticket.id}`); - } + function handleCardClick() { + navigate(`/support/${ticket.id}`); + } - return ( - + + {ticket.title}} + disableTypography + sx={{ + textAlign: "center", + p: "4px", + }} + /> + + - - {ticket.title}} - disableTypography - sx={{ - textAlign: "center", - p: "4px", - }} - /> - - - - {new Date(ticket.top_message.created_at).toLocaleDateString()} - - - {ticket.top_message.message} - - - - - - ИНФО - - - - - ); -} \ No newline at end of file + p: 0, + }} + > + + {new Date(ticket.top_message.created_at).toLocaleDateString()} + + + {ticket.top_message.message} + + + + + setActiveUserId(ticket.user)}> + ИНФО + + + + + ); +} diff --git a/src/pages/dashboard/Content/Support/TicketList/TicketList.tsx b/src/pages/dashboard/Content/Support/TicketList/TicketList.tsx index 7d92abf..0dcfcdf 100644 --- a/src/pages/dashboard/Content/Support/TicketList/TicketList.tsx +++ b/src/pages/dashboard/Content/Support/TicketList/TicketList.tsx @@ -1,122 +1,146 @@ -import HighlightOffOutlinedIcon from '@mui/icons-material/HighlightOffOutlined'; -import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined'; +import HighlightOffOutlinedIcon from "@mui/icons-material/HighlightOffOutlined"; +import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined"; import { Box, Button, useMediaQuery, useTheme } from "@mui/material"; import { Ticket } from "@root/model/ticket"; import { incrementTicketsApiPage, useTicketStore } from "@root/stores/tickets"; import { useEffect, useRef } from "react"; import TicketItem from "./TicketItem"; -import { throttle } from '@frontend/kitui'; +import { throttle } from "@frontend/kitui"; +type TicketListProps = { + closeCollapse?: () => void; + setActiveUserId: (id: string) => void; +}; -export default function TicketList() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const tickets = useTicketStore(state => state.tickets); - const ticketsFetchState = useTicketStore(state => state.ticketsFetchState); - const ticketsBoxRef = useRef(null); +export default function TicketList({ + closeCollapse, + setActiveUserId, +}: TicketListProps) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const tickets = useTicketStore((state) => state.tickets); + const ticketsFetchState = useTicketStore((state) => state.ticketsFetchState); + const ticketsBoxRef = useRef(null); - useEffect(function updateCurrentPageOnScroll() { - if (!ticketsBoxRef.current) return; + useEffect( + function updateCurrentPageOnScroll() { + if (!ticketsBoxRef.current) return; - const ticketsBox = ticketsBoxRef.current; - const scrollHandler = () => { - const scrollBottom = ticketsBox.scrollHeight - ticketsBox.scrollTop - ticketsBox.clientHeight; - if ( - scrollBottom < ticketsBox.clientHeight && - ticketsFetchState === "idle" - ) incrementTicketsApiPage(); - }; + const ticketsBox = ticketsBoxRef.current; + const scrollHandler = () => { + const scrollBottom = + ticketsBox.scrollHeight - + ticketsBox.scrollTop - + ticketsBox.clientHeight; + if ( + scrollBottom < ticketsBox.clientHeight && + ticketsFetchState === "idle" + ) + incrementTicketsApiPage(); + }; - const throttledScrollHandler = throttle(scrollHandler, 200); - ticketsBox.addEventListener("scroll", throttledScrollHandler); + const throttledScrollHandler = throttle(scrollHandler, 200); + ticketsBox.addEventListener("scroll", throttledScrollHandler); - return () => { - ticketsBox.removeEventListener("scroll", throttledScrollHandler); - }; - }, [ticketsFetchState]); + return () => { + ticketsBox.removeEventListener("scroll", throttledScrollHandler); + }; + }, + [ticketsFetchState] + ); - const sortedTickets = tickets.sort(sortTicketsByUpdateTime).sort(sortTicketsByUnread); + const sortedTickets = tickets + .sort(sortTicketsByUpdateTime) + .sort(sortTicketsByUnread); - return ( - - - - - - - {sortedTickets.map(ticket => - - )} - - - ); + return ( + + + + + + + {sortedTickets.map((ticket) => ( + + + + ))} + + + ); } function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) { - const date1 = new Date(ticket1.updated_at).getTime(); - const date2 = new Date(ticket2.updated_at).getTime(); - return date2 - date1; + const date1 = new Date(ticket1.updated_at).getTime(); + const date2 = new Date(ticket2.updated_at).getTime(); + return date2 - date1; } function sortTicketsByUnread(ticket1: Ticket, ticket2: Ticket) { - const isUnread1 = ticket1.user === ticket1.top_message.user_id; - const isUnread2 = ticket2.user === ticket2.top_message.user_id; - return Number(isUnread2) - Number(isUnread1); + const isUnread1 = ticket1.user === ticket1.top_message.user_id; + const isUnread2 = ticket2.user === ticket2.top_message.user_id; + return Number(isUnread2) - Number(isUnread1); } diff --git a/src/pages/dashboard/Content/Tariffs/CreateTariff.tsx b/src/pages/dashboard/Content/Tariffs/CreateTariff.tsx index 1ba890b..f3785e1 100644 --- a/src/pages/dashboard/Content/Tariffs/CreateTariff.tsx +++ b/src/pages/dashboard/Content/Tariffs/CreateTariff.tsx @@ -187,6 +187,7 @@ export default function CreateTariff() { data-cy={`select-option-${privilege.description}`} key={privilege.description} value={privilege._id} + sx={{whiteSpace: "normal", wordBreak: "break-world"}} > {privilege.serviceKey}:{privilege.description} diff --git a/src/pages/dashboard/Content/Tariffs/EditModal.tsx b/src/pages/dashboard/Content/Tariffs/EditModal.tsx index c7d2594..627e0d0 100644 --- a/src/pages/dashboard/Content/Tariffs/EditModal.tsx +++ b/src/pages/dashboard/Content/Tariffs/EditModal.tsx @@ -44,7 +44,7 @@ export default function EditModal() { updatedTariff.name = nameField; updatedTariff.price = price; updatedTariff.description = descriptionField; - updatedTariff.order = orderField; + updatedTariff.order = parseInt(orderField); const [_, putedTariffError] = await putTariff(updatedTariff); diff --git a/src/pages/dashboard/Content/Users.tsx b/src/pages/dashboard/Content/Users.tsx index 7fd8010..dfc4a89 100644 --- a/src/pages/dashboard/Content/Users.tsx +++ b/src/pages/dashboard/Content/Users.tsx @@ -23,14 +23,24 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ConditionalRender from "@root/pages/Setting/ConditionalRender"; import ModalUser from "@root/pages/dashboard/ModalUser"; import ServiceUsersDG from "./ServiceUsersDG"; -import { getRegisteredUsers, getManagersList } from "@root/api/user"; +import { useUsers, useManagers, useAdmins } from "@root/api/user/swr"; import { getRoles } from "@root/api/privilegies"; import { getRoles_mock, TMockData } from "../../../api/roles"; import theme from "../../../theme"; -import type { UserType } from "../../../api/roles"; +type Pages = { + adminPage: number; + managerPage: number; + userPage: number; +}; + +type PagesSize = { + adminPageSize: number; + managerPageSize: number; + userPageSize: number; +}; const Users: React.FC = () => { const radioboxes = ["admin", "manager", "user"]; @@ -39,11 +49,11 @@ const Users: React.FC = () => { const navigate = useNavigate(); - const [data, setData] = React.useState([]); + const [mockData, setMockData] = React.useState([]); const handleChangeData = () => { getRoles_mock().then((mockdata) => { - setData(mockdata); + setMockData(mockdata); setAccordionText(mockdata[0].desc || ""); }); }; @@ -53,7 +63,7 @@ const Users: React.FC = () => { const handleChange = (value: string) => { setSelectedValue(value); - setAccordionText(data.find(({ name }) => name === value)?.desc || ""); + setAccordionText(mockData.find(({ name }) => name === value)?.desc || ""); if (selectedValue === "manager") { } @@ -64,12 +74,33 @@ const Users: React.FC = () => { }; const [roles, setRoles] = React.useState([]); - const [users, setUsers] = React.useState([]); - const [manager, setManager] = React.useState([]); + + const [page, setPage] = useState({ + adminPage: 0, + managerPage: 0, + userPage: 0, + }); + const [pageSize, setPageSize] = useState({ + adminPageSize: 10, + managerPageSize: 10, + userPageSize: 10, + }); const [openUserModal, setOpenUserModal] = useState(false); const [activeUserId, setActiveUserId] = useState(""); const { userId } = useParams(); + const { data: adminData, adminPages } = useAdmins( + page.adminPage + 1, + pageSize.adminPageSize + ); + const { data: managerData, managerPages } = useManagers( + page.managerPage + 1, + pageSize.managerPageSize + ); + const { data: userData, userPagesCount } = useUsers( + page.userPage + 1, + pageSize.userPageSize + ); useEffect(() => { handleChangeData(); @@ -88,18 +119,6 @@ const Users: React.FC = () => { }, [userId]); useEffect(() => { - getManagersList().then(([managersListResponse]) => { - if (managersListResponse) { - setManager(managersListResponse.users); - } - }); - - getRegisteredUsers().then(([registeredUsersResponse]) => { - if (registeredUsersResponse) { - setUsers(registeredUsersResponse.users); - } - }); - getRoles().then(([rolesResponse]) => { if (rolesResponse) { setRoles(rolesResponse); @@ -107,7 +126,9 @@ const Users: React.FC = () => { }); }, [selectedValue]); - const [selectedTariffs, setSelectedTariffs] = useState([]); + const [selectedTariffs, setSelectedTariffs] = useState( + [] + ); return (