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,110 +1,104 @@
import makeRequest from "@root/kitUI/makeRequest";
import { import {
GetMessagesRequest, GetMessagesRequest,
GetMessagesResponse, GetMessagesResponse,
GetTicketsRequest, GetTicketsRequest,
GetTicketsResponse, GetTicketsResponse,
SendTicketMessageRequest, SendTicketMessageRequest,
} from "@root/model/ticket"; } from "@root/model/ticket";
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,
accessToken, accessToken,
}: { }: {
accessToken: string; accessToken: string;
onMessage: (e: MessageEvent) => void; onMessage: (e: MessageEvent) => void;
onError: (e: Event) => void; onError: (e: Event) => void;
}) { }) {
const url = `${supportApiUrl}/subscribe?Authorization=${accessToken}`; const url = `${supportApiUrl}/subscribe?Authorization=${accessToken}`;
const eventSource = createEventSource(onMessage, onError, url); const eventSource = createEventSource(onMessage, onError, url);
return () => { return () => {
eventSource.close(); eventSource.close();
}; };
} }
export function subscribeToTicketMessages({ export function subscribeToTicketMessages({
onMessage, onMessage,
onError, onError,
accessToken, accessToken,
ticketId, ticketId,
}: { }: {
accessToken: string; accessToken: string;
ticketId: string; ticketId: string;
onMessage: (e: MessageEvent) => void; onMessage: (e: MessageEvent) => void;
onError: (e: Event) => void; onError: (e: Event) => void;
}) { }) {
const url = `${supportApiUrl}/ticket?ticket=${ticketId}&Authorization=${accessToken}`; const url = `${supportApiUrl}/ticket?ticket=${ticketId}&Authorization=${accessToken}`;
const eventSource = createEventSource(onMessage, onError, url); const eventSource = createEventSource(onMessage, onError, url);
return () => { return () => {
eventSource.close(); eventSource.close();
}; };
} }
export async function getTickets({ export async function getTickets({
body,
signal,
}: {
body: GetTicketsRequest;
signal: AbortSignal;
}): Promise<GetTicketsResponse> {
return makeRequest({
url: `${supportApiUrl}/getTickets`,
method: "POST",
useToken: true,
body, body,
signal, signal,
}).then((response) => { }: {
const result = (response as any).data as GetTicketsResponse; body: GetTicketsRequest;
return result; signal: AbortSignal;
}); }): Promise<GetTicketsResponse> {
return makeRequest<GetTicketsRequest, GetTicketsResponse>({
url: `${supportApiUrl}/getTickets`,
method: "POST",
useToken: true,
body,
signal,
});
} }
export async function getTicketMessages({ export async function getTicketMessages({
body,
signal,
}: {
body: GetMessagesRequest;
signal: AbortSignal;
}): Promise<GetMessagesResponse> {
return makeRequest({
url: `${supportApiUrl}/getMessages`,
method: "POST",
useToken: true,
body, body,
signal, signal,
}).then((response) => { }: {
const result = (response as any).data as GetMessagesResponse; body: GetMessagesRequest;
return result; signal: AbortSignal;
}); }): Promise<GetMessagesResponse> {
return makeRequest<GetMessagesRequest, GetMessagesResponse>({
url: `${supportApiUrl}/getMessages`,
method: "POST",
useToken: true,
body,
signal,
});
} }
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",
useToken: true, useToken: true,
body, body,
}); });
} }
function createEventSource(onMessage: (e: MessageEvent) => void, onError: (e: Event) => void, url: string) { function createEventSource(onMessage: (e: MessageEvent) => void, onError: (e: Event) => void, url: string) {
const eventSource = new ReconnectingEventSource(url); const eventSource = new ReconnectingEventSource(url);
eventSource.addEventListener("open", () => console.log(`EventSource connected with ${url}`)); eventSource.addEventListener("open", () => console.log(`EventSource connected with ${url}`));
eventSource.addEventListener("close", () => console.log(`EventSource closed with ${url}`)); eventSource.addEventListener("close", () => console.log(`EventSource closed with ${url}`));
eventSource.addEventListener("message", onMessage); eventSource.addEventListener("message", onMessage);
eventSource.addEventListener("error", onError); eventSource.addEventListener("error", onError);
return eventSource; return eventSource;
} }

