From 77c2d834c97696ea041e4dc3d44459d319d81ede Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Wed, 21 Feb 2024 12:00:40 +0300 Subject: [PATCH 01/13] feat: info button logic --- src/assets/icons/close.svg | 74 ++++++ .../dashboard/Content/Support/Collapse.tsx | 84 +++---- .../dashboard/Content/Support/Support.tsx | 138 +++++++---- .../Content/Support/TicketList/TicketItem.tsx | 165 +++++++------ .../Content/Support/TicketList/TicketList.tsx | 228 ++++++++++-------- src/pages/dashboard/Content/Users.tsx | 31 ++- src/pages/dashboard/ModalUser/index.tsx | 21 +- 7 files changed, 461 insertions(+), 280 deletions(-) create mode 100644 src/assets/icons/close.svg 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/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/Users.tsx b/src/pages/dashboard/Content/Users.tsx index 7fd8010..4eece62 100644 --- a/src/pages/dashboard/Content/Users.tsx +++ b/src/pages/dashboard/Content/Users.tsx @@ -107,7 +107,9 @@ const Users: React.FC = () => { }); }, [selectedValue]); - const [selectedTariffs, setSelectedTariffs] = useState([]); + const [selectedTariffs, setSelectedTariffs] = useState( + [] + ); return ( - - - - - - - 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..61d4f67 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx @@ -0,0 +1,300 @@ +import { useEffect, useState } from "react"; +import { + Typography, + TextField, + Button, + RadioGroup, + Radio, + FormControlLabel, + Select, +} from "@mui/material"; +import { Formik, Field, Form } from "formik"; +import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker"; +import MenuItem from "@mui/material/MenuItem"; +import theme from "../../../../theme"; +import { requestPrivileges } from "@root/services/privilegies.service"; +import { usePrivilegeStore } from "@root/stores/privilegesStore"; +import { useCartStore } from "@root/stores/cart"; + +import type { TextFieldProps } from "@mui/material"; + +type BonusType = "discount" | "privilege"; +type LayerType = "privilege" | "service"; +type FormValues = { + codeword: string; + description: string; + greetings: string; + dueTo: string; + activationCount: string; + privilegeId: string; + amount: string; + layer: string; + factor: string; + target: string; + threshold: string; +}; + +type CustomTextFieldProps = { + name: string; + label: string; + required?: boolean; +}; + +const CustomTextField = ({ + name, + label, + required = false, +}: CustomTextFieldProps) => ( + +); + +export const CreatePromocodeForm = () => { + const [dueTo, setDueTo] = useState(new Date()); + const [bonusType, setBonusType] = useState("discount"); + const [layerType, setLayerType] = useState("privilege"); + + const { privileges } = usePrivilegeStore(); + const { cartData } = useCartStore(); + + const initialValues: FormValues = { + codeword: "", + description: "", + greetings: "", + dueTo: "", + activationCount: "", + privilegeId: "", + amount: "", + layer: "", + factor: "", + target: "", + threshold: "", + }; + + useEffect(() => { + requestPrivileges(); + }, []); + + const createPromocode = (values: FormValues) => { + console.log(values); + }; + + return ( + + {(props) => ( +
+ + + + + Время существования промокода + + { + if (date) { + setDueTo(date); + } + }} + 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 }, + }, + }} + /> + + ) => { + setBonusType(target.value as BonusType); + }} + onBlur={props.handleBlur} + > + } + label="Скидка" + /> + } + label="Привилегия" + /> + + {bonusType === "discount" && ( + <> + ) => { + setLayerType(target.value as LayerType); + }} + onBlur={props.handleBlur} + > + } + label="Привилегия" + /> + } + label="Сервис" + /> + + + + {layerType === "privilege" + ? "Выбор привилегии" + : "Выбор сервиса"} + + ( + + {name} + + )) + : cartData?.services.map(({ serviceKey }) => ( + + {serviceKey} + + )) + } + /> + + + )} + {bonusType === "privilege" && ( + <> + + Выбор привилегии + + ( + + {name} + + ))} + /> + + + )} + + + )} +
+ ); +}; diff --git a/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx b/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx new file mode 100644 index 0000000..ef88c39 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx @@ -0,0 +1,56 @@ +import { Box } from "@mui/material"; +import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid"; + +import { usePromocodeStore } from "@root/stores/promocodes"; + +import theme from "@root/theme"; + +const columns: GridColDef[] = [ + { field: "id", headerName: "ID", width: 30, sortable: false }, + { + field: "name", + headerName: "Название промокода", + width: 200, + sortable: false, + }, + { field: "endless", headerName: "Бесконечный", width: 120, sortable: false }, + { field: "from", headerName: "От", width: 120, sortable: false }, + { field: "dueTo", headerName: "До", width: 120, sortable: false }, + { + field: "privileges", + headerName: "Привилегии", + width: 210, + sortable: false, + }, +]; + +export const PromocodesList = () => { + const { promocodes } = usePromocodeStore(); + + return ( + + + console.log("datagrid select")} + /> + + + ); +}; diff --git a/src/pages/dashboard/Content/PromocodeManagement/index.tsx b/src/pages/dashboard/Content/PromocodeManagement/index.tsx new file mode 100644 index 0000000..835cbfc --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/index.tsx @@ -0,0 +1,30 @@ +import { Typography } from "@mui/material"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; + +import { PromocodesList } from "./PromocodesList"; +import { CreatePromocodeForm } from "./CreatePromocodeForm"; + +import theme from "@root/theme"; + +export const PromocodeManagement = () => ( + + + Создание промокода + + + + +); From 430a6c2436613aab6ac77df4ed54e51f15194484 Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Fri, 23 Feb 2024 17:10:57 +0300 Subject: [PATCH 03/13] feat: CreatePromocodeForm logic --- .../CreatePromocodeForm.tsx | 151 ++++++++++++------ 1 file changed, 100 insertions(+), 51 deletions(-) diff --git a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx index 61d4f67..74a81f5 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx @@ -1,4 +1,6 @@ import { useEffect, useState } from "react"; +import moment from "moment"; +import { Formik, Field, Form } from "formik"; import { Typography, TextField, @@ -7,43 +9,46 @@ import { Radio, FormControlLabel, Select, + MenuItem, } from "@mui/material"; -import { Formik, Field, Form } from "formik"; import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker"; -import MenuItem from "@mui/material/MenuItem"; -import theme from "../../../../theme"; + import { requestPrivileges } from "@root/services/privilegies.service"; import { usePrivilegeStore } from "@root/stores/privilegesStore"; -import { useCartStore } from "@root/stores/cart"; +import { SERVICE_LIST } from "@root/model/privilege"; +import theme from "@root/theme"; + +import type { ChangeEvent } from "react"; import type { TextFieldProps } from "@mui/material"; type BonusType = "discount" | "privilege"; -type LayerType = "privilege" | "service"; type FormValues = { codeword: string; description: string; greetings: string; - dueTo: string; - activationCount: string; + dueTo: number; + activationCount: number; privilegeId: string; - amount: string; - layer: string; - factor: string; + amount: number; + layer: 1 | 2; + factor: number; target: string; - threshold: string; + threshold: number; }; type CustomTextFieldProps = { name: string; label: string; required?: boolean; + onChange: (event: ChangeEvent) => void; }; const CustomTextField = ({ name, label, required = false, + onChange, }: CustomTextFieldProps) => ( ); +const initialValues: FormValues = { + codeword: "", + description: "", + greetings: "", + dueTo: 0, + activationCount: 0, + privilegeId: "", + amount: 0, + layer: 1, + factor: 0, + target: "", + threshold: 0, +}; + export const CreatePromocodeForm = () => { - const [dueTo, setDueTo] = useState(new Date()); const [bonusType, setBonusType] = useState("discount"); - const [layerType, setLayerType] = useState("privilege"); - const { privileges } = usePrivilegeStore(); - const { cartData } = useCartStore(); - - const initialValues: FormValues = { - codeword: "", - description: "", - greetings: "", - dueTo: "", - activationCount: "", - privilegeId: "", - amount: "", - layer: "", - factor: "", - target: "", - threshold: "", - }; useEffect(() => { requestPrivileges(); }, []); const createPromocode = (values: FormValues) => { - console.log(values); + const body = { + ...values, + threshold: values.layer === 1 ? values.threshold : values.threshold * 100, + }; + + console.log(body); }; return ( - {(props) => ( + {({ values, handleChange, handleBlur, setFieldValue }) => (
- - + + { name="dueTo" as={DesktopDatePicker} inputFormat="DD/MM/YYYY" - value={dueTo} + value={values.dueTo ? new Date(Number(values.dueTo) * 1000) : null} onChange={(date: Date | null) => { if (date) { - setDueTo(date); + setFieldValue("dueTo", moment(date).unix() || null); } }} renderInput={(params: TextFieldProps) => } @@ -146,6 +165,12 @@ export const CreatePromocodeForm = () => { + setFieldValue( + "activationCount", + Number(target.value.replace(/\D/g, "")) + ) + } /> { onChange={({ target }: React.ChangeEvent) => { setBonusType(target.value as BonusType); }} - onBlur={props.handleBlur} + onBlur={handleBlur} > { ) => { - setLayerType(target.value as LayerType); + setFieldValue("target", ""); + setFieldValue("layer", Number(target.value)); }} - onBlur={props.handleBlur} + onBlur={handleBlur} > } label="Привилегия" /> } label="Сервис" /> - + + setFieldValue( + "factor", + Number(target.value.replace(/\D/g, "")) + ) + } + /> { color: theme.palette.secondary.main, }} > - {layerType === "privilege" - ? "Выбор привилегии" - : "Выбор сервиса"} + {values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"} { ".MuiSvgIcon-root ": { fill: theme.palette.secondary.main }, }} children={ - layerType === "privilege" + values.layer === 1 ? privileges.map(({ name, privilegeId }) => ( {name} )) - : cartData?.services.map(({ serviceKey }) => ( + : SERVICE_LIST.map(({ displayName, serviceKey }) => ( - {serviceKey} + {displayName} )) } @@ -237,7 +271,12 @@ export const CreatePromocodeForm = () => { + setFieldValue( + "threshold", + Number(target.value.replace(/\D/g, "")) + ) + } /> )} @@ -275,7 +314,17 @@ export const CreatePromocodeForm = () => { ))} /> - + + setFieldValue( + "amount", + Number(target.value.replace(/\D/g, "")) + ) + } + /> )} - - )} -
- ); + 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/PromocodesList.tsx b/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx index ef88c39..2913d2e 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx @@ -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 { usePromocodeStore } from "@root/stores/promocodes"; +import { Promocode } from "@root/model/promocodes"; +import DeleteIcon from '@mui/icons-material/Delete'; import theme from "@root/theme"; +import { deletePromocode } from "@root/api/promocode/swr"; -const columns: GridColDef[] = [ - { field: "id", headerName: "ID", width: 30, sortable: false }, - { - field: "name", - headerName: "Название промокода", - width: 200, - sortable: false, - }, - { field: "endless", headerName: "Бесконечный", width: 120, sortable: false }, - { field: "from", headerName: "От", width: 120, sortable: false }, - { field: "dueTo", headerName: "До", width: 120, sortable: false }, - { - field: "privileges", - headerName: "Привилегии", - width: 210, - sortable: false, - }, +const columns: GridColDef[] = [ + { + 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: 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 ( + deletePromocode(row.id)}> + + + ); + }, + }, ]; -export const PromocodesList = () => { - const { promocodes } = usePromocodeStore(); - - return ( - - - console.log("datagrid select")} - /> - - - ); +type Props = { + promocodes: Promocode[]; +}; + +export const PromocodesList = ({ promocodes }: Props) => { + + return ( + + + console.log("datagrid select")} + /> + + + ); }; diff --git a/src/pages/dashboard/Content/PromocodeManagement/index.tsx b/src/pages/dashboard/Content/PromocodeManagement/index.tsx index 835cbfc..ecf17e2 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/index.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/index.tsx @@ -1,30 +1,39 @@ -import { Typography } from "@mui/material"; -import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { CircularProgress, Typography } from "@mui/material"; 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 { PromocodesList } from "./PromocodesList"; +import { usePromocodes } from "@root/api/promocode/swr"; import theme from "@root/theme"; -export const PromocodeManagement = () => ( - - - Создание промокода - - - - -); +export const PromocodeManagement = () => { + const { data, error, isLoading } = usePromocodes(); + + if (error) return Ошибка загрузки промокодов; + if (isLoading) return ; + if (!data) return null; + + return ( + + + Создание промокода + + + + + ); +}; diff --git a/src/stores/promocodes.ts b/src/stores/promocodes.ts deleted file mode 100644 index 2b15680..0000000 --- a/src/stores/promocodes.ts +++ /dev/null @@ -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()( - 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" - } - ) -); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b0afa7b..2d09764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4327,6 +4327,11 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.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: version "7.0.4" resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" @@ -11801,6 +11806,14 @@ svgo@^2.7.0: picocolors "^1.0.0" 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: version "3.2.4" 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" 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" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== From 5021f260b08633cf4e1abc34bb8af4e2f7663d36 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Sun, 3 Mar 2024 13:49:24 +0300 Subject: [PATCH 07/13] fix type error --- src/pages/dashboard/Content/Tariffs/EditModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 89780811982cb2b4a8c37d098a2f58a6c3630f60 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Sun, 3 Mar 2024 16:30:57 +0300 Subject: [PATCH 08/13] add promocode datagrid pagination --- src/api/promocode/requests.ts | 10 +- src/api/promocode/swr.ts | 116 ++++++++++-------- src/model/promocodes.ts | 2 +- .../CreatePromocodeForm.tsx | 8 +- .../PromocodeManagement/PromocodesList.tsx | 93 -------------- .../Content/PromocodeManagement/index.tsx | 61 +++++++-- .../usePromocodeGridColDef.tsx | 59 +++++++++ src/utils/style/keyframes.ts | 11 ++ 8 files changed, 194 insertions(+), 166 deletions(-) delete mode 100644 src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx create mode 100644 src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx create mode 100644 src/utils/style/keyframes.ts diff --git a/src/api/promocode/requests.ts b/src/api/promocode/requests.ts index a287bcb..f28ee26 100644 --- a/src/api/promocode/requests.ts +++ b/src/api/promocode/requests.ts @@ -5,9 +5,7 @@ import { parseAxiosError } from "@root/utils/parse-error"; const baseUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode"; -const getPromocodeList = async ( - body: GetPromocodeListBody -): Promise => { +const getPromocodeList = async (body: GetPromocodeListBody) => { try { const promocodeListResponse = await makeRequest< GetPromocodeListBody, @@ -19,16 +17,14 @@ const getPromocodeList = async ( useToken: false, }); - return promocodeListResponse.items; + return promocodeListResponse; } catch (nativeError) { const [error] = parseAxiosError(nativeError); throw new Error(`Ошибка при получении списка промокодов. ${error}`); } }; -const createPromocode = async ( - body: CreatePromocodeBody -): Promise => { +const createPromocode = async (body: CreatePromocodeBody) => { try { const createPromocodeResponse = await makeRequest< CreatePromocodeBody, diff --git a/src/api/promocode/swr.ts b/src/api/promocode/swr.ts index fb0e9b6..064d41a 100644 --- a/src/api/promocode/swr.ts +++ b/src/api/promocode/swr.ts @@ -1,69 +1,81 @@ -import { CreatePromocodeBody, Promocode } from "@root/model/promocodes"; +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() { - return useSwr( - "promocodes", - () => promocodeApi.getPromocodeList({ - limit: 100, - filter: { - active: true, - }, - page: 0, - }), + +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, } ); -} -export async function createPromocode(body: CreatePromocodeBody) { - try { - await mutate( - "promocodes", - promocodeApi.createPromocode(body), - { - populateCache(result, currentData) { - if (!currentData) return; + 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]); - return [...currentData, result]; - }, - revalidate: false, - } - ); - } catch (error) { - console.log("Error creating promocode", error); - if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" }); - } -} + const deletePromocode = useCallback(async function (id: string) { + try { + await mutate( + ["promocodes", page, pageSize], + promocodeApi.deletePromocode(id), + { + optimisticData(currentData, displayedData) { + if (!displayedData) return; -export async function deletePromocode(id: string) { - try { - await mutate( - "promocodes", - 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 displayedData.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 currentData.filter((item) => item.id !== id); - }, - } - ); - } catch (error) { - console.log("Error deleting promocode", error); - if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" }); - } -} + return { + ...swrResponse, + createPromocode, + deletePromocode, + promocodesCount: promocodesCountRef.current, + }; +} diff --git a/src/model/promocodes.ts b/src/model/promocodes.ts index 4900bbf..1a6ff14 100644 --- a/src/model/promocodes.ts +++ b/src/model/promocodes.ts @@ -36,6 +36,6 @@ export type Promocode = CreatePromocodeBody & { }; export type PromocodeList = { - count: 0; + count: number; items: Promocode[]; }; diff --git a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx index 8d47edc..9ce1870 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx @@ -20,7 +20,7 @@ import { SERVICE_LIST } from "@root/model/privilege"; import theme from "@root/theme"; import type { TextFieldProps } from "@mui/material"; -import { createPromocode } from "@root/api/promocode/swr"; +import { CreatePromocodeBody } from "@root/model/promocodes"; import type { ChangeEvent } from "react"; type BonusType = "discount" | "privilege"; @@ -53,7 +53,11 @@ const initialValues: FormValues = { threshold: 0, }; -export const CreatePromocodeForm = () => { +type Props = { + createPromocode: (body: CreatePromocodeBody) => Promise; +}; + +export const CreatePromocodeForm = ({ createPromocode }: Props) => { const [bonusType, setBonusType] = useState("discount"); const { privileges } = usePrivilegeStore(); diff --git a/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx b/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx deleted file mode 100644 index 2913d2e..0000000 --- a/src/pages/dashboard/Content/PromocodeManagement/PromocodesList.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Box, IconButton } from "@mui/material"; -import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid"; -import { Promocode } from "@root/model/promocodes"; -import DeleteIcon from '@mui/icons-material/Delete'; - -import theme from "@root/theme"; -import { deletePromocode } from "@root/api/promocode/swr"; - -const columns: GridColDef[] = [ - { - 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: 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 ( - deletePromocode(row.id)}> - - - ); - }, - }, -]; - -type Props = { - promocodes: Promocode[]; -}; - -export const PromocodesList = ({ promocodes }: Props) => { - - return ( - - - console.log("datagrid select")} - /> - - - ); -}; diff --git a/src/pages/dashboard/Content/PromocodeManagement/index.tsx b/src/pages/dashboard/Content/PromocodeManagement/index.tsx index ecf17e2..bdd7be5 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/index.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/index.tsx @@ -1,19 +1,22 @@ -import { CircularProgress, Typography } from "@mui/material"; +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 { CreatePromocodeForm } from "./CreatePromocodeForm"; -import { PromocodesList } from "./PromocodesList"; - import { usePromocodes } from "@root/api/promocode/swr"; -import theme from "@root/theme"; +import { fadeIn } from "@root/utils/style/keyframes"; +import { useState } from "react"; +import { CreatePromocodeForm } from "./CreatePromocodeForm"; +import { usePromocodeGridColDef } from "./usePromocodeGridColDef"; + export const PromocodeManagement = () => { - const { data, error, isLoading } = usePromocodes(); + 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 Ошибка загрузки промокодов; - if (isLoading) return ; - if (!data) return null; return ( @@ -32,8 +35,44 @@ export const PromocodeManagement = () => { > Создание промокода - - + + + + ); }; 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/utils/style/keyframes.ts b/src/utils/style/keyframes.ts new file mode 100644 index 0000000..a0900a2 --- /dev/null +++ b/src/utils/style/keyframes.ts @@ -0,0 +1,11 @@ +import { keyframes } from "@emotion/react"; + + +export const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; From 13ec627b310f31eb6a90db93db85cf69d514daae Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Thu, 7 Mar 2024 15:44:44 +0300 Subject: [PATCH 09/13] feat: users pagination --- src/api/user.ts | 70 ------------ src/api/user/requests.ts | 89 +++++++++++++++ src/api/user/swr.ts | 104 ++++++++++++++++++ .../dashboard/Content/ServiceUsersDG.tsx | 17 +++ src/pages/dashboard/Content/Users.tsx | 98 +++++++++++++---- src/pages/dashboard/ModalUser/UserTab.tsx | 4 +- 6 files changed, 287 insertions(+), 95 deletions(-) delete mode 100644 src/api/user.ts create mode 100644 src/api/user/requests.ts create mode 100644 src/api/user/swr.ts 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..859165f --- /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 RegisteredUsersResponse = { + 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<[RegisteredUsersResponse | 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<[RegisteredUsersResponse | 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<[RegisteredUsersResponse | 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/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/Users.tsx b/src/pages/dashboard/Content/Users.tsx index 4eece62..7c9f20c 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); @@ -210,8 +229,8 @@ const Users: React.FC = () => { - {data.length ? ( - data.map(function (item, index) { + {mockData.length ? ( + mockData.map(function (item, index) { return ( { + setPage((pages) => ({ ...pages, adminPage })) + } + pagesCount={adminPages} + pageSize={pageSize.adminPageSize} + handleSelectionChange={setSelectedTariffs} + onPageSizeChange={(adminPageSize) => + setPageSize((pageSize) => ({ ...pageSize, adminPageSize })) + } + /> + } childrenManager={ + setPage((pages) => ({ ...pages, managerPage })) + } + pagesCount={managerPages} + pageSize={pageSize.managerPageSize} handleSelectionChange={setSelectedTariffs} + onPageSizeChange={(managerPageSize) => + setPageSize((pageSize) => ({ ...pageSize, managerPageSize })) + } /> } childrenUser={ + setPage((pages) => ({ ...pages, userPage })) + } + pagesCount={userPagesCount} + pageSize={pageSize.userPageSize} handleSelectionChange={setSelectedTariffs} + onPageSizeChange={(userPageSize) => + setPageSize((pageSize) => ({ ...pageSize, userPageSize })) + } /> } /> diff --git a/src/pages/dashboard/ModalUser/UserTab.tsx b/src/pages/dashboard/ModalUser/UserTab.tsx index ad03896..4fc1b22 100644 --- a/src/pages/dashboard/ModalUser/UserTab.tsx +++ b/src/pages/dashboard/ModalUser/UserTab.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { Box, Typography, useTheme, useMediaQuery } from "@mui/material"; -import { getUserInfo } from "@root/api/user"; +import { userApi } from "@root/api/user/requests"; import { getAccountInfo } from "@root/api/account"; import type { UserType } from "@root/api/roles"; @@ -19,7 +19,7 @@ export const UserTab = ({ userId }: UserTabProps) => { useEffect(() => { if (userId) { - getUserInfo(userId).then(([userInfo]) => setUser(userInfo)); + userApi.getUserInfo(userId).then(([userInfo]) => setUser(userInfo)); getAccountInfo(userId).then(([accountsInfo]) => setAccount(accountsInfo)); } }, []); From 9d8491dd6d203df40c3843a2fd2847ccb342b837 Mon Sep 17 00:00:00 2001 From: Tamara Date: Sun, 10 Mar 2024 03:10:40 +0300 Subject: [PATCH 10/13] =?UTF-8?q?=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/kitUI/Cart/Cart.tsx | 6 ++++-- src/pages/Authorization/signin.tsx | 5 +++-- src/pages/Setting/FormCreateRoles.tsx | 21 ++++++++++--------- src/pages/Setting/SettingRoles.tsx | 20 +++++++++++++++--- .../DiscountManagement/ControlPanel.tsx | 12 +++++++++-- .../Content/Tariffs/CreateTariff.tsx | 1 + src/pages/dashboard/Content/Users.tsx | 2 +- src/pages/dashboard/Menu/index.tsx | 4 ++-- 8 files changed, 49 insertions(+), 22 deletions(-) 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/FormCreateRoles.tsx b/src/pages/Setting/FormCreateRoles.tsx index 2621fec..b70cbe8 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(400)); const handleChange = (event: SelectChangeEvent) => { const { target: { value }, @@ -39,7 +40,7 @@ export default function CreateForm() { fullWidth sx={{ alignItems: "center", - width: "400px", + width: mobile ? "320px" : "400px", "& .MuiInputBase-root": { backgroundColor: "#F2F3F7", height: "48px", diff --git a/src/pages/Setting/SettingRoles.tsx b/src/pages/Setting/SettingRoles.tsx index 5937b40..ef7f5d0 100644 --- a/src/pages/Setting/SettingRoles.tsx +++ b/src/pages/Setting/SettingRoles.tsx @@ -1,4 +1,14 @@ -import { AccordionDetails, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material"; +import { + AccordionDetails, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, + useMediaQuery, + useTheme +} from "@mui/material"; import { CustomWrapper } from "@root/kitUI/CustomWrapper"; @@ -9,15 +19,19 @@ import { PrivilegesWrapper } from "./PrivilegiesWrapper"; import theme from "../../theme"; export const SettingRoles = (): JSX.Element => { + const theme = useTheme(); + const mobile = useMediaQuery(theme.breakpoints.down(400)); return ( - + { let done = 0; let fatal = 0; @@ -46,7 +48,13 @@ export default function DiscountDataGrid({ selectedRows }: Props) { }; return ( - + 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/Users.tsx b/src/pages/dashboard/Content/Users.tsx index 4eece62..1426cf5 100644 --- a/src/pages/dashboard/Content/Users.tsx +++ b/src/pages/dashboard/Content/Users.tsx @@ -159,7 +159,7 @@ const Users: React.FC = () => { {accordionText} - +
{ }; const Menu: React.FC = () => { - const tablet = useMediaQuery("(max-width:600px)"); + const tablet = useMediaQuery("(max-width:900px)"); - const mobile = useMediaQuery("(max-width:340px)"); + const mobile = useMediaQuery("(max-width:600px)"); const theme = useTheme(); const [open, setOpen] = React.useState(tablet ? false : true); From 71102972cdce56ed3d075a84abe02936ea059cc5 Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Mon, 11 Mar 2024 09:55:29 +0300 Subject: [PATCH 11/13] feat: user history --- package.json | 1 + src/api/history/requests.ts | 50 ++++++++++++ src/api/history/swr.ts | 43 ++++++++++ src/api/user/requests.ts | 14 ++-- src/pages/dashboard/ModalUser/PurchaseTab.tsx | 81 ++++++++++++++++++- src/pages/dashboard/ModalUser/index.tsx | 2 +- yarn.lock | 5 ++ 7 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 src/api/history/requests.ts create mode 100644 src/api/history/swr.ts diff --git a/package.json b/package.json index 786b98d..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", 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/user/requests.ts b/src/api/user/requests.ts index 859165f..4ae9590 100644 --- a/src/api/user/requests.ts +++ b/src/api/user/requests.ts @@ -4,7 +4,7 @@ import { parseAxiosError } from "@root/utils/parse-error"; import type { UserType } from "@root/api/roles"; -export type RegisteredUsersResponse = { +export type UsersListResponse = { totalPages: number; users: UserType[]; }; @@ -30,9 +30,9 @@ const getUserInfo = async (id: string): Promise<[UserType | null, string?]> => { const getUserList = async ( page = 1, limit = 10 -): Promise<[RegisteredUsersResponse | null, string?]> => { +): Promise<[UsersListResponse | null, string?]> => { try { - const userResponse = await makeRequest({ + const userResponse = await makeRequest({ method: "get", url: baseUrl + `/?page=${page}&limit=${limit}`, }); @@ -48,9 +48,9 @@ const getUserList = async ( const getManagerList = async ( page = 1, limit = 10 -): Promise<[RegisteredUsersResponse | null, string?]> => { +): Promise<[UsersListResponse | null, string?]> => { try { - const managerResponse = await makeRequest({ + const managerResponse = await makeRequest({ method: "get", url: baseUrl + `/?page=${page}&limit=${limit}`, }); @@ -66,9 +66,9 @@ const getManagerList = async ( const getAdminList = async ( page = 1, limit = 10 -): Promise<[RegisteredUsersResponse | null, string?]> => { +): Promise<[UsersListResponse | null, string?]> => { try { - const adminResponse = await makeRequest({ + const adminResponse = await makeRequest({ method: "get", url: baseUrl + `/?page=${page}&limit=${limit}`, }); diff --git a/src/pages/dashboard/ModalUser/PurchaseTab.tsx b/src/pages/dashboard/ModalUser/PurchaseTab.tsx index dbc87cf..361b3f7 100644 --- a/src/pages/dashboard/ModalUser/PurchaseTab.tsx +++ b/src/pages/dashboard/ModalUser/PurchaseTab.tsx @@ -1,7 +1,9 @@ import { useEffect, useRef, useState } from "react"; import { Box, useTheme, useMediaQuery } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; +import { format } from "date-fns"; +import { useHistory } from "@root/api/history/swr"; import { scrollBlock } from "@root/utils/scrollBlock"; import forwardIcon from "@root/assets/icons/forward.svg"; @@ -9,6 +11,10 @@ import forwardIcon from "@root/assets/icons/forward.svg"; import type { ChangeEvent } from "react"; import type { GridColDef } from "@mui/x-data-grid"; +type PurchaseTabProps = { + userId: string; +}; + const COLUMNS: GridColDef[] = [ { field: "date", @@ -68,12 +74,22 @@ const ROWS = [ }, ]; -export const PurchaseTab = () => { +export const PurchaseTab = ({ userId }: PurchaseTabProps) => { const [canScrollToRight, setCanScrollToRight] = useState(true); const [canScrollToLeft, setCanScrollToLeft] = useState(false); const theme = useTheme(); const smallScreen = useMediaQuery(theme.breakpoints.down(830)); const gridContainer = useRef(null); + const { data: historyData } = useHistory(userId); + + const rows = + historyData?.[0].records.map((history) => ({ + id: history.id, + date: format(history.updatedAt, "dd.MM.yyyy"), + time: format(history.updatedAt, "HH:mm"), + product: "", + amount: "", + })) ?? []; useEffect(() => { const handleScroll = (nativeEvent: unknown) => { @@ -145,10 +161,8 @@ export const PurchaseTab = () => { }} > { ); }; + +const a = { + id: "65e4f1b157004756bc5bb15c", + userId: "64eb6ce57047f28fdabf69ec", + comment: "Успешная оплата корзины", + key: "payCart", + rawDetails: [ + [ + { Key: "id", Value: "65e4f1881747c1eea8007d3b" }, + { + Key: "name", + Value: + "Количество Заявок, Скрытие шильдика в опроснике, 2024-03-03T21:54:16.434Z", + }, + { Key: "price", Value: 0 }, + { Key: "iscustom", Value: true }, + { + Key: "privileges", + Value: [ + [ + { Key: "id", Value: "" }, + { Key: "name", Value: "Количество Заявок" }, + { Key: "privilegeid", Value: "quizCnt" }, + { Key: "servicekey", Value: "squiz" }, + { + Key: "description", + Value: "Количество полных прохождений опросов", + }, + { Key: "amount", Value: 100 }, + { Key: "type", Value: "count" }, + { Key: "value", Value: "заявка" }, + { Key: "price", Value: 2000 }, + ], + [ + { Key: "id", Value: "" }, + { Key: "name", Value: "Скрытие шильдика в опроснике" }, + { Key: "privilegeid", Value: "squizHideBadge" }, + { Key: "servicekey", Value: "squiz" }, + { + Key: "description", + Value: "Количество дней скрытия шильдика в опроснике", + }, + { Key: "amount", Value: 30 }, + { Key: "type", Value: "day" }, + { Key: "value", Value: "день" }, + { Key: "price", Value: 0 }, + ], + ], + }, + { Key: "deleted", Value: false }, + { Key: "createdat", Value: "2024-03-03T21:54:16.825Z" }, + { Key: "updatedat", Value: "2024-03-03T21:54:16.825Z" }, + { Key: "deletedat", Value: null }, + ], + ], + isDeleted: false, + createdAt: "2024-03-03T21:54:57.433Z", + updatedAt: "2024-03-03T21:54:57.433Z", +}; diff --git a/src/pages/dashboard/ModalUser/index.tsx b/src/pages/dashboard/ModalUser/index.tsx index b0b4171..63014df 100644 --- a/src/pages/dashboard/ModalUser/index.tsx +++ b/src/pages/dashboard/ModalUser/index.tsx @@ -191,7 +191,7 @@ const ModalUser = ({ open, onClose, userId }: ModalUserProps) => { }} > {value === 0 && } - {value === 1 && } + {value === 1 && } {value === 2 && } {value === 3 && } diff --git a/yarn.lock b/yarn.lock index 2d09764..e38adc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5028,6 +5028,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-fns@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.3.1.tgz#7581daca0892d139736697717a168afbb908cfed" + integrity sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw== + dayjs@^1.10.4: version "1.11.9" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" From 4ad467ae7bd0830039fd89b64635d8789efe291d Mon Sep 17 00:00:00 2001 From: Nastya Date: Mon, 11 Mar 2024 22:48:38 +0300 Subject: [PATCH 12/13] =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B5=D0=B4=D0=B8=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.tsx | 2 + .../dashboard/Content/Support/Chat/Chat.tsx | 154 +++++++++++++++++- .../Content/Support/Chat/ChatDocument.tsx | 61 +++++++ .../Content/Support/Chat/ChatImage.tsx | 67 ++++++++ .../Content/Support/Chat/ChatMessage.tsx | 52 ++++++ .../Content/Support/Chat/ChatVideo.tsx | 68 ++++++++ .../Content/Support/Chat/fileUpload.ts | 9 + .../Content/Support/ChatImageNewWindow.tsx | 20 +++ 8 files changed, 427 insertions(+), 6 deletions(-) create mode 100644 src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx create mode 100644 src/pages/dashboard/Content/Support/Chat/ChatImage.tsx create mode 100644 src/pages/dashboard/Content/Support/Chat/ChatMessage.tsx create mode 100644 src/pages/dashboard/Content/Support/Chat/ChatVideo.tsx create mode 100644 src/pages/dashboard/Content/Support/Chat/fileUpload.ts create mode 100644 src/pages/dashboard/Content/Support/ChatImageNewWindow.tsx diff --git a/src/index.tsx b/src/index.tsx index 3f4aa62..0223de9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ import DiscountManagement from "@root/pages/dashboard/Content/DiscountManagement 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"; @@ -111,6 +112,7 @@ root.render( /> ))} + } /> } /> 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 ( + <> + + + ); +} From 5007935f8268f743b6086645b09f141095fbe36f Mon Sep 17 00:00:00 2001 From: Tamara Date: Tue, 12 Mar 2024 00:38:59 +0300 Subject: [PATCH 13/13] =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Setting/CardPrivilegie.tsx | 13 +++++++++---- src/pages/Setting/FormCreateRoles.tsx | 9 +++++---- src/pages/Setting/FormDeleteRoles.tsx | 3 ++- src/pages/Setting/SettingRoles.tsx | 18 ++++++++++++------ 4 files changed, 28 insertions(+), 15 deletions(-) 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 b70cbe8..900a556 100644 --- a/src/pages/Setting/FormCreateRoles.tsx +++ b/src/pages/Setting/FormCreateRoles.tsx @@ -25,7 +25,7 @@ const MenuProps = { export default function CreateForm() { const [personName, setPersonName] = useState([]); const theme = useTheme(); - const mobile = useMediaQuery(theme.breakpoints.down(400)); + const mobile = useMediaQuery(theme.breakpoints.down(600)); const handleChange = (event: SelectChangeEvent) => { const { target: { value }, @@ -40,7 +40,8 @@ export default function CreateForm() { fullWidth sx={{ alignItems: "center", - width: mobile ? "320px" : "400px", + maxWidth: "400px", + width: "100%", "& .MuiInputBase-root": { backgroundColor: "#F2F3F7", height: "48px", @@ -55,8 +56,8 @@ export default function CreateForm() { }, }} /> - - + +
{ alignItems: "center", borderTop: "2px solid", borderColor: theme.palette.grayLight.main, - height: "100px", - cursor: "pointer", + height: mobile ? undefined : "100px", + cursor: "pointer", + flexDirection: mobile ? "column" : "row", + gap: "5px" }} > @@ -124,7 +129,8 @@ export const SettingRoles = (): JSX.Element => { } /> - + ); };