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 {
GetMessagesRequest,
GetMessagesResponse,
GetTicketsRequest,
GetTicketsResponse,
SendTicketMessageRequest,
GetMessagesRequest,
GetMessagesResponse,
GetTicketsRequest,
GetTicketsResponse,
SendTicketMessageRequest,
} from "@root/model/ticket";
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,
accessToken,
onMessage,
onError,
accessToken,
}: {
accessToken: string;
onMessage: (e: MessageEvent) => void;
onError: (e: Event) => void;
accessToken: string;
onMessage: (e: MessageEvent) => 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 () => {
eventSource.close();
};
return () => {
eventSource.close();
};
}
export function subscribeToTicketMessages({
onMessage,
onError,
accessToken,
ticketId,
onMessage,
onError,
accessToken,
ticketId,
}: {
accessToken: string;
ticketId: string;
onMessage: (e: MessageEvent) => void;
onError: (e: Event) => void;
accessToken: string;
ticketId: string;
onMessage: (e: MessageEvent) => 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 () => {
eventSource.close();
};
return () => {
eventSource.close();
};
}
export async function getTickets({
body,
signal,
}: {
body: GetTicketsRequest;
signal: AbortSignal;
}): Promise<GetTicketsResponse> {
return makeRequest({
url: `${supportApiUrl}/getTickets`,
method: "POST",
useToken: true,
body,
signal,
}).then((response) => {
const result = (response as any).data as GetTicketsResponse;
return result;
});
}: {
body: GetTicketsRequest;
signal: AbortSignal;
}): Promise<GetTicketsResponse> {
return makeRequest<GetTicketsRequest, GetTicketsResponse>({
url: `${supportApiUrl}/getTickets`,
method: "POST",
useToken: true,
body,
signal,
});
}
export async function getTicketMessages({
body,
signal,
}: {
body: GetMessagesRequest;
signal: AbortSignal;
}): Promise<GetMessagesResponse> {
return makeRequest({
url: `${supportApiUrl}/getMessages`,
method: "POST",
useToken: true,
body,
signal,
}).then((response) => {
const result = (response as any).data as GetMessagesResponse;
return result;
});
}: {
body: GetMessagesRequest;
signal: AbortSignal;
}): Promise<GetMessagesResponse> {
return makeRequest<GetMessagesRequest, GetMessagesResponse>({
url: `${supportApiUrl}/getMessages`,
method: "POST",
useToken: true,
body,
signal,
});
}
export async function sendTicketMessage({ body }: { body: SendTicketMessageRequest }) {
return makeRequest({
url: `${supportApiUrl}/send`,
method: "POST",
useToken: true,
body,
});
export async function sendTicketMessage({ body }: { body: SendTicketMessageRequest; }) {
return makeRequest({
url: `${supportApiUrl}/send`,
method: "POST",
useToken: true,
body,
});
}
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("close", () => console.log(`EventSource closed with ${url}`));
eventSource.addEventListener("message", onMessage);
eventSource.addEventListener("error", onError);
eventSource.addEventListener("open", () => console.log(`EventSource connected with ${url}`));
eventSource.addEventListener("close", () => console.log(`EventSource closed with ${url}`));
eventSource.addEventListener("message", onMessage);
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 = [
{
serviceKey: "templategen",
displayName: "Шаблонизатор документов"
},
{
serviceKey: "squiz",
displayName: "Опросник"
},
{
serviceKey: "dwarfener",
displayName: "Сокращатель ссылок"
}
{
serviceKey: "templategen",
displayName: "Шаблонизатор документов",
},
{
serviceKey: "squiz",
displayName: "Опросник",
},
{
serviceKey: "dwarfener",
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;
name: PrivilegeType;
privilegeId: string;
description: string;
/** Единица измерения привелегии: время в днях/кол-во */
type: "day" | "count";
/** Стоимость одной единицы привелегии */
pricePerUnit: number;
serviceKey: ServiceType;
name: PrivilegeType;
privilegeId: string;
description: string;
/** Единица измерения привелегии: время в днях/кол-во */
type: "day" | "count";
/** Стоимость одной единицы привелегии */
pricePerUnit: number;
}
export interface Tariff {
id: string;
name: string;
privilege: Privilege;
/** Количество единиц привелегии */
amount: number;
/** Кастомная цена, если есть, то используется вместо privilege.price */
customPricePerUnit?: number;
}
id: string;
name: string;
privilege: Privilege;
/** Количество единиц привелегии */
amount: number;
/** Кастомная цена, если есть, то используется вместо privilege.price */
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 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>
);
};

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

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

@ -1,35 +1,46 @@
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 theme from "../../../theme";
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 { makeRequest } = authStore();
// makeRequest({
// url: "https://admin.pena.digital/strator/account",
// method: "get",
// bearer: true,
// contentType: true,
// })
const { makeRequest } = authStore();
// makeRequest({
// url: "https://admin.pena.digital/strator/account",
// method: "get",
// bearer: true,
// contentType: true,
// })
const radioboxes = ["admin", "manager", "user"];
const [selectedValue, setSelectedValue] = React.useState("admin");
@ -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 () => {
const { data } = await axios({
method: "get",
url: "https://admin.pena.digital/strator/role/",
});
setRoles(data);
};
const gettingRegisteredUsers = async () => {
const { data } = await axios({
method: "get",
url: "https://hub.pena.digital/user/",
});
setUsers(data);
};
useEffect(() => {
async function axiosRoles() {
try {
const { data } = await axios({
method: "get",
url: "https://admin.pena.digital/strator/role/",
});
setRoles(data);
} 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 () => {
const { data } = await axios({
method: "get",
url: "https://admin.pena.digital/user/",
});
setManager(data);
};
async function gettingListManagers() {
try {
const { data } = await axios({
method: "get",
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>
<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>
<Box component="section" sx={{ width: "90%", mt: "45px", display: "flex", justifyContent: "center" }}>
<ConditionalRender
isLoading={false}
role={selectedValue}
childrenManager={
<ServiceUsersDG
users={manager}
handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
/>
}
childrenUser={
<ServiceUsersDG
users={users}
handleSelectionChange={(selectionModel) => setSelectedTariffs(selectionModel)}
/>
}
/>
</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 });
}