@ -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,46 +1,39 @@
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;
name: PrivilegeType; name: PrivilegeType;
privilegeId: string; privilegeId: string;
description: string; description: string;
/** Единица измерения привелегии: время в днях/кол-во */ /** Единица измерения привелегии: время в днях/кол-во */
type: "day" | "count"; type: "day" | "count";
/** Стоимость одной единицы привелегии */ /** Стоимость одной единицы привелегии */
pricePerUnit: number; pricePerUnit: number;
} }
export interface Tariff { export interface Tariff {
id: string; id: string;
name: string; name: string;
privilege: Privilege; privilege: Privilege;
/** Количество единиц привелегии */ /** Количество единиц привелегии */
amount: number; amount: number;
/** Кастомная цена, если есть, то используется вместо privilege.price */ /** Кастомная цена, если есть, то используется вместо privilege.price */
customPricePerUnit?: number; customPricePerUnit?: number;
} }

@ -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,13 +30,8 @@ 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(); if (!ticketId) return;
}, [messages]);
useEffect(
function fetchTicketMessages() {
if (!ticketId) return;
const getTicketsBody: GetMessagesRequest = { const getTicketsBody: GetMessagesRequest = {
amt: messagesPerPage, amt: messagesPerPage,
@ -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,39 +58,36 @@ 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({
ticketId,
const unsubscribe = subscribeToTicketMessages({ accessToken: token,
ticketId, onMessage(event) {
accessToken: token, try {
onMessage(event) { const newMessage = JSON.parse(event.data) as TicketMessage;
try { console.log("SSE: parsed newMessage:", newMessage);
const newMessage = JSON.parse(event.data) as TicketMessage; addOrUpdateMessages([newMessage]);
console.log("SSE: parsed newMessage:", newMessage); } catch (error) {
addOrUpdateMessages([newMessage]); console.log("SSE: couldn't parse:", event.data);
} catch (error) { console.log("Error parsing message SSE", error);
console.log("SSE: couldn't parse:", event.data); }
console.log("Error parsing message SSE", error); },
} onError(event) {
}, console.log("SSE Error:", event);
onError(event) { },
console.log("SSE Error:", event); });
},
});
return () => { return () => {
unsubscribe(); unsubscribe();
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();
} }
}; };
@ -159,7 +151,7 @@ export default function Chat() {
setMessageField(""); setMessageField("");
} }
function handleAddAttachment() {} function handleAddAttachment() { }
function handleTextfieldKeyPress(e: KeyboardEvent) { function handleTextfieldKeyPress(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
@ -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,81 +174,72 @@ export default function Chat() {
alignItems: "center", alignItems: "center",
gap: "8px", gap: "8px",
}}> }}>
{ticket ? <Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<> <Box
<Typography>{ticket.title}</Typography> ref={chatBoxRef}
<Box sx={{
ref={chatBoxRef} width: "100%",
sx={{ backgroundColor: "#46474a",
width: "100%", flexGrow: 1,
backgroundColor: "#46474a", display: "flex",
flexGrow: 1, flexDirection: "column",
display: "flex", gap: "12px",
flexDirection: "column", p: "8px",
gap: "12px", overflow: "auto",
p: "8px", colorScheme: "dark",
overflow: "auto", }}
colorScheme: "dark", >
}} {ticket && messages.map(message =>
> <Message key={message.id} message={message} isSelf={ticket.user !== message.user_id} />
{sortedMessages.map(message => )}
<Message key={message.id} message={message} isSelf={ticket.user !== message.user_id} /> </Box>
)} {ticket &&
</Box> <TextField
<TextField value={messageField}
value={messageField} onChange={e => setMessageField(e.target.value)}
onChange={e => setMessageField(e.target.value)} onKeyPress={handleTextfieldKeyPress}
onKeyPress={handleTextfieldKeyPress} id="message-input"
id="message-input" placeholder="Написать сообщение"
placeholder="Написать сообщение" fullWidth
fullWidth multiline
multiline maxRows={8}
maxRows={8} InputProps={{
InputProps={{ style: {
style: { backgroundColor: theme.palette.content.main,
backgroundColor: theme.palette.content.main, color: theme.palette.secondary.main,
color: theme.palette.secondary.main, },
}, endAdornment: (
endAdornment: ( <InputAdornment position="end">
<InputAdornment position="end"> <IconButton
<IconButton onClick={handleSendMessage}
onClick={handleSendMessage} sx={{
sx={{ height: "45px",
height: "45px", width: "45px",
width: "45px", p: 0,
p: 0, }}
}} >
> <SendIcon sx={{ color: theme.palette.golden.main }} />
<SendIcon sx={{ color: theme.palette.golden.main }} /> </IconButton>
</IconButton> <IconButton
<IconButton onClick={handleAddAttachment}
onClick={handleAddAttachment} sx={{
sx={{ height: "45px",
height: "45px", width: "45px",
width: "45px", p: 0,
p: 0, }}
}} >
> <AttachFileIcon sx={{ color: theme.palette.golden.main }} />
<AttachFileIcon sx={{ color: theme.palette.golden.main }} /> </IconButton>
</IconButton> </InputAdornment>
</InputAdornment> )
) }}
}} InputLabelProps={{
InputLabelProps={{ style: {
style: { color: theme.palette.secondary.main,
color: theme.palette.secondary.main, }
} }}
}} />
/> }
</>
:
<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;
}

