diff --git a/src/App.js b/src/App.js index 67b53b2e..8c987891 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,8 @@ import { Navigate, } from "react-router-dom"; +import { getNotification } from "@redux/outstaffingSlice"; + import AuthForPartners from "./pages/AuthForPartners/AuthForPartners"; import AuthForDevelopers from "./pages/AuthForDevelopers/AuthForDevelopers"; import { TrackerIntro } from "./pages/TrackerIntro/TrackerIntro" @@ -42,12 +44,15 @@ import Blog from "./pages/Blog/Blog"; import { ProjectTracker } from "./pages/ProjectTracker/ProjectTracker"; import { FrequentlyAskedQuestions } from "./pages/FrequentlyAskedQuestions/FrequentlyAskedQuestions"; import { FrequentlyAskedQuestion } from "./pages/FrequentlyAskedQuestion/FrequentlyAskedQuestion"; +import Notification from "@components/Notification/Notification"; +import { useSelector } from "react-redux"; import "./assets/global.scss"; import "./assets/fonts/stylesheet.css"; import "bootstrap/dist/css/bootstrap.min.css"; const App = () => { + const notification = useSelector(getNotification) return ( <> @@ -130,6 +135,9 @@ const App = () => { } /> + {notification.show && + + } ); }; diff --git a/src/assets/icons/archiveNotification.svg b/src/assets/icons/archiveNotification.svg new file mode 100644 index 00000000..c52c04a9 --- /dev/null +++ b/src/assets/icons/archiveNotification.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/copyNotification.svg b/src/assets/icons/copyNotification.svg new file mode 100644 index 00000000..4d94ea9b --- /dev/null +++ b/src/assets/icons/copyNotification.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/errorNotification.svg b/src/assets/icons/errorNotification.svg new file mode 100644 index 00000000..af935231 --- /dev/null +++ b/src/assets/icons/errorNotification.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/successNotification.svg b/src/assets/icons/successNotification.svg new file mode 100644 index 00000000..1e3c3af9 --- /dev/null +++ b/src/assets/icons/successNotification.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/components/Modal/AcceptModal/AcceptModal.jsx b/src/components/Modal/AcceptModal/AcceptModal.jsx new file mode 100644 index 00000000..a2b3750b --- /dev/null +++ b/src/components/Modal/AcceptModal/AcceptModal.jsx @@ -0,0 +1,33 @@ +import React from "react"; + +import close from "assets/icons/closeProjectPersons.svg"; + +import "./acceptModal.scss"; + +export const AcceptModal = ({ closeModal, agreeHandler }) => { + return ( +
+
+

+ Вы точно хотите переместить задачу в архив? +

+
+ + +
+ close +
+
+ ); +}; + +export default AcceptModal; diff --git a/src/components/Modal/AcceptModal/acceptModal.scss b/src/components/Modal/AcceptModal/acceptModal.scss new file mode 100644 index 00000000..5f1abce2 --- /dev/null +++ b/src/components/Modal/AcceptModal/acceptModal.scss @@ -0,0 +1,61 @@ +.backDrop { + height: 100%; + width: 100%; + background-color: rgba(0, 0, 0, 0.11); + position: fixed; + top: 0; + left: 0; + display: flex; + z-index: 11; + align-items: center; + justify-content: center; + + .acceptModal { + border-radius: 20px; + background: linear-gradient(180deg, #FFF 0%, #EBEBEB 100%); + padding: 50px 34px 25px; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + row-gap: 25px; + + &__title { + max-width: 260px; + font-size: 18px; + font-weight: 500; + text-align: center; + margin-bottom: 0; + } + + &__buttons { + display: flex; + column-gap: 20px; + + button { + min-width: 90px; + height: 37px; + border-radius: 44px; + border: none; + font-size: 14px; + font-weight: 500; + color: white; + } + + .agree { + background: #52B709; + } + + .cancel { + background: #B0BABF; + } + } + + &__close { + position: absolute; + top: 15px; + right: 22px; + cursor: pointer; + } + } +} diff --git a/src/components/Modal/Tracker/ModalTicket/ModalTicket.jsx b/src/components/Modal/Tracker/ModalTicket/ModalTicket.jsx index 210445e3..ed830c61 100644 --- a/src/components/Modal/Tracker/ModalTicket/ModalTicket.jsx +++ b/src/components/Modal/Tracker/ModalTicket/ModalTicket.jsx @@ -20,6 +20,9 @@ import { import { apiRequest } from "@api/request"; +import { useNotification } from "@hooks/useNotification"; + +import AcceptModal from "@components/Modal/AcceptModal/AcceptModal"; import TrackerModal from "@components/Modal/Tracker/TrackerModal/TrackerModal"; import TrackerTaskComment from "@components/TrackerTaskComment/TrackerTaskComment"; @@ -82,6 +85,8 @@ export const ModalTiсket = ({ const [correctProjectUsers, setCorrectProjectUsers] = useState(projectUsers); const [executorId, setExecutorId] = useState(task.executor_id); const profileInfo = useSelector(getProfileInfo); + const [acceptModalOpen, setAcceptModalOpen] = useState(false); + const { showNotification } = useNotification(); function deleteTask() { apiRequest("/task/update-task", { @@ -93,9 +98,18 @@ export const ModalTiсket = ({ }).then(() => { setActive(false); dispatch(setProjectBoardFetch(projectId)); + showNotification({ + show: true, + text: "Задача успешно была перемещена в архив", + type: "archive", + }); }); } + function archiveTask() { + setAcceptModalOpen(true); + } + function editTask() { apiRequest("/task/update-task", { method: "PUT", @@ -416,6 +430,11 @@ export const ModalTiсket = ({ navigator.clipboard.writeText( `https://itguild.info/tracker/task/${task.id}` ); + showNotification({ + show: true, + text: "Ссылка скопирована в буфер обмена", + type: "copy", + }); } function selectDeadLine(date) { @@ -430,6 +449,10 @@ export const ModalTiсket = ({ }); } + function closeAcceptModal() { + setAcceptModalOpen(false); + } + return (

ссылка на задачу

-
+

в архив

@@ -819,8 +842,13 @@ export const ModalTiсket = ({
+ {acceptModalOpen && ( + + )} - { const [startDate, setStartDate] = useState(null); const [uploadedFile, setUploadedFile] = useState(null); const [taskFiles, setTaskFiles] = useState([]); + const [acceptModalOpen, setAcceptModalOpen] = useState(false); useEffect(() => { apiRequest(`/task/get-task?task_id=${ticketId.id}`).then((taskInfo) => { @@ -161,6 +163,10 @@ export const TicketFullScreen = () => { }); } + function archiveTask() { + setAcceptModalOpen(true); + } + function editTask() { apiRequest("/task/update-task", { method: "PUT", @@ -450,6 +456,10 @@ export const TicketFullScreen = () => { }); } + function closeAcceptModal() { + setAcceptModalOpen(false); + } + return (
@@ -976,7 +986,7 @@ export const TicketFullScreen = () => { link

ссылка на задачу

-
+
arch

в архив

@@ -990,6 +1000,9 @@ export const TicketFullScreen = () => { )}
+ {acceptModalOpen && ( + + )}
); diff --git a/src/components/Modal/Tracker/TrackerModal/TrackerModal.jsx b/src/components/Modal/Tracker/TrackerModal/TrackerModal.jsx index a640dc53..32ccdad0 100644 --- a/src/components/Modal/Tracker/TrackerModal/TrackerModal.jsx +++ b/src/components/Modal/Tracker/TrackerModal/TrackerModal.jsx @@ -23,6 +23,8 @@ import { urlForLocal } from "@utils/helper"; import { apiRequest } from "@api/request"; +import { useNotification } from "@hooks/useNotification"; + import BaseButton from "@components/Common/BaseButton/BaseButton"; import ModalLayout from "@components/Common/ModalLayout/ModalLayout"; @@ -67,9 +69,11 @@ export const TrackerModal = ({ const [correctProjectUsers, setCorrectProjectUsers] = useState([]); const [selectColumnPriorityOpen, setSelectColumnPriorityOpen] = useState(false); + const { showNotification } = useNotification(); function createTab() { if (!valueColumn) { + showNotification({ show: true, text: "Введите название", type: "error" }); return; } @@ -91,6 +95,11 @@ export const TrackerModal = ({ function createTiket() { if (!valueTiket || !descriptionTicket) { + showNotification({ + show: true, + text: "Введите название и описание", + type: "error", + }); return; } @@ -106,25 +115,38 @@ export const TrackerModal = ({ priority: priorityTask, }, }).then((res) => { - if (selectedExecutorTask.user_id) { - apiRequest("/task/update-task", { - method: "PUT", - data: { - task_id: res.id, - executor_id: selectedExecutorTask.user_id, - }, - }).then(() => { - dispatch(setProjectBoardFetch(projectBoard.id)); + if (res.status === 500) { + showNotification({ + show: true, + text: "Задача с таким именем уже существует", + type: "error", + }); + } else { + if (selectedExecutorTask.user_id) { + apiRequest("/task/update-task", { + method: "PUT", + data: { + task_id: res.id, + executor_id: selectedExecutorTask.user_id, + }, + }).then(() => { + dispatch(setProjectBoardFetch(projectBoard.id)); + setActive(false); + setValueTiket(""); + setDescriptionTicket("Описание задачи"); + setSelectedExecutorTask("Выберите исполнителя задачи"); + }); + } else { setActive(false); setValueTiket(""); setDescriptionTicket("Описание задачи"); - setSelectedExecutorTask("Выберите исполнителя задачи"); + dispatch(setProjectBoardFetch(projectBoard.id)); + } + showNotification({ + show: true, + text: "Задача создана", + type: "success", }); - } else { - setActive(false); - setValueTiket(""); - setDescriptionTicket("Описание задачи"); - dispatch(setProjectBoardFetch(projectBoard.id)); } }); } @@ -204,10 +226,18 @@ export const TrackerModal = ({ status: 19, }, }).then((res) => { - const result = { ...res, columns: [] }; - dispatch(setProject(result)); - setActive(false); - setNameProject(""); + if (!Array.isArray(res.name)) { + const result = { ...res, columns: [] }; + dispatch(setProject(result)); + setActive(false); + setNameProject(""); + } else { + showNotification({ + show: true, + text: "Проект с таким именем уже существует", + type: "error", + }); + } }); } } diff --git a/src/components/Notification/Notification.jsx b/src/components/Notification/Notification.jsx new file mode 100644 index 00000000..6ff81d78 --- /dev/null +++ b/src/components/Notification/Notification.jsx @@ -0,0 +1,40 @@ +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { closeNotification, getNotification } from "@redux/outstaffingSlice"; + +import archive from "assets/icons/archiveNotification.svg"; +import close from "assets/icons/closeProjectPersons.svg"; +import copy from "assets/icons/copyNotification.svg"; +import error from "assets/icons/errorNotification.svg"; +import success from "assets/icons/successNotification.svg"; + +import "./notification.scss"; + +const images = { + archive: archive, + error: error, + copy: copy, + success: success, +}; + +export const Notification = () => { + const dispatch = useDispatch(); + const notificationInfo = useSelector(getNotification); + return ( +
+
+ img +

{notificationInfo.text}

+
+ dispatch(closeNotification())} + className="notification__close" + src={close} + alt="close" + /> +
+ ); +}; + +export default Notification; diff --git a/src/components/Notification/notification.scss b/src/components/Notification/notification.scss new file mode 100644 index 00000000..cab02ae3 --- /dev/null +++ b/src/components/Notification/notification.scss @@ -0,0 +1,35 @@ +.notification { + border-radius: 40px; + background: linear-gradient(180deg, #FFF 0%, #EBEBEB 100%); + padding: 20px 82px 17px 27px; + position: fixed; + bottom: 25px; + right: 25px; + z-index: 20; + + &__info { + display: flex; + column-gap: 10px; + align-items: center; + + h2 { + max-width: 194px; + font-weight: 500; + font-size: 16px; + margin-bottom: 0; + } + + img { + max-width: 24px; + } + } + + &__close { + cursor: pointer; + position: absolute; + top: 15px; + right: 25px; + width: 15px; + height: 15px; + } +} diff --git a/src/components/ProjectTiket/ProjectTiket.jsx b/src/components/ProjectTiket/ProjectTiket.jsx index cefb1f1e..7b2f4f59 100644 --- a/src/components/ProjectTiket/ProjectTiket.jsx +++ b/src/components/ProjectTiket/ProjectTiket.jsx @@ -6,6 +6,9 @@ import { deleteProject, modalToggle } from "@redux/projectsTrackerSlice"; import { apiRequest } from "@api/request"; +import { useNotification } from "@hooks/useNotification"; + +import AcceptModal from "@components/Modal/AcceptModal/AcceptModal"; import { ModalSelect } from "@components/Modal/ModalSelect/ModalSelect"; import TrackerModal from "@components/Modal/Tracker/TrackerModal/TrackerModal"; @@ -19,7 +22,9 @@ import "./projectTiket.scss"; export const ProjectTiket = ({ project, index }) => { const [modalSelect, setModalSelect] = useState(false); const [modalAdd, setModalAdd] = useState(false); + const [acceptModalOpen, setAcceptModalOpen] = useState(false); const dispatch = useDispatch(); + const { showNotification } = useNotification(); useEffect(() => { initListeners(); @@ -49,6 +54,11 @@ export const ProjectTiket = ({ project, index }) => { }, }).then(() => { dispatch(deleteProject(project)); + showNotification({ + show: true, + text: "Проект успешно была перемещена в архив", + type: "archive", + }); }); } @@ -58,6 +68,10 @@ export const ProjectTiket = ({ project, index }) => { ); } + function closeAcceptModal() { + setAcceptModalOpen(false); + } + return (
{project.name} @@ -98,7 +112,12 @@ export const ProjectTiket = ({ project, index }) => {

ссылка на проект

-
+
{ + setModalSelect(false); + setAcceptModalOpen(true); + }} + >

в архив

@@ -108,6 +127,12 @@ export const ProjectTiket = ({ project, index }) => {
+ {acceptModalOpen && ( + + )} ); }; diff --git a/src/hooks/useNotification.js b/src/hooks/useNotification.js new file mode 100644 index 00000000..bce98653 --- /dev/null +++ b/src/hooks/useNotification.js @@ -0,0 +1,15 @@ +import { useDispatch } from "react-redux"; + +import { closeNotification, setNotification } from "../redux/outstaffingSlice"; + +export const useNotification = () => { + const dispatch = useDispatch(); + + const showNotification = (notification) => { + dispatch(setNotification(notification)); + setTimeout(() => { + dispatch(closeNotification()); + }, 2500); + }; + return { showNotification }; +}; diff --git a/src/pages/ProjectTracker/ProjectTracker.js b/src/pages/ProjectTracker/ProjectTracker.js index f9094701..0ab9b422 100644 --- a/src/pages/ProjectTracker/ProjectTracker.js +++ b/src/pages/ProjectTracker/ProjectTracker.js @@ -25,6 +25,8 @@ import { caseOfNum } from "@utils/helper"; import { apiRequest } from "@api/request"; +import { useNotification } from "@hooks/useNotification"; + import BaseButton from "@components/Common/BaseButton/BaseButton"; import { Footer } from "@components/Common/Footer/Footer"; import { Loader } from "@components/Common/Loader/Loader"; @@ -72,6 +74,7 @@ export const ProjectTracker = () => { const startWrapperIndexTest = useRef({}); const projectBoard = useSelector(getProjectBoard); const loader = useSelector(getBoarderLoader); + const { showNotification } = useNotification(); useEffect(() => { dispatch(activeLoader()); @@ -224,6 +227,7 @@ export const ProjectTracker = () => { } else { dispatch(setProjectBoardFetch(projectBoard.id)); } + showNotification({ show: true, text: "Колонка удалена", type: "error" }); }); } diff --git a/src/redux/outstaffingSlice.js b/src/redux/outstaffingSlice.js index f6f60f69..82f9e84c 100644 --- a/src/redux/outstaffingSlice.js +++ b/src/redux/outstaffingSlice.js @@ -1,4 +1,4 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; const initialState = { tags: [], @@ -14,6 +14,11 @@ const initialState = { partnerRequestId: null, partnerRequests: [], partnerRequestInfo: {}, + notification: { + show: false, + text: "", + type: "", + }, }; export const outstaffingSlice = createSlice({ @@ -62,6 +67,12 @@ export const outstaffingSlice = createSlice({ setPartnerRequestInfo: (state, action) => { state.partnerRequestInfo = action.payload; }, + setNotification: (state, action) => { + state.notification = action.payload; + }, + closeNotification: (state) => { + state.notification.show = false; + }, }, }); @@ -80,12 +91,15 @@ export const { setPartnerRequestId, setPartnerRequests, setPartnerRequestInfo, + setNotification, + closeNotification, } = outstaffingSlice.actions; export const selectProfiles = (state) => state.outstaffing.profiles; export const selectTags = (state) => state.outstaffing.tags; export const selectFilteredCandidates = (state) => state.outstaffing.filteredCandidates; +export const getNotification = (state) => state.outstaffing.notification; export const selectItems = (state) => state.outstaffing.selectedItems; export const selectCurrentCandidate = (state) => state.outstaffing.currentCandidate;