Merge branch 'dev' into 'main'
Users: replacing table with dataGrid && See merge request frontend/admin!12
This commit is contained in:
commit
28fdd70cab
@ -1,4 +1,3 @@
|
||||
import makeRequest from "@root/kitUI/makeRequest";
|
||||
import {
|
||||
GetMessagesRequest,
|
||||
GetMessagesResponse,
|
||||
@ -9,10 +8,11 @@ import {
|
||||
import { authStore } from "@root/stores/auth";
|
||||
import ReconnectingEventSource from "reconnecting-eventsource";
|
||||
|
||||
// const { makeRequest } = authStore();
|
||||
|
||||
const supportApiUrl = "https://admin.pena.digital/heruvym";
|
||||
|
||||
const makeRequest = authStore.getState().makeRequest;
|
||||
|
||||
export function subscribeToAllTickets({
|
||||
onMessage,
|
||||
onError,
|
||||
@ -58,15 +58,12 @@ export async function getTickets({
|
||||
body: GetTicketsRequest;
|
||||
signal: AbortSignal;
|
||||
}): Promise<GetTicketsResponse> {
|
||||
return makeRequest({
|
||||
return makeRequest<GetTicketsRequest, GetTicketsResponse>({
|
||||
url: `${supportApiUrl}/getTickets`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body,
|
||||
signal,
|
||||
}).then((response) => {
|
||||
const result = (response as any).data as GetTicketsResponse;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@ -77,19 +74,16 @@ export async function getTicketMessages({
|
||||
body: GetMessagesRequest;
|
||||
signal: AbortSignal;
|
||||
}): Promise<GetMessagesResponse> {
|
||||
return makeRequest({
|
||||
return makeRequest<GetMessagesRequest, GetMessagesResponse>({
|
||||
url: `${supportApiUrl}/getMessages`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body,
|
||||
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({
|
||||
url: `${supportApiUrl}/send`,
|
||||
method: "POST",
|
||||
|
52
src/hooks/privilege.hook.ts
Normal file
52
src/hooks/privilege.hook.ts
Normal file
@ -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
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 = [
|
||||
{
|
||||
serviceKey: "templategen",
|
||||
displayName: "Шаблонизатор документов"
|
||||
displayName: "Шаблонизатор документов",
|
||||
},
|
||||
{
|
||||
serviceKey: "squiz",
|
||||
displayName: "Опросник"
|
||||
displayName: "Опросник",
|
||||
},
|
||||
{
|
||||
serviceKey: "dwarfener",
|
||||
displayName: "Сокращатель ссылок"
|
||||
}
|
||||
displayName: "Сокращатель ссылок",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ServiceType = typeof SERVICE_LIST[number]["serviceKey"];
|
||||
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
|
||||
|
||||
export type PrivilegeType =
|
||||
| "unlim"
|
||||
| "gencount"
|
||||
| "activequiz"
|
||||
| "abcount"
|
||||
| "extended";
|
||||
export type PrivilegeType = "unlim" | "gencount" | "activequiz" | "abcount" | "extended";
|
||||
|
||||
export interface Privilege {
|
||||
serviceKey: ServiceType;
|
||||
|
117
src/pages/Setting/CardPrivilegie.tsx
Normal file
117
src/pages/Setting/CardPrivilegie.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
139
src/pages/Setting/PrivilegiesWrapper.tsx
Normal file
139
src/pages/Setting/PrivilegiesWrapper.tsx
Normal file
@ -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 FormCreateRoles from "./FormCreateRoles";
|
||||
import { PrivilegiesWrapper } from "./PrivilegiesWrapper";
|
||||
|
||||
import theme from "../../theme";
|
||||
|
||||
export const SettingRoles = () => {
|
||||
export const SettingRoles = (): JSX.Element => {
|
||||
return (
|
||||
<AccordionDetails>
|
||||
<Table
|
||||
@ -99,6 +100,7 @@ export const SettingRoles = () => {
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<PrivilegiesWrapper text="Привелегии" sx={{ mt: "50px" }} />
|
||||
</AccordionDetails>
|
||||
);
|
||||
};
|
||||
|
45
src/pages/dashboard/Content/ServiceUsersDG.tsx
Normal file
45
src/pages/dashboard/Content/ServiceUsersDG.tsx
Normal file
@ -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 { useTicketStore } from "@root/stores/tickets";
|
||||
import { throttle } from "@root/utils/throttle";
|
||||
|
||||
import { authStore } from "@root/stores/auth";
|
||||
|
||||
|
||||
export default function Chat() {
|
||||
const { token } = authStore();
|
||||
const token = authStore(state => state.token);
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const tickets = useTicketStore(state => state.tickets);
|
||||
@ -30,12 +30,7 @@ export default function Chat() {
|
||||
|
||||
const ticket = tickets.find(ticket => ticket.id === ticketId);
|
||||
|
||||
useEffect(function scrollOnNewMessage() {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
useEffect(
|
||||
function fetchTicketMessages() {
|
||||
useEffect(function fetchTicketMessages() {
|
||||
if (!ticketId) return;
|
||||
|
||||
const getTicketsBody: GetMessagesRequest = {
|
||||
@ -52,6 +47,7 @@ export default function Chat() {
|
||||
}).then(result => {
|
||||
console.log("GetMessagesResponse", result);
|
||||
if (result?.length > 0) {
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||
addOrUpdateMessages(result);
|
||||
setMessageFetchState("idle");
|
||||
} else setMessageFetchState("all fetched");
|
||||
@ -62,14 +58,11 @@ export default function Chat() {
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
clearMessageState();
|
||||
};
|
||||
}, [messageApiPage, messagesPerPage, ticketId]);
|
||||
|
||||
useEffect(function subscribeToMessages() {
|
||||
if (!ticketId) return;
|
||||
|
||||
if (!token) return;
|
||||
if (!ticketId || !token) return;
|
||||
|
||||
const unsubscribe = subscribeToTicketMessages({
|
||||
ticketId,
|
||||
@ -94,7 +87,7 @@ export default function Chat() {
|
||||
clearMessageState();
|
||||
setIsPreventAutoscroll(false);
|
||||
};
|
||||
}, [ticketId]);
|
||||
}, [ticketId, token]);
|
||||
|
||||
useEffect(function attachScrollHandler() {
|
||||
if (!chatBoxRef.current) return;
|
||||
@ -108,7 +101,6 @@ export default function Chat() {
|
||||
if (messagesFetchStateRef.current !== "idle") return;
|
||||
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
if (chatBox.scrollTop < 1) chatBox.scrollTop = 1;
|
||||
incrementMessageApiPage();
|
||||
}
|
||||
};
|
||||
@ -168,8 +160,6 @@ export default function Chat() {
|
||||
}
|
||||
}
|
||||
|
||||
const sortedMessages = messages.sort(sortMessagesByTime);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
border: "1px solid",
|
||||
@ -184,9 +174,7 @@ export default function Chat() {
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}>
|
||||
{ticket ?
|
||||
<>
|
||||
<Typography>{ticket.title}</Typography>
|
||||
<Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
@ -201,10 +189,11 @@ export default function Chat() {
|
||||
colorScheme: "dark",
|
||||
}}
|
||||
>
|
||||
{sortedMessages.map(message =>
|
||||
{ticket && messages.map(message =>
|
||||
<Message key={message.id} message={message} isSelf={ticket.user !== message.user_id} />
|
||||
)}
|
||||
</Box>
|
||||
{ticket &&
|
||||
<TextField
|
||||
value={messageField}
|
||||
onChange={e => setMessageField(e.target.value)}
|
||||
@ -250,15 +239,7 @@ export default function Chat() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
:
|
||||
<Typography>Выберите тикет</Typography>}
|
||||
}
|
||||
</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 ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
||||
const ticketApiPage = useTicketStore(state => state.apiPage);
|
||||
const { token } = authStore();
|
||||
const token = authStore(state => state.token);
|
||||
|
||||
useEffect(function fetchTickets() {
|
||||
const getTicketsBody: GetTicketsRequest = {
|
||||
@ -69,7 +69,7 @@ export default function Support() {
|
||||
clearMessageState();
|
||||
clearTickets();
|
||||
};
|
||||
}, []);
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
|
@ -1,28 +1,33 @@
|
||||
import Cart from "@root/kitUI/Cart/Cart";
|
||||
import { useState } from "react";
|
||||
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 TariffsDG from "./tariffsDG";
|
||||
import CreateTariff from "./CreateTariff";
|
||||
import { GridSelectionModel } from "@mui/x-data-grid";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
export default function Tariffs() {
|
||||
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]);
|
||||
|
||||
return (
|
||||
<Container sx={{
|
||||
<Container
|
||||
sx={{
|
||||
width: "90%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Список привелегий</Typography>
|
||||
<PrivilegesDG />
|
||||
<CreateTariff />
|
||||
<Typography variant="h6" mt="20px">Список тарифов</Typography>
|
||||
<TariffsDG handleSelectionChange={selectionModel => setSelectedTariffs(selectionModel)} />
|
||||
<Typography variant="h6" mt="20px">
|
||||
Список тарифов
|
||||
</Typography>
|
||||
<TariffsDG handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)} />
|
||||
<Cart selectedTariffs={selectedTariffs} />
|
||||
</Container>
|
||||
);
|
||||
|
@ -1,20 +1,21 @@
|
||||
import * as React from "react";
|
||||
import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
|
||||
|
||||
import DataGrid from "@kitUI/datagrid";
|
||||
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import { SERVICE_LIST } from "@root/model/tariff";
|
||||
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', width: 100 },
|
||||
{ field: 'name', headerName: 'Название тарифа', width: 150 },
|
||||
{ field: 'serviceName', headerName: 'Сервис', width: 150 },//инфо из гитлаба.
|
||||
{ field: 'privilege', headerName: 'Привелегия', width: 150 },
|
||||
{ field: 'amount', headerName: 'Количество', width: 110 },
|
||||
{ field: 'type', headerName: 'Единица', width: 100 },
|
||||
{ field: 'pricePerUnit', headerName: 'Цена за ед.', width: 100 },
|
||||
{ field: 'isCustomPrice', headerName: 'Кастомная цена', width: 130 },
|
||||
{ field: 'total', headerName: 'Сумма', width: 130 },
|
||||
{ field: "id", headerName: "ID", width: 100 },
|
||||
{ field: "name", headerName: "Название тарифа", width: 150 },
|
||||
{ field: "serviceName", headerName: "Сервис", width: 150 }, //инфо из гитлаба.
|
||||
{ field: "privilege", headerName: "Привелегия", width: 150 },
|
||||
{ field: "amount", headerName: "Количество", width: 110 },
|
||||
{ field: "type", headerName: "Единица", width: 100 },
|
||||
{ field: "pricePerUnit", headerName: "Цена за ед.", width: 100 },
|
||||
{ field: "isCustomPrice", headerName: "Кастомная цена", width: 130 },
|
||||
{ field: "total", headerName: "Сумма", width: 130 },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
@ -22,12 +23,12 @@ interface 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,
|
||||
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}`,
|
||||
amount: tariff.amount,
|
||||
type: tariff.privilege.type === "count" ? "день" : "шт.",
|
||||
|
@ -1,26 +1,37 @@
|
||||
import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Box, Typography, TextField, Button } from "@mui/material";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import axios from "axios";
|
||||
import {
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Accordion,
|
||||
Skeleton,
|
||||
Radio,
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Table,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import { GridSelectionModel } from "@mui/x-data-grid";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
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 type { UsersType } from "../../../api/roles";
|
||||
|
||||
import theme from "../../../theme";
|
||||
import axios from "axios";
|
||||
import {authStore} from "@stores/auth";
|
||||
import ConditionalRender from "@root/pages/Setting/ConditionalRender";
|
||||
|
||||
import type { UsersType } from "../../../api/roles";
|
||||
|
||||
const Users: React.FC = () => {
|
||||
const { makeRequest } = authStore();
|
||||
@ -75,35 +86,48 @@ const Users: React.FC = () => {
|
||||
const [users, setUsers] = React.useState<UsersType>();
|
||||
const [manager, setManager] = React.useState<UsersType>();
|
||||
|
||||
React.useEffect(() => {
|
||||
const axiosRoles = async () => {
|
||||
useEffect(() => {
|
||||
async function axiosRoles() {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "get",
|
||||
url: "https://admin.pena.digital/strator/role/",
|
||||
});
|
||||
setRoles(data);
|
||||
};
|
||||
const gettingRegisteredUsers = async () => {
|
||||
} catch (error) {
|
||||
console.error("Ошибка при получении ролей!");
|
||||
}
|
||||
}
|
||||
async function gettingRegisteredUsers() {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "get",
|
||||
url: "https://hub.pena.digital/user/",
|
||||
});
|
||||
setUsers(data);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Ошибка при получении пользователей!");
|
||||
}
|
||||
}
|
||||
|
||||
const gettingListManagers = async () => {
|
||||
async function gettingListManagers() {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "get",
|
||||
url: "https://admin.pena.digital/user/",
|
||||
url: "https://hub.pena.digital/user/",
|
||||
});
|
||||
setManager(data);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Ошибка при получении менеджеров!");
|
||||
}
|
||||
}
|
||||
|
||||
gettingListManagers();
|
||||
gettingRegisteredUsers();
|
||||
axiosRoles();
|
||||
}, []);
|
||||
}, [selectedValue]);
|
||||
|
||||
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
@ -390,219 +414,24 @@ const Users: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
<Box component="section" sx={{ width: "90%", mt: "45px", display: "flex", justifyContent: "center" }}>
|
||||
<ConditionalRender
|
||||
isLoading={false}
|
||||
role={selectedValue}
|
||||
childrenManager={
|
||||
<>
|
||||
{manager &&
|
||||
manager.map(({ login, email, phoneNumber, isDeleted, createdAt }) => (
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
<ServiceUsersDG
|
||||
users={manager}
|
||||
handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
|
||||
/>
|
||||
}
|
||||
childrenUser={
|
||||
<>
|
||||
{users &&
|
||||
users.map(({ login, email, phoneNumber, isDeleted, createdAt }) => (
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
<ServiceUsersDG
|
||||
users={users}
|
||||
handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -58,10 +58,10 @@ export const incrementMessageApiPage = () => {
|
||||
useMessageStore.setState({ apiPage: state.apiPage + 1 });
|
||||
};
|
||||
|
||||
export const setIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useMessageStore.setState({ isPreventAutoscroll });
|
||||
|
||||
function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) {
|
||||
const date1 = new Date(ticket1.created_at).getTime();
|
||||
const date2 = new Date(ticket2.created_at).getTime();
|
||||
return date1 - date2;
|
||||
}
|
||||
|
||||
export const setIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useMessageStore.setState({ isPreventAutoscroll });
|
Loading…
Reference in New Issue
Block a user