@ -7,7 +7,7 @@ import { getTickets, subscribeToAllTickets } from "@root/api/tickets";
import { GetTicketsRequest, Ticket } from "@root/model/ticket"; import { GetTicketsRequest, 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 { enqueueSnackbar } from "notistack";
import {clearMessageState } from "@root/stores/messages"; import { clearMessageState } from "@root/stores/messages";
import { authStore } from "@root/stores/auth"; import { authStore } from "@root/stores/auth";
@ -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,29 +1,34 @@
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
width: "90%", sx={{
display: "flex", width: "90%",
flexDirection: "column", display: "flex",
justifyContent: "center", flexDirection: "column",
alignItems: "center", justifyContent: "center",
}}> alignItems: "center",
<Typography variant="h6">Список привелегий</Typography> }}
<PrivilegesDG /> >
<CreateTariff /> <Typography variant="h6">Список привелегий</Typography>
<Typography variant="h6" mt="20px">Список тарифов</Typography> <PrivilegesDG />
<TariffsDG handleSelectionChange={selectionModel => setSelectedTariffs(selectionModel)} /> <CreateTariff />
<Cart selectedTariffs={selectedTariffs} /> <Typography variant="h6" mt="20px">
</Container> Список тарифов
); </Typography>
} <TariffsDG handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)} />
<Cart selectedTariffs={selectedTariffs} />
</Container>
);
}

@ -1,48 +1,49 @@
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 {
handleSelectionChange: (selectionModel: GridSelectionModel) => void; handleSelectionChange: (selectionModel: GridSelectionModel) => void;
} }
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" ? "день" : "шт.",
pricePerUnit: tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit, pricePerUnit: tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit,
isCustomPrice: tariff.customPricePerUnit === undefined ? "Нет" : "Да", isCustomPrice: tariff.customPricePerUnit === undefined ? "Нет" : "Да",
total: tariff.amount * (tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit), total: tariff.amount * (tariff.customPricePerUnit ?? tariff.privilege.pricePerUnit),
})); }));
return ( return (
<DataGrid <DataGrid
checkboxSelection={true} checkboxSelection={true}
rows={gridData} rows={gridData}
columns={columns} columns={columns}
components={{ Toolbar: GridToolbar }} components={{ Toolbar: GridToolbar }}
onSelectionModelChange={handleSelectionChange} onSelectionModelChange={handleSelectionChange}
/> />
); );
} }

@ -1,35 +1,46 @@
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 theme from "../../../theme";
import type { UsersType } 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";
const Users: React.FC = () => { const Users: React.FC = () => {
const { makeRequest } = authStore(); const { makeRequest } = authStore();
// makeRequest({ // makeRequest({
// url: "https://admin.pena.digital/strator/account", // url: "https://admin.pena.digital/strator/account",
// method: "get", // method: "get",
// bearer: true, // bearer: true,
// contentType: true, // contentType: true,
// }) // })
const radioboxes = ["admin", "manager", "user"]; const radioboxes = ["admin", "manager", "user"];
const [selectedValue, setSelectedValue] = React.useState("admin"); const [selectedValue, setSelectedValue] = React.useState("admin");
@ -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() {
const { data } = await axios({ try {
method: "get", const { data } = await axios({
url: "https://admin.pena.digital/strator/role/", method: "get",
}); url: "https://admin.pena.digital/strator/role/",
setRoles(data); });
}; setRoles(data);
const gettingRegisteredUsers = async () => { } catch (error) {
const { data } = await axios({ console.error("Ошибка при получении ролей!");
method: "get", }
url: "https://hub.pena.digital/user/", }
}); async function gettingRegisteredUsers() {
setUsers(data); 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() {
const { data } = await axios({ try {
method: "get", const { data } = await axios({
url: "https://admin.pena.digital/user/", method: "get",
}); 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 <ConditionalRender
sx={{ isLoading={false}
width: "90%", role={selectedValue}
border: "2px solid", childrenManager={
borderColor: theme.palette.grayLight.main, <ServiceUsersDG
marginTop: "35px", users={manager}
}} handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
aria-label="simple table" />
> }
<TableHead> childrenUser={
<TableRow <ServiceUsersDG
sx={{ users={users}
borderBottom: "2px solid", handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
borderColor: theme.palette.grayLight.main, />
height: "100px", }
}} />
> </Box>
<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
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>
))}
</>
}
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>
))}
</>
}
/>
</TableBody>
</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 });