Merge branch 'dev' into 'main'

Users: replacing table with dataGrid &&

See merge request frontend/admin!12
This commit is contained in:
Mikhail 2023-05-16 16:57:44 +00:00
commit 28fdd70cab
15 changed files with 716 additions and 597 deletions

@ -1,4 +1,3 @@
import makeRequest from "@root/kitUI/makeRequest";
import { import {
GetMessagesRequest, GetMessagesRequest,
GetMessagesResponse, GetMessagesResponse,
@ -9,10 +8,11 @@ import {
import { authStore } from "@root/stores/auth"; import { authStore } from "@root/stores/auth";
import ReconnectingEventSource from "reconnecting-eventsource"; import ReconnectingEventSource from "reconnecting-eventsource";
// const { makeRequest } = authStore();
const supportApiUrl = "https://admin.pena.digital/heruvym"; const supportApiUrl = "https://admin.pena.digital/heruvym";
const makeRequest = authStore.getState().makeRequest;
export function subscribeToAllTickets({ export function subscribeToAllTickets({
onMessage, onMessage,
onError, onError,
@ -58,15 +58,12 @@ export async function getTickets({
body: GetTicketsRequest; body: GetTicketsRequest;
signal: AbortSignal; signal: AbortSignal;
}): Promise<GetTicketsResponse> { }): Promise<GetTicketsResponse> {
return makeRequest({ return makeRequest<GetTicketsRequest, GetTicketsResponse>({
url: `${supportApiUrl}/getTickets`, url: `${supportApiUrl}/getTickets`,
method: "POST", method: "POST",
useToken: true, useToken: true,
body, body,
signal, signal,
}).then((response) => {
const result = (response as any).data as GetTicketsResponse;
return result;
}); });
} }
@ -77,19 +74,16 @@ export async function getTicketMessages({
body: GetMessagesRequest; body: GetMessagesRequest;
signal: AbortSignal; signal: AbortSignal;
}): Promise<GetMessagesResponse> { }): Promise<GetMessagesResponse> {
return makeRequest({ return makeRequest<GetMessagesRequest, GetMessagesResponse>({
url: `${supportApiUrl}/getMessages`, url: `${supportApiUrl}/getMessages`,
method: "POST", method: "POST",
useToken: true, useToken: true,
body, body,
signal, signal,
}).then((response) => {
const result = (response as any).data as GetMessagesResponse;
return result;
}); });
} }
export async function sendTicketMessage({ body }: { body: SendTicketMessageRequest }) { export async function sendTicketMessage({ body }: { body: SendTicketMessageRequest; }) {
return makeRequest({ return makeRequest({
url: `${supportApiUrl}/send`, url: `${supportApiUrl}/send`,
method: "POST", method: "POST",

@ -0,0 +1,52 @@
import { useEffect, useState } from "react";
import axios from "axios";
export type Privilege = {
createdAt: string;
description: string;
isDeleted: boolean;
name: string;
price: string;
privilegeId: string;
serviceKey: string;
type: string;
updatedAt: string;
value: string;
_id: string;
};
type UsePrivilegies = {
privilegies: Record<"Шаблонизатор", Privilege[]> | undefined;
isError: boolean;
isLoading: boolean;
errorMessage: string;
};
export const usePrivilegies = (): UsePrivilegies => {
const [privilegies, setPrivilegies] = useState<Record<string, Privilege[]>>();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
useEffect(() => {
const getPrivilegies = async () => {
const { data } = await axios<Record<string, Privilege[]>>({
method: "get",
url: "https://admin.pena.digital/strator/privilege/service",
});
return data;
};
setIsLoading(true);
getPrivilegies()
.then(setPrivilegies)
.catch(() => {
setIsError(true);
setErrorMessage("Ошибка при получении привилегий");
})
.finally(() => setIsLoading(false));
}, []);
return { privilegies, isError, isLoading, errorMessage };
};

19
src/kitUI/Article.tsx Normal file

@ -0,0 +1,19 @@
import React from "react";
import { Box, Typography } from "@mui/material";
type ArticleProps = {
header: JSX.Element;
body: JSX.Element;
isBoby?: boolean;
};
export const Article = ({ header, body, isBoby = false }: ArticleProps) => {
return (
<Box component="section">
<Box>
<Typography variant="h1">{header}</Typography>
</Box>
{isBoby ? <Box>{body}</Box> : <React.Fragment />}
</Box>
);
};

@ -1,58 +0,0 @@
import axios from "axios";
interface MakeRequest {
method?: string;
url: string;
body?: unknown;
useToken?: boolean;
contentType?: boolean;
signal?: AbortSignal;
}
export default (props: MakeRequest) => {
return new Promise(async (resolve, reject) => {
await makeRequest(props)
.then((r) => resolve(r))
.catch((r) => reject(r));
});
};
function makeRequest({ method = "post", url, body, useToken = true, signal, contentType = false }: MakeRequest) {
//В случае 401 рефреш должен попробовать вызваться 1 раз
let counterRefresh = true;
let headers: any = {};
if (useToken) headers["Authorization"] = localStorage.getItem("AT");
if (contentType) headers["Content-Type"] = "application/json";
return axios({
url: url,
method: method,
headers: headers,
data: body,
signal,
})
.then((response) => {
if (response.data && response.data.accessToken) {
localStorage.setItem("AT", response.data.accessToken);
}
return response;
})
.catch((error) => {
if (error.response.status == 401 && counterRefresh) {
refresh().then((response) => {
if (response.data && response.data.accessToken) localStorage.setItem("AT", response.data.accessToken);
counterRefresh = false;
});
} else {
throw error;
}
throw error;
});
}
function refresh() {
return axios("https://admin.pena.digital/auth/refresh", {
headers: {
Authorization: localStorage.getItem("AT"),
"Content-Type": "application/json",
},
});
}

@ -1,28 +1,21 @@
export const SERVICE_LIST = [ export const SERVICE_LIST = [
{ {
serviceKey: "templategen", serviceKey: "templategen",
displayName: "Шаблонизатор документов" displayName: "Шаблонизатор документов",
}, },
{ {
serviceKey: "squiz", serviceKey: "squiz",
displayName: "Опросник" displayName: "Опросник",
}, },
{ {
serviceKey: "dwarfener", serviceKey: "dwarfener",
displayName: "Сокращатель ссылок" displayName: "Сокращатель ссылок",
} },
] as const; ] as const;
export type ServiceType = typeof SERVICE_LIST[number]["serviceKey"]; export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
export type PrivilegeType = export type PrivilegeType = "unlim" | "gencount" | "activequiz" | "abcount" | "extended";
| "unlim"
| "gencount"
| "activequiz"
| "abcount"
| "extended";
export interface Privilege { export interface Privilege {
serviceKey: ServiceType; serviceKey: ServiceType;

@ -0,0 +1,117 @@
import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined";
import { Box, IconButton, TextField, Tooltip, Typography } from "@mui/material";
import axios from "axios";
import { useState } from "react";
interface CardPrivilegie {
name: string;
type: string;
price: string;
description: string;
}
export const СardPrivilegie = ({ name, type, price, description }: CardPrivilegie) => {
const [inputOpen, setInputOpen] = useState<boolean>(false);
const [inputValue, setInputValue] = useState<string>("");
const PutPrivilegies = () => {
axios({
method: "put",
url: "https://admin.pena.digital/strator/privilege/service",
data: {
price: inputValue,
},
});
};
const requestOnclickEnter = (event: any) => {
if (event.key === "Enter" && inputValue !== "") {
PutPrivilegies();
setInputValue("");
setInputOpen(false);
}
};
const onCloseInput = (event: any) => {
if (event.key === "Escape") {
setInputOpen(false);
}
};
return (
<Box
key={type}
sx={{
px: "20px",
py: "25px",
backgroundColor: "#F1F2F6",
display: "flex",
alignItems: "center",
}}
>
<Box sx={{ display: "flex" }}>
<Box sx={{ width: "200px", borderRight: "1px solid black" }}>
<Typography
variant="h6"
sx={{
color: "#fe9903",
whiteSpace: "nowrap",
}}
>
{name}
</Typography>
<Tooltip placement="top" title={description}>
<IconButton>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.25 9.25H10V14.5H10.75"
stroke="#7E2AEA"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M9.8125 7C10.4338 7 10.9375 6.49632 10.9375 5.875C10.9375 5.25368 10.4338 4.75 9.8125 4.75C9.19118 4.75 8.6875 5.25368 8.6875 5.875C8.6875 6.49632 9.19118 7 9.8125 7Z"
fill="#7E2AEA"
/>
</svg>
</IconButton>
</Tooltip>
<IconButton onClick={() => setInputOpen(!inputOpen)}>
<ModeEditOutlineOutlinedIcon />
</IconButton>
</Box>
</Box>
<Box sx={{ width: "600px", display: "flex", justifyContent: "space-around" }}>
{inputOpen ? (
<TextField
onKeyDown={onCloseInput}
onKeyPress={requestOnclickEnter}
placeholder="введите число"
fullWidth
onChange={(event) => setInputValue(event.target.value)}
sx={{
alignItems: "center",
width: "400px",
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
height: "48px",
},
}}
inputProps={{
sx: {
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
) : (
<Typography sx={{ color: "black", mr: "5px" }}>price: {price}</Typography>
)}
<Typography sx={{ color: "black" }}>{type}</Typography>
</Box>
</Box>
);
};

@ -0,0 +1,139 @@
import { useState } from "react";
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
import { СardPrivilegie } from "./CardPrivilegie";
import { usePrivilegies } from "@root/hooks/privilege.hook";
interface CustomWrapperProps {
text: string;
sx?: SxProps<Theme>;
result?: boolean;
}
export const PrivilegiesWrapper = ({ text, sx, result }: CustomWrapperProps) => {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const { privilegies, isError, isLoading, errorMessage } = usePrivilegies();
return (
<Box
sx={{
width: "100%",
overflow: "hidden",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.067
4749)`,
...sx,
}}
>
<Box
sx={{
border: "1px solid white",
"&:first-of-type": {
borderTopLeftRadius: "12px ",
borderTopRightRadius: "12px",
},
"&:last-of-type": {
borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px",
},
"&:not(:last-of-type)": {
borderBottom: `1px solid gray`,
},
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "88px",
px: "20px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Typography
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
fontSize: "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: "#FFFFFF",
px: 0,
}}
>
{text}
</Typography>
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
}}
>
{result ? (
<>
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<path
d="M7.5 19.5625L15 12.0625L22.5 19.5625"
stroke="#7E2AEA"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<Box
sx={{
borderLeft: upSm ? "1px solid #9A9AAF" : "none",
pl: upSm ? "2px" : 0,
height: "50%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
/>
</>
) : (
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<path
d="M7.5 19.5625L15 12.0625L22.5 19.5625"
stroke="#fe9903"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
)}
</Box>
</Box>
{isExpanded &&
(isError ? (
<Typography>errorMessage</Typography>
) : (
privilegies?.Шаблонизатор.map(({ name, type, price, description }) => (
<СardPrivilegie key={type} name={name} type={type} price={price} description={description} />
))
))}
</Box>
</Box>
);
};

@ -2,10 +2,11 @@ import { AccordionDetails, Table, TableBody, TableCell, TableHead, TableRow, Typ
import FormDeleteRoles from "./FormDeleteRoles"; import FormDeleteRoles from "./FormDeleteRoles";
import FormCreateRoles from "./FormCreateRoles"; import FormCreateRoles from "./FormCreateRoles";
import { PrivilegiesWrapper } from "./PrivilegiesWrapper";
import theme from "../../theme"; import theme from "../../theme";
export const SettingRoles = () => { export const SettingRoles = (): JSX.Element => {
return ( return (
<AccordionDetails> <AccordionDetails>
<Table <Table
@ -99,6 +100,7 @@ export const SettingRoles = () => {
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
<PrivilegiesWrapper text="Привелегии" sx={{ mt: "50px" }} />
</AccordionDetails> </AccordionDetails>
); );
}; };

@ -0,0 +1,45 @@
import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
import { Skeleton } from "@mui/material";
import DataGrid from "@kitUI/datagrid";
import type { UsersType } from "@root/api/roles";
const columns: GridColDef[] = [
{ field: "login", headerName: "Логин", width: 100 },
{ field: "email", headerName: "E-mail", width: 200 },
{ field: "phoneNumber", headerName: "Номер телефона", width: 200 },
{ field: "isDeleted", headerName: "Удалено", width: 100 },
{ field: "createdAt", headerName: "Дата создания", width: 200 },
];
interface Props {
handleSelectionChange: (selectionModel: GridSelectionModel) => void;
users: UsersType | undefined;
}
export default function ServiceUsersDG({ handleSelectionChange, users }: Props) {
if (!users) {
return <Skeleton>Loading...</Skeleton>;
}
const gridData = users.map((user) => ({
login: user.login,
email: user.email,
phoneNumber: user.phoneNumber,
isDeleted: `${user.isDeleted ? "true" : "false"}`,
createdAt: user.createdAt,
}));
return (
<DataGrid
sx={{ maxWidth: "90%", mt: "30px" }}
getRowId={(users: any) => users.login}
checkboxSelection={true}
rows={gridData}
columns={columns}
components={{ Toolbar: GridToolbar }}
onSelectionModelChange={handleSelectionChange}
/>
);
}

@ -10,11 +10,11 @@ import { getTicketMessages, sendTicketMessage, subscribeToTicketMessages } from
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets"; import { useTicketStore } from "@root/stores/tickets";
import { throttle } from "@root/utils/throttle"; import { throttle } from "@root/utils/throttle";
import { authStore } from "@root/stores/auth"; import { authStore } from "@root/stores/auth";
export default function Chat() { export default function Chat() {
const { token } = authStore(); const token = authStore(state => state.token);
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const tickets = useTicketStore(state => state.tickets); const tickets = useTicketStore(state => state.tickets);
@ -30,12 +30,7 @@ export default function Chat() {
const ticket = tickets.find(ticket => ticket.id === ticketId); const ticket = tickets.find(ticket => ticket.id === ticketId);
useEffect(function scrollOnNewMessage() { useEffect(function fetchTicketMessages() {
scrollToBottom();
}, [messages]);
useEffect(
function fetchTicketMessages() {
if (!ticketId) return; if (!ticketId) return;
const getTicketsBody: GetMessagesRequest = { const getTicketsBody: GetMessagesRequest = {
@ -52,6 +47,7 @@ export default function Chat() {
}).then(result => { }).then(result => {
console.log("GetMessagesResponse", result); console.log("GetMessagesResponse", result);
if (result?.length > 0) { if (result?.length > 0) {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
addOrUpdateMessages(result); addOrUpdateMessages(result);
setMessageFetchState("idle"); setMessageFetchState("idle");
} else setMessageFetchState("all fetched"); } else setMessageFetchState("all fetched");
@ -62,14 +58,11 @@ export default function Chat() {
return () => { return () => {
controller.abort(); controller.abort();
clearMessageState();
}; };
}, [messageApiPage, messagesPerPage, ticketId]); }, [messageApiPage, messagesPerPage, ticketId]);
useEffect(function subscribeToMessages() { useEffect(function subscribeToMessages() {
if (!ticketId) return; if (!ticketId || !token) return;
if (!token) return;
const unsubscribe = subscribeToTicketMessages({ const unsubscribe = subscribeToTicketMessages({
ticketId, ticketId,
@ -94,7 +87,7 @@ export default function Chat() {
clearMessageState(); clearMessageState();
setIsPreventAutoscroll(false); setIsPreventAutoscroll(false);
}; };
}, [ticketId]); }, [ticketId, token]);
useEffect(function attachScrollHandler() { useEffect(function attachScrollHandler() {
if (!chatBoxRef.current) return; if (!chatBoxRef.current) return;
@ -108,7 +101,6 @@ export default function Chat() {
if (messagesFetchStateRef.current !== "idle") return; if (messagesFetchStateRef.current !== "idle") return;
if (chatBox.scrollTop < chatBox.clientHeight) { if (chatBox.scrollTop < chatBox.clientHeight) {
if (chatBox.scrollTop < 1) chatBox.scrollTop = 1;
incrementMessageApiPage(); incrementMessageApiPage();
} }
}; };
@ -168,8 +160,6 @@ export default function Chat() {
} }
} }
const sortedMessages = messages.sort(sortMessagesByTime);
return ( return (
<Box sx={{ <Box sx={{
border: "1px solid", border: "1px solid",
@ -184,9 +174,7 @@ export default function Chat() {
alignItems: "center", alignItems: "center",
gap: "8px", gap: "8px",
}}> }}>
{ticket ? <Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<>
<Typography>{ticket.title}</Typography>
<Box <Box
ref={chatBoxRef} ref={chatBoxRef}
sx={{ sx={{
@ -201,10 +189,11 @@ export default function Chat() {
colorScheme: "dark", colorScheme: "dark",
}} }}
> >
{sortedMessages.map(message => {ticket && messages.map(message =>
<Message key={message.id} message={message} isSelf={ticket.user !== message.user_id} /> <Message key={message.id} message={message} isSelf={ticket.user !== message.user_id} />
)} )}
</Box> </Box>
{ticket &&
<TextField <TextField
value={messageField} value={messageField}
onChange={e => setMessageField(e.target.value)} onChange={e => setMessageField(e.target.value)}
@ -250,15 +239,7 @@ export default function Chat() {
} }
}} }}
/> />
</> }
:
<Typography>Выберите тикет</Typography>}
</Box> </Box>
); );
} }
function sortMessagesByTime(message1: TicketMessage, message2: TicketMessage) {
const date1 = new Date(message1.created_at).getTime();
const date2 = new Date(message2.created_at).getTime();
return date1 - date2;
}

@ -16,7 +16,7 @@ export default function Support() {
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage); const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
const ticketApiPage = useTicketStore(state => state.apiPage); const ticketApiPage = useTicketStore(state => state.apiPage);
const { token } = authStore(); const token = authStore(state => state.token);
useEffect(function fetchTickets() { useEffect(function fetchTickets() {
const getTicketsBody: GetTicketsRequest = { const getTicketsBody: GetTicketsRequest = {
@ -69,7 +69,7 @@ export default function Support() {
clearMessageState(); clearMessageState();
clearTickets(); clearTickets();
}; };
}, []); }, [token]);
return ( return (
<Box sx={{ <Box sx={{

@ -1,28 +1,33 @@
import Cart from "@root/kitUI/Cart/Cart"; import { useState } from "react";
import { Container, Typography } from "@mui/material"; import { Container, Typography } from "@mui/material";
import { GridSelectionModel } from "@mui/x-data-grid";
import Cart from "@root/kitUI/Cart/Cart";
import PrivilegesDG from "./privilegesDG"; import PrivilegesDG from "./privilegesDG";
import TariffsDG from "./tariffsDG"; import TariffsDG from "./tariffsDG";
import CreateTariff from "./CreateTariff"; import CreateTariff from "./CreateTariff";
import { GridSelectionModel } from "@mui/x-data-grid";
import { useState } from "react";
export default function Tariffs() { export default function Tariffs() {
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]); const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]);
return ( return (
<Container sx={{ <Container
sx={{
width: "90%", width: "90%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}}> }}
>
<Typography variant="h6">Список привелегий</Typography> <Typography variant="h6">Список привелегий</Typography>
<PrivilegesDG /> <PrivilegesDG />
<CreateTariff /> <CreateTariff />
<Typography variant="h6" mt="20px">Список тарифов</Typography> <Typography variant="h6" mt="20px">
<TariffsDG handleSelectionChange={selectionModel => setSelectedTariffs(selectionModel)} /> Список тарифов
</Typography>
<TariffsDG handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)} />
<Cart selectedTariffs={selectedTariffs} /> <Cart selectedTariffs={selectedTariffs} />
</Container> </Container>
); );

@ -1,20 +1,21 @@
import * as React from "react"; import * as React from "react";
import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid"; import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
import DataGrid from "@kitUI/datagrid"; import DataGrid from "@kitUI/datagrid";
import { useTariffStore } from "@root/stores/tariffs"; import { useTariffStore } from "@root/stores/tariffs";
import { SERVICE_LIST } from "@root/model/tariff"; import { SERVICE_LIST } from "@root/model/tariff";
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 100 }, { field: "id", headerName: "ID", width: 100 },
{ field: 'name', headerName: 'Название тарифа', width: 150 }, { field: "name", headerName: "Название тарифа", width: 150 },
{ field: 'serviceName', headerName: 'Сервис', width: 150 },//инфо из гитлаба. { field: "serviceName", headerName: "Сервис", width: 150 }, //инфо из гитлаба.
{ field: 'privilege', headerName: 'Привелегия', width: 150 }, { field: "privilege", headerName: "Привелегия", width: 150 },
{ field: 'amount', headerName: 'Количество', width: 110 }, { field: "amount", headerName: "Количество", width: 110 },
{ field: 'type', headerName: 'Единица', width: 100 }, { field: "type", headerName: "Единица", width: 100 },
{ field: 'pricePerUnit', headerName: 'Цена за ед.', width: 100 }, { field: "pricePerUnit", headerName: "Цена за ед.", width: 100 },
{ field: 'isCustomPrice', headerName: 'Кастомная цена', width: 130 }, { field: "isCustomPrice", headerName: "Кастомная цена", width: 130 },
{ field: 'total', headerName: 'Сумма', width: 130 }, { field: "total", headerName: "Сумма", width: 130 },
]; ];
interface Props { interface Props {
@ -22,12 +23,12 @@ interface Props {
} }
export default function TariffsDG({ handleSelectionChange }: Props) { export default function TariffsDG({ handleSelectionChange }: Props) {
const tariffs = useTariffStore(state => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const gridData = tariffs.map(tariff => ({ const gridData = tariffs.map((tariff) => ({
id: tariff.id, id: tariff.id,
name: tariff.name, name: tariff.name,
serviceName: SERVICE_LIST.find(service => service.serviceKey === tariff.privilege.serviceKey)?.displayName, serviceName: SERVICE_LIST.find((service) => service.serviceKey === tariff.privilege.serviceKey)?.displayName,
privilege: `(${tariff.privilege.privilegeId}) ${tariff.privilege.description}`, privilege: `(${tariff.privilege.privilegeId}) ${tariff.privilege.description}`,
amount: tariff.amount, amount: tariff.amount,
type: tariff.privilege.type === "count" ? "день" : "шт.", type: tariff.privilege.type === "count" ? "день" : "шт.",

@ -1,26 +1,37 @@
import * as React from "react"; import * as React from "react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Box, Typography, TextField, Button } from "@mui/material"; import axios from "axios";
import Table from "@mui/material/Table"; import {
import TableHead from "@mui/material/TableHead"; AccordionDetails,
import TableBody from "@mui/material/TableBody"; AccordionSummary,
import TableCell from "@mui/material/TableCell"; Accordion,
import TableRow from "@mui/material/TableRow"; Skeleton,
import Radio from "@mui/material/Radio"; Radio,
import Skeleton from "@mui/material/Skeleton"; Box,
import Accordion from "@mui/material/Accordion"; Typography,
import AccordionSummary from "@mui/material/AccordionSummary"; TextField,
import AccordionDetails from "@mui/material/AccordionDetails"; Button,
Table,
TableHead,
TableBody,
TableCell,
TableRow,
} from "@mui/material";
import { GridSelectionModel } from "@mui/x-data-grid";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import ConditionalRender from "@root/pages/Setting/ConditionalRender";
import ServiceUsersDG from "./ServiceUsersDG";
import { authStore } from "@stores/auth";
import { getRoles_mock, TMockData } from "../../../api/roles"; import { getRoles_mock, TMockData } from "../../../api/roles";
import type { UsersType } from "../../../api/roles";
import theme from "../../../theme"; import theme from "../../../theme";
import axios from "axios";
import {authStore} from "@stores/auth"; import type { UsersType } from "../../../api/roles";
import ConditionalRender from "@root/pages/Setting/ConditionalRender";
const Users: React.FC = () => { const Users: React.FC = () => {
const { makeRequest } = authStore(); const { makeRequest } = authStore();
@ -75,35 +86,48 @@ const Users: React.FC = () => {
const [users, setUsers] = React.useState<UsersType>(); const [users, setUsers] = React.useState<UsersType>();
const [manager, setManager] = React.useState<UsersType>(); const [manager, setManager] = React.useState<UsersType>();
React.useEffect(() => { useEffect(() => {
const axiosRoles = async () => { async function axiosRoles() {
try {
const { data } = await axios({ const { data } = await axios({
method: "get", method: "get",
url: "https://admin.pena.digital/strator/role/", url: "https://admin.pena.digital/strator/role/",
}); });
setRoles(data); setRoles(data);
}; } catch (error) {
const gettingRegisteredUsers = async () => { console.error("Ошибка при получении ролей!");
}
}
async function gettingRegisteredUsers() {
try {
const { data } = await axios({ const { data } = await axios({
method: "get", method: "get",
url: "https://hub.pena.digital/user/", url: "https://hub.pena.digital/user/",
}); });
setUsers(data); setUsers(data);
}; } catch (error) {
console.error("Ошибка при получении пользователей!");
}
}
const gettingListManagers = async () => { async function gettingListManagers() {
try {
const { data } = await axios({ const { data } = await axios({
method: "get", method: "get",
url: "https://admin.pena.digital/user/", url: "https://hub.pena.digital/user/",
}); });
setManager(data); setManager(data);
}; } catch (error) {
console.error("Ошибка при получении менеджеров!");
}
}
gettingListManagers(); gettingListManagers();
gettingRegisteredUsers(); gettingRegisteredUsers();
axiosRoles(); axiosRoles();
}, []); }, [selectedValue]);
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]);
return ( return (
<React.Fragment> <React.Fragment>
<Button <Button
@ -390,219 +414,24 @@ const Users: React.FC = () => {
</Box> </Box>
</Box> </Box>
</Box> </Box>
<Box component="section" sx={{ width: "90%", mt: "45px", display: "flex", justifyContent: "center" }}>
<Table
sx={{
width: "90%",
border: "2px solid",
borderColor: theme.palette.grayLight.main,
marginTop: "35px",
}}
aria-label="simple table"
>
<TableHead>
<TableRow
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
login
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
email
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
phoneNumber
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
isDeleted
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
createdAt
</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<ConditionalRender <ConditionalRender
isLoading={false} isLoading={false}
role={selectedValue} role={selectedValue}
childrenManager={ childrenManager={
<> <ServiceUsersDG
{manager && users={manager}
manager.map(({ login, email, phoneNumber, isDeleted, createdAt }) => ( handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
<TableRow />
key={createdAt}
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{login}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{email}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{phoneNumber}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{isDeleted ? "true" : "false"}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{createdAt}
</Typography>
</TableCell>
</TableRow>
))}
</>
} }
childrenUser={ childrenUser={
<> <ServiceUsersDG
{users && users={users}
users.map(({ login, email, phoneNumber, isDeleted, createdAt }) => ( handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
<TableRow />
key={createdAt}
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{login}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{email}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{phoneNumber}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{isDeleted ? "true" : "false"}
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
{createdAt}
</Typography>
</TableCell>
</TableRow>
))}
</>
} }
/> />
</TableBody> </Box>
</Table>
</React.Fragment> </React.Fragment>
); );
}; };

@ -58,10 +58,10 @@ export const incrementMessageApiPage = () => {
useMessageStore.setState({ apiPage: state.apiPage + 1 }); useMessageStore.setState({ apiPage: state.apiPage + 1 });
}; };
export const setIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useMessageStore.setState({ isPreventAutoscroll });
function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) { function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) {
const date1 = new Date(ticket1.created_at).getTime(); const date1 = new Date(ticket1.created_at).getTime();
const date2 = new Date(ticket2.created_at).getTime(); const date2 = new Date(ticket2.created_at).getTime();
return date1 - date2; return date1 - date2;
} }
export const setIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useMessageStore.setState({ isPreventAutoscroll });