diff --git a/Dockerfile b/Dockerfile index 2d2cc1a..5218d77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,8 @@ RUN npm config delete proxy RUN npm install RUN npm run build +FROM nginx:latest + COPY --from=tres-build-base /app/dist /usr/share/nginx/html COPY ./etc/www/tres.png /usr/share/nginx/html/tres.png COPY ./etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index aa65bbe..95b6edc 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -27,7 +27,8 @@ "tooltip": { "report": "Report ticket", "suggestion": "Suggestion ticket", - "question": "Question ticket" + "question": "Question ticket", + "notSelected": "Not selected" } }, "sidebar": { @@ -50,7 +51,8 @@ "scopes": { "reportTitle": "Report Tickets", "questionTitle": "Question Tickets", - "suggestionTitle": "Suggestion Tickets" + "suggestionTitle": "Suggestion Tickets", + "notSelectedTitle": "Not selected Tickets" } }, "sent": { @@ -85,11 +87,16 @@ "additionalInfo": "Additional Information", "author": "Author", "faculty": "Faculty", - "dateOfCreation": "Date of creation" + "dateOfCreation": "Date of creation", + "comments": "Comments", + "admin": { + "selectAssignee": "Select assignee" + } }, "createTicket": { "heading": "Ticket creation", "queue": "Queue", + "faculty": "Faculty", "selectQueue": "Select queue", "ticketTitle": "Ticket title", "ticketTitlePlaceholder": "Enter ticket title", diff --git a/public/locales/ua/translation.json b/public/locales/ua/translation.json index 843229b..f387bb4 100644 --- a/public/locales/ua/translation.json +++ b/public/locales/ua/translation.json @@ -27,7 +27,8 @@ "tooltip": { "report": "Тікет - скарга", "suggestion": "Тікет - пропозиція", - "question": "Тікет - питання" + "question": "Тікет - питання", + "notSelected": "Не обрано" } }, "sidebar": { @@ -50,7 +51,8 @@ "scopes": { "reportTitle": "Тікети - скарги", "questionTitle": "Тікети - питання", - "suggestionTitle": "Тікети - пропозиції" + "suggestionTitle": "Тікети - пропозиції", + "notSelectedTitle": "Не визначені Тікети" } }, "sent": { @@ -85,11 +87,16 @@ "additionalInfo": "Додаткова інформація", "author": "Автор", "faculty": "Факультет", - "dateOfCreation": "Дата створення" + "dateOfCreation": "Дата створення", + "comments": "Коментарі", + "admin": { + "selectAssignee": "Обери відповідального" + } }, "createTicket": { "heading": "Створення тікета", "queue": "Черга", + "faculty": "Факультет", "selectQueue": "Оберіть чергу", "ticketTitle": "Назва тікета", "ticketTitlePlaceholder": "Введіть назву тікета", diff --git a/src/Pages/CreateTicketForm/CreateTicketForm.tsx b/src/Pages/CreateTicketForm/CreateTicketForm.tsx index 0af6bf6..58daac5 100644 --- a/src/Pages/CreateTicketForm/CreateTicketForm.tsx +++ b/src/Pages/CreateTicketForm/CreateTicketForm.tsx @@ -6,6 +6,7 @@ import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import useTheme from "@mui/material/styles/useTheme"; +import { FacultySelect } from "./components/FacultySelect"; import { QueueSelect } from "./components/QueueSelect"; import { TicketTitleInput } from "./components/TicketTitleInput"; import { TicketBodyTextField } from "./components/TicketBodyTextField"; @@ -23,7 +24,9 @@ const CreateTicketForm: FC = () => { const facultyId = getUserFacultyId(); const [selectedOptions, setSelectedOptions] = useState([]); - const [queue, setQueue] = useState("none"); + const [queue, setQueue] = useState(-1); + const [faculty, setFaculty] = useState(facultyId); + const [formattedText, setFormattedText] = useState(""); const [createTicket] = useCreateTicketMutation(); @@ -37,16 +40,15 @@ const CreateTicketForm: FC = () => { const handleClear = (): void => { resetField("subject"); - resetField("body"); - setQueue("none"); + setValue("queue", null); + setFormattedText(""); + setQueue(-1); setSelectedOptions([]); }; const onSubmit = (data: ICreateTicketRequestBody): void => { - if (data.queue) { - createTicket({ body: JSON.stringify(data) }); - handleClear(); - } + createTicket({ body: JSON.stringify(data) }); + handleClear(); }; return ( @@ -75,15 +77,26 @@ const CreateTicketForm: FC = () => { }, }} > + - + ; + setValue: UseFormSetValue; + setFaculty: (faculty: number) => void; +} + +interface faculty { + faculty_id: number; + name: string; +} + +const FacultySelect: FC = ({ + facultyId, + register, + setValue, + faculty, + setFaculty, +}) => { + const { t } = useTranslation(); + const { palette }: IPalette = useTheme(); + + const { data, isLoading, isSuccess } = useGetFacultiesQuery({}); + + const handleChange = (event: SelectChangeEvent): void => { + const selectedFaculty: number = parseInt(event.target.value); + + setFaculty(selectedFaculty); + setValue("faculty", selectedFaculty); + }; + + useEffect(() => { + setValue("faculty", facultyId); + }, []); + + return ( + + {t("createTicket.faculty")} + + {isLoading && } + {isSuccess && ( + + )} + + + ); +}; + +export { FacultySelect }; diff --git a/src/Pages/CreateTicketForm/components/FacultySelect/index.ts b/src/Pages/CreateTicketForm/components/FacultySelect/index.ts new file mode 100644 index 0000000..0396916 --- /dev/null +++ b/src/Pages/CreateTicketForm/components/FacultySelect/index.ts @@ -0,0 +1 @@ +export { FacultySelect } from "./FacultySelect"; diff --git a/src/Pages/CreateTicketForm/components/QueueSelect/QueueSelect.tsx b/src/Pages/CreateTicketForm/components/QueueSelect/QueueSelect.tsx index c7b0827..88b0fc4 100644 --- a/src/Pages/CreateTicketForm/components/QueueSelect/QueueSelect.tsx +++ b/src/Pages/CreateTicketForm/components/QueueSelect/QueueSelect.tsx @@ -18,7 +18,7 @@ import { useGetQueueByFacultyMutation } from "../../../../store/api/api"; import IPalette from "../../../../theme/IPalette.interface"; interface QueueSelectProps { - faculty: number | null; + facultyId: number | null; queue: number | "none"; register: UseFormRegister; setValue: UseFormSetValue; @@ -26,7 +26,7 @@ interface QueueSelectProps { } const QueueSelect: FC = ({ - faculty, + facultyId, register, setValue, queue, @@ -42,13 +42,14 @@ const QueueSelect: FC = ({ const handleChange = (event: SelectChangeEvent): void => { const selectedQueue: number = parseInt(event.target.value); + setQueue(selectedQueue); setValue("queue", selectedQueue); }; useEffect(() => { - faculty && getQueues({ body: JSON.stringify({ faculty: faculty }) }); - }, [faculty, getQueues]); + facultyId && getQueues({ body: JSON.stringify({ faculty: facultyId }) }); + }, [facultyId, getQueues]); useEffect(() => { if (isSuccess) { @@ -62,7 +63,7 @@ const QueueSelect: FC = ({ useEffect(() => { setValue("queue", null); - }, [setValue]); + }, []); const menuItems: JSX.Element[] = []; let currentScope: string | null = null; @@ -97,17 +98,14 @@ const QueueSelect: FC = ({ + + + + {data.admin_list.map((assignee: assignee, index: number) => { + let isSelected = false; + + if (assignee.user_id === index + 1) { + isSelected = true; + } + + return ( + + + + ); + })} + + )} + + + ); +}; + +export { AssigneeSelect }; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/AssigneeSelect/index.ts b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/AssigneeSelect/index.ts new file mode 100644 index 0000000..871d453 --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/AssigneeSelect/index.ts @@ -0,0 +1 @@ +export { AssigneeSelect } from "./AssigneeSelect"; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/FacultySelect/FacultySelect.tsx b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/FacultySelect/FacultySelect.tsx new file mode 100644 index 0000000..620fc54 --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/FacultySelect/FacultySelect.tsx @@ -0,0 +1,85 @@ +import { FC } from "react"; + +import Box from "@mui/material/Box"; +import FormControl from "@mui/material/FormControl"; +import ListItemText from "@mui/material/ListItemText"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { SelectChangeEvent } from "@mui/material/Select"; +import useTheme from "@mui/material/styles/useTheme"; + +import { Loader } from "../../../../../../components/Loader"; + +import { useGetFacultiesQuery } from "../../../../../../store/api/api"; +import IPalette from "../../../../../../theme/IPalette.interface"; + +interface FacultySelectProps { + facultyId: number; + faculty: number; + setFaculty: (faculty: number) => void; +} + +interface faculty { + faculty_id: number; + name: string; +} + +const FacultySelect: FC = ({ + facultyId, + faculty, + setFaculty, +}) => { + const { palette }: IPalette = useTheme(); + + const { data, isLoading, isSuccess } = useGetFacultiesQuery({}); + + const handleChange = (event: SelectChangeEvent): void => { + const selectedFaculty: number = parseInt(event.target.value); + + setFaculty(selectedFaculty); + }; + + return ( + + .MuiInputBase-root > .MuiSelect-select": { + p: "8px 32px 8px 16px", + }, + }} + > + {isLoading && } + {isSuccess && ( + + )} + + + ); +}; + +export { FacultySelect }; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/FacultySelect/index.ts b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/FacultySelect/index.ts new file mode 100644 index 0000000..0396916 --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/FacultySelect/index.ts @@ -0,0 +1 @@ +export { FacultySelect } from "./FacultySelect"; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/QueueSelect/QueueSelect.tsx b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/QueueSelect/QueueSelect.tsx new file mode 100644 index 0000000..c8a03e1 --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/QueueSelect/QueueSelect.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState, FC } from "react"; +import { useTranslation } from "react-i18next"; + +import Box from "@mui/material/Box"; +import FormControl from "@mui/material/FormControl"; +import ListItemText from "@mui/material/ListItemText"; +import ListSubheader from "@mui/material/ListSubheader"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { SelectChangeEvent } from "@mui/material/Select"; +import useTheme from "@mui/material/styles/useTheme"; + +import { Loader } from "../../../../../../components/Loader"; + +import { useGetQueueByFacultyMutation } from "../../../../../../store/api/api"; +import IPalette from "../../../../../../theme/IPalette.interface"; + +interface QueueSelectProps { + facultyId: number | null; + queue: number; + setQueue: (queue: number) => void; +} + +const QueueSelect: FC = ({ facultyId, queue, setQueue }) => { + const { t } = useTranslation(); + const { palette }: IPalette = useTheme(); + + const [sortedQueues, setSortedQueues] = useState([]); + const [isFirstLoad, setIsFirstLoad] = useState(true); + + const [getQueues, { data, isSuccess, isLoading }] = + useGetQueueByFacultyMutation(); + + const handleChange = (event: SelectChangeEvent): void => { + const selectedQueue: number = parseInt(event.target.value); + + setQueue(selectedQueue); + }; + + useEffect(() => { + facultyId && getQueues({ body: JSON.stringify({ faculty: facultyId }) }); + + if (!isFirstLoad) { + setQueue(-1); + } else { + setIsFirstLoad(false); + } + }, [facultyId]); + + useEffect(() => { + if (isSuccess) { + const newSortedQueues: IQueueData[] = [...data.queues_list].sort((a, b) => + a.scope.localeCompare(b.scope) + ); + + setSortedQueues(newSortedQueues); + } + }, [isSuccess, data?.queues_list]); + + const menuItems: JSX.Element[] = []; + let currentScope: string | null = null; + + if (sortedQueues) { + for (let i = 0; i < sortedQueues.length; i++) { + const queue = sortedQueues[i]; + + const isFirstItemWithScope = queue.scope !== currentScope; + + if (isFirstItemWithScope) { + currentScope = queue.scope; + + menuItems.push( + + {currentScope} + + ); + } + + menuItems.push( + + + + ); + } + } + + return ( + + .MuiInputBase-root > .MuiSelect-select": { + p: "8px 32px 8px 16px", + }, + }} + > + + + + ); +}; + +export { QueueSelect }; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/QueueSelect/index.ts b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/QueueSelect/index.ts new file mode 100644 index 0000000..b3b7424 --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/QueueSelect/index.ts @@ -0,0 +1 @@ +export { QueueSelect } from "./QueueSelect"; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/StatusSelect/StatusSelect.tsx b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/StatusSelect/StatusSelect.tsx new file mode 100644 index 0000000..7049421 --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/StatusSelect/StatusSelect.tsx @@ -0,0 +1,80 @@ +import { FC } from "react"; + +import Box from "@mui/material/Box"; +import FormControl from "@mui/material/FormControl"; +import ListItemText from "@mui/material/ListItemText"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { SelectChangeEvent } from "@mui/material/Select"; +import useTheme from "@mui/material/styles/useTheme"; + +import { Loader } from "../../../../../../components/Loader"; + +import { useGetStatusesQuery } from "../../../../../../store/api/api"; +import IPalette from "../../../../../../theme/IPalette.interface"; + +interface StatusSelectProps { + status: number; + setStatus: (faculty: number) => void; +} + +interface status { + status_id: number; + name: string; +} + +const StatusSelect: FC = ({ status, setStatus }) => { + const { palette }: IPalette = useTheme(); + + const { data, isLoading, isSuccess } = useGetStatusesQuery({}); + + const handleChange = (event: SelectChangeEvent): void => { + const selectedStatus: number = parseInt(event.target.value); + + setStatus(selectedStatus); + }; + + return ( + + .MuiInputBase-root > .MuiSelect-select": { + p: "8px 32px 8px 16px", + }, + }} + > + {isLoading && } + {isSuccess && ( + + )} + + + ); +}; + +export { StatusSelect }; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/components/StatusSelect/index.ts b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/StatusSelect/index.ts new file mode 100644 index 0000000..51cf38e --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/components/StatusSelect/index.ts @@ -0,0 +1 @@ +export { StatusSelect } from "./StatusSelect"; diff --git a/src/Pages/FullTicketInfo/components/FullTicketHeader/index.ts b/src/Pages/FullTicketInfo/components/FullTicketHeader/index.ts new file mode 100644 index 0000000..9ee2afd --- /dev/null +++ b/src/Pages/FullTicketInfo/components/FullTicketHeader/index.ts @@ -0,0 +1 @@ +export { FullTicketHeader } from "./FullTicketHeader"; diff --git a/src/Pages/Layout/components/Sidebar/components/SidebarActions/SidebarActions.tsx b/src/Pages/Layout/components/Sidebar/components/SidebarActions/SidebarActions.tsx index aa1f3ef..6130edb 100644 --- a/src/Pages/Layout/components/Sidebar/components/SidebarActions/SidebarActions.tsx +++ b/src/Pages/Layout/components/Sidebar/components/SidebarActions/SidebarActions.tsx @@ -9,7 +9,7 @@ import Box from "@mui/material/Box"; import useTheme from "@mui/material/styles/useTheme"; import { GeneralActions } from "./components/GeneralActions"; -import { AdditionActions } from "./components/AdditionActions"; +// import { AdditionActions } from "./components/AdditionActions"; import { VerticalDivider } from "../../../../../../components/VerticalDivider"; import { endpoints } from "../../../../../../constants"; @@ -47,10 +47,10 @@ const SidebarActions: FC = () => { setSelectedKey={setSelectedKey} /> - + /> */} = ({ const [open, setOpen] = useState(false); - const countOfNotification = 0; + // const countOfNotification = 0; const handleClick = (): void => { setOpen(!open); @@ -53,16 +53,16 @@ const GeneralActions: FC = ({ setSelectedKey(key); }; - function notificationsLabel(count: number): string { - if (count === 0) { - return "no notifications"; - } - if (count > 99) { - return "more than 99 notifications"; - } + // function notificationsLabel(count: number): string { + // if (count === 0) { + // return "no notifications"; + // } + // if (count > 99) { + // return "more than 99 notifications"; + // } - return `${count} notifications`; - } + // return `${count} notifications`; + // } useEffect(() => { setOpen(false); @@ -114,7 +114,7 @@ const GeneralActions: FC = ({ selectedKey={selectedKey} handleListItemClick={handleListItemClick} /> - + {/* = ({ )} - + */} = ({ isSuccess, option, userId, + assignee, }) => { const { t, i18n } = useTranslation(); @@ -114,6 +116,7 @@ const MyTicketPage: FC = ({ const data: { creator?: number | boolean | undefined; + assignee?: number; start_page: number; faculty: number | null; status: number[]; @@ -128,6 +131,10 @@ const MyTicketPage: FC = ({ data.creator = userId; } + if (assignee) { + data.assignee = assignee; + } + return data; }, [facultyId, searchParams]); diff --git a/src/Pages/Queue/Queue.tsx b/src/Pages/Queue/Queue.tsx index c900e74..1e5dbbd 100644 --- a/src/Pages/Queue/Queue.tsx +++ b/src/Pages/Queue/Queue.tsx @@ -7,11 +7,11 @@ import { SerializedError } from "@reduxjs/toolkit"; import Box from "@mui/material/Box"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; -import { Button, useTheme } from "@mui/material"; +import { useTheme } from "@mui/material"; -import EditIcon from "@mui/icons-material/Edit"; +// import EditIcon from "@mui/icons-material/Edit"; -import { EditQueuesPopup } from "./components/EditQueuesPopup"; +// import { EditQueuesPopup } from "./components/EditQueuesPopup"; import { Scope } from "./components/Scope"; import IPalette from "../../theme/IPalette.interface"; @@ -49,7 +49,7 @@ const Queue: FC = () => { ? searchParamOrder.split(",").map(item => Number(item)) : []; - const [open, setOpen] = useState(false); + // const [open, setOpen] = useState(false); const [currentScope, setCurrentScope] = useState(null); const [scopesList, setScopesList] = useState([ { @@ -73,12 +73,20 @@ const Queue: FC = () => { title: t("queue.scopes.suggestionTitle"), queues: [], }, + { + id: 4, + order: queue[3] || 4, + name: "Not defined", + title: t("queue.scopes.notSelectedTitle"), + queues: [], + }, ]); const mapScopeToIndex: { [key: string]: number } = { Reports: 0, "Q/A": 1, Suggestion: 2, + "Not defined": 3, }; const [getQueues] = useGetQueueByFacultyMutation({}); @@ -109,15 +117,15 @@ const Queue: FC = () => { return a.order - b.order; }; - const handleOpenDialog = () => { - setOpen(true); - }; + // const handleOpenDialog = () => { + // setOpen(true); + // }; return ( {t("queue.heading")} - + */} { /> ))} - + {/* */} ); }; diff --git a/src/Pages/Queue/components/Scope/Scope.tsx b/src/Pages/Queue/components/Scope/Scope.tsx index 1da1f14..d0e8ab8 100644 --- a/src/Pages/Queue/components/Scope/Scope.tsx +++ b/src/Pages/Queue/components/Scope/Scope.tsx @@ -119,7 +119,7 @@ const Scope: FC = ({ - {!!scope.queues.length && ( + {!!scope.queues.length && scope.name !== "Not defined" && ( )} diff --git a/src/Pages/Queue/components/Scope/components/ScopeTicketList/ScopeTicketList.tsx b/src/Pages/Queue/components/Scope/components/ScopeTicketList/ScopeTicketList.tsx index b8edd26..f03db6b 100644 --- a/src/Pages/Queue/components/Scope/components/ScopeTicketList/ScopeTicketList.tsx +++ b/src/Pages/Queue/components/Scope/components/ScopeTicketList/ScopeTicketList.tsx @@ -20,6 +20,15 @@ interface ScopeTicketListProps { queues: number[]; } +interface RequestQueuesParams { + scope?: string; + queue?: number[]; + assignee?: number; + status?: number[]; + items_count?: number; + start_page?: number; +} + const ScopeTicketList: FC = ({ scope, queues }) => { const { palette }: IPalette = useTheme(); @@ -63,13 +72,20 @@ const ScopeTicketList: FC = ({ scope, queues }) => { container.scrollTop = 0; } } else { - const requestParams = { - scope: scope, - queue: queues, + const requestParams: RequestQueuesParams = { + assignee: -1, + status: [1], items_count: Math.floor(window.innerHeight / 200), start_page: isQueuesChanged ? 1 : currentPage, }; + if (scope === "Not defined") { + requestParams.queue = [-1]; + } else { + requestParams.scope = scope; + requestParams.queue = queues; + } + axios({ method: "POST", url: "https://burrito.tres.cyberbydlo.com/admin/tickets/ticket_list", diff --git a/src/Pages/Received/Received.tsx b/src/Pages/Received/Received.tsx index e084dc5..e667289 100644 --- a/src/Pages/Received/Received.tsx +++ b/src/Pages/Received/Received.tsx @@ -17,7 +17,7 @@ const Received: FC = () => { isLoading={isLoading} isSuccess={isSuccess} option={"tickets"} - userId={userId} + assignee={userId} /> ); }; diff --git a/src/components/Action/Action.tsx b/src/components/Action/Action.tsx new file mode 100644 index 0000000..7ca28de --- /dev/null +++ b/src/components/Action/Action.tsx @@ -0,0 +1,79 @@ +import { ForwardRefExoticComponent, RefAttributes, forwardRef } from "react"; +import { NavLink } from "react-router-dom"; + +import Box from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material"; + +import IPalette from "../../theme/IPalette.interface"; +import { endpoints } from "../../constants"; +import useRandomNickColor from "../../shared/hooks/useRandomNickColor"; +import { useFormatDate } from "../../shared/hooks"; + +export type IAction = { + action_id: 1; + author: { + user_id: 2; + firstname: string; + lastname: string; + login: string; + faculty: { faculty_id: number; name: string }; + group: { group_id: number; name: string }; + }; + creation_date: string; + field_name: string; + new_value: string; + old_value: string; + ticket_id: number; + type_: "action"; +}; + +interface ActionProps { + action: IAction; +} + +const Action: ForwardRefExoticComponent< + Omit & RefAttributes +> = forwardRef(({ action }, ref) => { + const { palette }: IPalette = useTheme(); + + const color = useRandomNickColor(); + const formattedDate: string = useFormatDate(action.creation_date, "date"); + + return ( + + + + + + {`${action.author.firstname} ${action.author.lastname}`} + + {` on ${formattedDate} changed the ${action.field_name} from `} + {action.old_value} + {` to `} + {action.new_value} + + + + + ); +}); + +export { Action }; diff --git a/src/components/Action/index.ts b/src/components/Action/index.ts new file mode 100644 index 0000000..ced2207 --- /dev/null +++ b/src/components/Action/index.ts @@ -0,0 +1 @@ +export { Action } from "./Action"; diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx new file mode 100644 index 0000000..4abd553 --- /dev/null +++ b/src/components/Comment/Comment.tsx @@ -0,0 +1,249 @@ +import { + Dispatch, + ForwardRefExoticComponent, + RefAttributes, + SetStateAction, + forwardRef, +} from "react"; +import { Link } from "react-router-dom"; +import { MutationTrigger } from "@reduxjs/toolkit/dist/query/react/buildHooks"; +import { + BaseQueryFn, + FetchArgs, + FetchBaseQueryError, + MutationDefinition, +} from "@reduxjs/toolkit/dist/query"; + +import Box from "@mui/material/Grid"; +import Avatar from "@mui/material/Avatar"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material"; + +import ReplyIcon from "@mui/icons-material/Reply"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; + +import IPalette from "../../theme/IPalette.interface"; +import { endpoints } from "../../constants"; +import { getUserId } from "../../shared/functions/getLocalStorageData"; +import { useFormatDate } from "../../shared/hooks"; +import useRandomNickColor from "../../shared/hooks/useRandomNickColor"; +import { + EditedComment, + RepliedComment, +} from "../../Pages/FullTicketInfo/components/FullTicketComments/FullTicketComments"; + +export type IComment = { + comment_id: number; + author: { + user_id: 2; + firstname: string; + lastname: string; + login: string; + faculty: { faculty_id: number; name: string }; + group: { group_id: number; name: string }; + }; + body: string; + creation_date: string; + type_: "comment"; +}; + +interface CommentProps { + comment: IComment; + setEditedComment: Dispatch>; + setRepliedComment: Dispatch>; + deleteComment: MutationTrigger< + MutationDefinition< + any, + BaseQueryFn, + never, + any, + "api" + > + >; +} + +const Comment: ForwardRefExoticComponent< + Omit & RefAttributes +> = forwardRef( + ({ comment, setEditedComment, deleteComment, setRepliedComment }, ref) => { + const { palette }: IPalette = useTheme(); + + const userId = getUserId(); + const isMyComment = userId === comment.author.user_id; + + const color = useRandomNickColor(); + const formattedDate: string = useFormatDate(comment.creation_date, "time"); + + const getCommentBody = () => { + let body = ""; + + if (comment?.body) { + body = comment.body; + } else { + body = "Something went wrong"; + } + + return body; + }; + + const removeComment = () => { + deleteComment({ + body: JSON.stringify({ comment_id: comment.comment_id }), + }); + }; + + const changeComment = () => { + setEditedComment({ id: comment.comment_id, body: comment.body }); + setRepliedComment(null); + }; + + const handleReply = () => { + setRepliedComment({ + id: comment.comment_id, + body: comment.body, + fullName: `${comment.author.firstname} ${comment.author.lastname}`, + }); + setEditedComment(null); + }; + + return ( + + {!isMyComment && ( + + + + )} + + + {!isMyComment && ( + + + {`${comment.author.firstname} ${comment.author.lastname}`} + + + + + + )} + + {getCommentBody()} + {isMyComment && ( + <> + + + + + + + + )} + + {formattedDate} + + + + + + + ); + } +); + +export { Comment }; diff --git a/src/components/Comment/index.ts b/src/components/Comment/index.ts new file mode 100644 index 0000000..435c7a6 --- /dev/null +++ b/src/components/Comment/index.ts @@ -0,0 +1 @@ +export { Comment } from "./Comment"; diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index b0d2b16..774be01 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -1,22 +1,29 @@ +import { FC } from "react"; import { useTranslation } from "react-i18next"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; -const Loader = () => { +interface LoaderProps { + size?: "small" | "large"; +} + +const Loader: FC = ({ size = "large" }) => { const { t } = useTranslation(); return ( - {t("common.loading")} + + {t("common.loading")} + ); }; diff --git a/src/components/SimpleTicket/SimpleTicket.tsx b/src/components/SimpleTicket/SimpleTicket.tsx index 294b88f..f00198b 100644 --- a/src/components/SimpleTicket/SimpleTicket.tsx +++ b/src/components/SimpleTicket/SimpleTicket.tsx @@ -69,10 +69,12 @@ const SimpleTicket: ForwardRefExoticComponent< {ticket.subject} - + {ticket?.queue?.name && ( + + )} = ({ anchorOrigin={{ vertical: "bottom", horizontal: "right" }} open={open} onClose={handleClose} - autoHideDuration={3000} + autoHideDuration={2000} TransitionComponent={transition} key={transition ? transition.name : ""} /> diff --git a/src/components/Ticket/Ticket.tsx b/src/components/Ticket/Ticket.tsx index 2fb093f..9cab51a 100644 --- a/src/components/Ticket/Ticket.tsx +++ b/src/components/Ticket/Ticket.tsx @@ -58,7 +58,7 @@ const Ticket: FC = ({ ticket, ticketsPerRow }) => { setOpen(true); }; - const handleClose = (reason: string) => { + const handleClose = (_: React.SyntheticEvent | Event, reason: string) => { if (reason === "timeout") { setOpen(false); } diff --git a/src/components/TicketRow/TicketRow.tsx b/src/components/TicketRow/TicketRow.tsx index 1ad5f70..2dd3f09 100644 --- a/src/components/TicketRow/TicketRow.tsx +++ b/src/components/TicketRow/TicketRow.tsx @@ -57,7 +57,7 @@ const TicketRow: FC = ({ const color: string = checkStatus(ticket.status.name); const { icon, tooltipText }: { icon: JSX.Element; tooltipText: string } = - useCheckScope(ticket.queue.scope); + useCheckScope(ticket.queue?.scope); const formattedDate: string = ticket?.date && useFormatDate(ticket.date); const handleClick = (event: MouseEvent): void => { diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index d1bbb0a..16a665e 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -11,7 +11,10 @@ import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query"; import { SerializedError } from "@reduxjs/toolkit"; import { useNavigate } from "react-router-dom"; -import { getAccessToken } from "../shared/functions/getLocalStorageData"; +import { + getAccessToken, + getIsTokensExpired, +} from "../shared/functions/getLocalStorageData"; import { useLoginMutation } from "../store/api/api"; import { useGetProfileMutation } from "../store/api/profile/profile.api"; import { decodeJwt } from "../shared/functions"; @@ -64,7 +67,8 @@ const AuthContext = createContext({} as AuthContextProps); export default AuthContext; export const AuthProvider = ({ children }: { children: ReactNode }) => { - const [isAuth, setIsAuth] = useState(!!localStorage.getItem("access-token")); + const IsTokensExpired = getIsTokensExpired(); + const [isAuth, setIsAuth] = useState(!IsTokensExpired); // ============================================= const [authToken, setAuthToken] = useState(getAccessToken()); // const [user, setUser] = useState(getAccessToken()); diff --git a/src/shared/functions/getLocalStorageData.ts b/src/shared/functions/getLocalStorageData.ts index 287cc45..9ffda52 100644 --- a/src/shared/functions/getLocalStorageData.ts +++ b/src/shared/functions/getLocalStorageData.ts @@ -1,3 +1,6 @@ +import jwtDecode from "jwt-decode"; +import { IJwtDecodeData } from "../../store/api/useBaseQuery"; + export const getUserLogin = () => localStorage.getItem("login"); export const getUserId = () => Number(localStorage.getItem("user-id")); @@ -6,6 +9,32 @@ export const getAccessToken = () => localStorage.getItem("access-token"); export const getRefreshToken = () => localStorage.getItem("refresh-token"); +export const getIsTokensExpired = () => { + let isAccessExpired = true; + let isRefreshExpired = true; + + const accessToken = getAccessToken(); + const refreshToken = getRefreshToken(); + + if (accessToken) { + const decodeData: IJwtDecodeData = jwtDecode(accessToken); + + if (Date.now() < decodeData.exp * 1000) { + isAccessExpired = false; + } + } + + if (refreshToken) { + const decodeData: IJwtDecodeData = jwtDecode(refreshToken); + + if (Date.now() < decodeData.exp * 1000) { + isRefreshExpired = false; + } + } + + return isAccessExpired || isRefreshExpired; +}; + export const getUserRole = () => localStorage.getItem("role"); export const getUserFacultyId = () => diff --git a/src/shared/hooks/useCheckScope.tsx b/src/shared/hooks/useCheckScope.tsx index f57fe5d..f0a821e 100644 --- a/src/shared/hooks/useCheckScope.tsx +++ b/src/shared/hooks/useCheckScope.tsx @@ -3,6 +3,7 @@ import SvgIcon from "@mui/material/SvgIcon"; import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import FlagOutlinedIcon from "@mui/icons-material/FlagOutlined"; import HandshakeOutlinedIcon from "@mui/icons-material/HandshakeOutlined"; +import BuildIcon from "@mui/icons-material/Build"; import { useTranslation } from "react-i18next"; @@ -41,10 +42,10 @@ const useCheckScope = (scope: string) => { return { icon: ( - + ), - tooltipText: t("common.tooltip.suggestion"), + tooltipText: t("common.tooltip.notSelected"), }; } }; diff --git a/src/shared/hooks/useFormatDate.ts b/src/shared/hooks/useFormatDate.ts index 35ea621..59aeb25 100644 --- a/src/shared/hooks/useFormatDate.ts +++ b/src/shared/hooks/useFormatDate.ts @@ -1,14 +1,35 @@ import { useTranslation } from "react-i18next"; -const useFormatDate = (date: string): string => { - if (!date) { +type DateFormat = "time" | "date" | "full"; + +const useFormatDate = ( + date: string, + dateFormat: DateFormat = "date" +): string => { + if (!date || !dateFormat) { return "date"; } const { t } = useTranslation(); - const [year, month, day] = date.slice(0, 10).split("-"); - return `${day} ${t(`month.${month}`)} ${year}`; + if (dateFormat === "time") { + const time = date.slice(10, 16); + + return time; + } else if (dateFormat === "date") { + const [year, month, day] = date.slice(0, 10).split("-"); + const formattedDate = `${day} ${t(`month.${month}`)} ${year}`; + + return formattedDate; + } else if (dateFormat === "full") { + const time = date.slice(10, 16); + const [year, month, day] = date.slice(0, 10).split("-"); + const formattedDate = `${day} ${t(`month.${month}`)} ${year}`; + + return `${time} ${formattedDate}`; + } + + return "date"; }; export { useFormatDate }; diff --git a/src/shared/hooks/useRandomNickColor.ts b/src/shared/hooks/useRandomNickColor.ts new file mode 100644 index 0000000..4a94cd1 --- /dev/null +++ b/src/shared/hooks/useRandomNickColor.ts @@ -0,0 +1,6 @@ +const useRandomNickColor = () => { + const hue = Math.random() * 360; + return `hsl(${hue},70%,65%)`; +}; + +export default useRandomNickColor; diff --git a/src/store/api/admin/admin.api.ts b/src/store/api/admin/admin.api.ts index 130ad30..b38f441 100644 --- a/src/store/api/admin/admin.api.ts +++ b/src/store/api/admin/admin.api.ts @@ -11,7 +11,14 @@ export const adminApi = api.injectEndpoints({ }), adminShowTicket: builder.mutation({ query: ({ body }) => ({ - url: "admin/tickets/show", + url: "tickets/show", + method: "POST", + body, + }), + }), + adminUpdateTicket: builder.mutation({ + query: ({ body }) => ({ + url: "/admin/tickets/update", method: "POST", body, }), @@ -19,5 +26,8 @@ export const adminApi = api.injectEndpoints({ }), }); -export const { useGetAdminTicketsMutation, useAdminShowTicketMutation } = - adminApi; +export const { + useGetAdminTicketsMutation, + useAdminShowTicketMutation, + useAdminUpdateTicketMutation, +} = adminApi; diff --git a/src/store/api/api.ts b/src/store/api/api.ts index 0a49de6..acc5f16 100644 --- a/src/store/api/api.ts +++ b/src/store/api/api.ts @@ -18,6 +18,12 @@ export const api = createApi({ getStatuses: builder.query({ query: () => "/meta/get_statuses", }), + getAdmins: builder.mutation({ + query: () => ({ + url: "/meta/get_admins", + method: "POST", + }), + }), login: builder.mutation({ query: ({ body }) => ({ url: "/auth/password/login", @@ -46,6 +52,7 @@ export const { useGetVersionQuery, useGetUpdatesQuery, useLoginMutation, + useGetAdminsMutation, useGetFacultiesQuery, useGetStatusesQuery, useGetQueueByFacultyMutation, diff --git a/src/store/api/comments/comments.api.ts b/src/store/api/comments/comments.api.ts new file mode 100644 index 0000000..bd2a25c --- /dev/null +++ b/src/store/api/comments/comments.api.ts @@ -0,0 +1,45 @@ +import { api } from "../api"; + +export const commentsApi = api.injectEndpoints({ + endpoints: builder => ({ + createComment: builder.mutation({ + query: ({ body }) => ({ + url: "/comments/create", + method: "POST", + body, + }), + }), + editComment: builder.mutation({ + query: ({ body }) => ({ + url: "/comments/edit", + method: "POST", + body, + }), + }), + deleteComment: builder.mutation({ + query: ({ body }) => ({ + url: "/comments/delete", + method: "DELETE", + body, + }), + }), + getFullHistory: builder.mutation({ + query: ({ body }) => ({ + url: "/tickets/full_history", + method: "POST", + body, + }), + }), + getComment: builder.query({ + query: () => "/comments/", + }), + }), +}); + +export const { + useCreateCommentMutation, + useEditCommentMutation, + useDeleteCommentMutation, + useGetCommentQuery, + useGetFullHistoryMutation, +} = commentsApi; diff --git a/src/store/api/useBaseQuery.ts b/src/store/api/useBaseQuery.ts index 46332d9..29200f7 100644 --- a/src/store/api/useBaseQuery.ts +++ b/src/store/api/useBaseQuery.ts @@ -13,7 +13,7 @@ import { } from "../../shared/functions/getLocalStorageData"; import { endpoints } from "../../constants"; -interface IJwtDecodeData { +export interface IJwtDecodeData { role: string; token_id: string; token_type: string;