diff --git a/client/src/components/Card.tsx b/client/src/components/Card.tsx index 20ba6a6..7cc4ce6 100644 --- a/client/src/components/Card.tsx +++ b/client/src/components/Card.tsx @@ -1,184 +1,217 @@ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect, useState } from "react"; -import SelectUser from "./SelectUser"; -import MoreIcon from "./icons/MoreIcon"; +import { Link } from "react-router-dom"; +import { isAfter, isBefore, subMinutes } from "date-fns"; import api from "../utils/api"; import Button from "./Button"; -import { Link } from "react-router-dom"; -import IUser from "../types/IUser"; -import useAuthStore from "../stores/useAuthStore"; +import SelectUser from "./SelectUser"; import EntryIcon from "./icons/EntryIcon"; -import { isAfter, subMinutes } from "date-fns"; +import MoreIcon from "./icons/MoreIcon"; +import ShareIcon from "./icons/ShareIcon"; +import ShareModal from "./modals/ShareModal"; +import useAuthStore from "../stores/useAuthStore"; +import useModalStore from "../stores/useModalStore"; +import IUser from "../types/IUser"; interface CardProps { companyId: string; buildId: string; + buildName?: string; scheduledSessionId: string; scheduleSessionStartAt: string; - client?: { - name: string; - phone: string; - email: string; - }; + scheduleSessionEndAt?: string; + client?: { name: string; phone: string; email: string }; manager?: IUser; managers: IUser[]; handleSelect: (scheduledSessionId: string, managerId: string | null) => void; + fitContainer?: boolean; +} + +const STREAM_URL = import.meta.env.VITE_STREAM_URL as string; + +const STATUS = { + noManager: { bg: "bg-[#F2DADA]", dot: "bg-[#EB5757]", text: "text-[#EB5757]", label: "Нет менеджера" }, + ready: { bg: "bg-[#E6F2FE]", dot: "bg-[#49A1F5]", text: "text-[#49A1F5]", label: "Готов" }, + done: { bg: "bg-[#F0F1F2]", dot: "bg-[#77828C]", text: "text-[#77828C]", label: "Проведена" }, + scheduled: { bg: "bg-[#F0F1F2]", dot: "bg-[#77828C]", text: "text-[#77828C]", label: "Запланировано" }, +} as const; + +function getStatus( + manager: IUser | undefined, + canStart: boolean, + isCompleted: boolean +) { + if (!manager) return STATUS.noManager; + if (canStart) return STATUS.ready; + if (isCompleted) return STATUS.done; + return STATUS.scheduled; } function Card({ companyId, buildId, + buildName, scheduledSessionId, scheduleSessionStartAt, + scheduleSessionEndAt, client, manager, managers, handleSelect, + fitContainer = false, }: CardProps) { const { user } = useAuthStore(); - const [isShow, setIsShow] = useState(false); + const setModal = useModalStore((state) => state.setModal); + const [isShowManagerSelect, setIsShowManagerSelect] = useState(false); const [availableManagers, setAvailableManagers] = useState(); - async function getAvailableManagers() { - const result: any[] = await api + useEffect(() => { + if (!isShowManagerSelect) return; + + api .get( `companies/${companyId}/builds/${buildId}/scheduledSessions/${scheduledSessionId}/availableManagers?startAt=${scheduleSessionStartAt}` ) - .json(); + .json() + .then((ids) => setAvailableManagers(managers.filter((m) => ids.includes(m.id)))); + }, [isShowManagerSelect]); - const filteredManagers = managers.filter((manager) => - result.includes(manager.id) - ); + const now = new Date(); + const canStart = !!manager && isAfter(now, subMinutes(new Date(scheduleSessionStartAt), 10)); + const isCompleted = !!scheduleSessionEndAt && isBefore(new Date(scheduleSessionEndAt), now); + const status = getStatus(manager, canStart, isCompleted); - setAvailableManagers(filteredManagers); - } - - useEffect(() => { - if (!isShow) return; - getAvailableManagers(); - }, [isShow]); + const sessionUrl = `${STREAM_URL}/scheduled/${scheduledSessionId}?admin=true`; return ( -
-
-
-
-
-

Клиент

-

{client?.name || "Имя не указано"}

-
- {manager ? ( -
-
-

- Готов -

-
- ) : ( -
-
-

- Нет менеджера -

-
- )} +
+
+ {/* SessionInfo */} +
+

+ {buildName ? `ЖК «${buildName}»` : "ЖК"} +

+ +
+ + Клиент + + + {client?.name || "Имя не указано"} +
-
-

+ +

+ {client?.phone || "Телефон не указан"} -

-

+ + {client?.email || "Email не указан"} -

+
+
+ + {/* Status indicator */} +
+
+ + {status.label} + +
+ + {/* Share button */} +
+
-
-
- {manager ? ( - - ) : ( -
- )} -

+ + {/* Manager row */} +

+
+
+ {manager ? ( + <> + + {/* Online dot */} +
+ + ) : ( +
+ )} +
+ {manager ? manager.name : "Не назначен"} -

+
-
- {user?.role === "manager" && - (manager ? ( - isAfter( - new Date(), - subMinutes(new Date(scheduleSessionStartAt), 10) - ) ? ( - +
+ {user?.role === "manager" && ( + <> + {!manager && ( + + )} + {manager && canStart && ( + - ) : ( - manager.id === user.id && ( - - ) - ) - ) : ( - - ))} + )} + {manager && !canStart && manager.id === user.id && ( + + )} + + )} {user?.role === "admin" && ( -
- {manager && ( - - -
+ <> + +
- {availableManagers && availableManagers.length > 0 && ( - ( - handleSelect(scheduledSessionId, managerId), setIsShow(false) - )} - handleShown={() => setIsShow((prev) => !prev)} - /> - )} + { + handleSelect(scheduledSessionId, managerId); + setIsShowManagerSelect(false); + }} + handleShown={() => setIsShowManagerSelect(false)} + />
); diff --git a/client/src/components/ManagerMoreMenu.tsx b/client/src/components/ManagerMoreMenu.tsx new file mode 100644 index 0000000..8db17b7 --- /dev/null +++ b/client/src/components/ManagerMoreMenu.tsx @@ -0,0 +1,118 @@ +import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { Transition } from "react-transition-group"; +import { HTTPError } from "ky"; +import toast from "react-hot-toast"; +import api from "../utils/api"; +import IUser from "../types/IUser"; + +interface ManagerMoreMenuProps { + shown: boolean; + manager: IUser; + companyId: string; + anchorEl: HTMLElement | null; + onClose: () => void; + onManagerDeleted: () => void; +} + +function ManagerMoreMenu({ + shown, + manager, + companyId, + anchorEl, + onClose, + onManagerDeleted, +}: ManagerMoreMenuProps) { + const ref = useRef(null); + const [isDeleting, setIsDeleting] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0 }); + + useLayoutEffect(() => { + if (shown && anchorEl) { + const rect = anchorEl.getBoundingClientRect(); + setPosition({ + top: rect.bottom + 8, + left: rect.left, + }); + } + }, [shown, anchorEl]); + + useEffect(() => { + if (!shown) return; + + const handleMouseDown = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + onClose(); + } + }; + + const timer = setTimeout(() => { + document.addEventListener("mousedown", handleMouseDown); + }, 0); + + return () => { + clearTimeout(timer); + document.removeEventListener("mousedown", handleMouseDown); + }; + }, [shown, onClose]); + + async function handleDelete() { + if (isDeleting) return; + setIsDeleting(true); + try { + const result = await api + .delete(`companies/${companyId}/users/${manager.id}`) + .json<{ success?: boolean; error?: string }>(); + + if ("error" in result) { + toast.error(result.error ?? "Ошибка"); + return; + } + + onClose(); + onManagerDeleted(); + toast.success("Менеджер удалён"); + } catch (error) { + if (error instanceof HTTPError) { + const body = (await error.response.json()) as { error?: string }; + toast.error(body?.error ?? (error as Error).message ?? "Ошибка"); + } else { + toast.error((error as Error).message ?? "Ошибка"); + } + } finally { + setIsDeleting(false); + } + } + + const menuContent = ( + + {(state) => ( +
+ + +
+ )} +
+ ); + + return createPortal(menuContent, document.body); +} + +export default ManagerMoreMenu; diff --git a/client/src/components/Managers.tsx b/client/src/components/Managers.tsx index 756222b..1057c2d 100644 --- a/client/src/components/Managers.tsx +++ b/client/src/components/Managers.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import useAuthStore from "../stores/useAuthStore"; import useModalStore from "../stores/useModalStore"; import useSettingsStore from "../stores/useSettingsStore"; @@ -5,18 +6,21 @@ import useStore from "../stores/useStore"; import Button from "./Button"; import ChevronDownIcon from "./icons/ChevronDownIcon"; import ChevronUpIcon from "./icons/ChevronUpIcon"; -// import MoreIcon from "./icons/MoreIcon"; +import MoreIcon from "./icons/MoreIcon"; +import ManagerMoreMenu from "./ManagerMoreMenu"; import AddManagerModal from "./modals/AddManagerModal"; function Managers() { const { user } = useAuthStore(); - const { managers } = useStore(); + const { company, managers, setManagers } = useStore(); + const [openMenuManagerId, setOpenMenuManagerId] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); const { isShowManagers, setIsShowManagers } = useSettingsStore(); const { setModal } = useModalStore(); return (
-
+

Менеджеры

+ )}
- {/* {user?.role === "admin" && ( -
- ))} + )) + )}
+ {user?.role === "admin" && + company && + openMenuManagerId && + managers.find((m) => m.id === openMenuManagerId) && ( + m.id === openMenuManagerId)!} + companyId={company.id} + anchorEl={anchorEl} + onClose={() => { + setOpenMenuManagerId(null); + setAnchorEl(null); + }} + onManagerDeleted={() => { + const manager = managers.find( + (m) => m.id === openMenuManagerId + ); + if (manager) { + setManagers(managers.filter((m) => m.id !== manager.id)); + } + setOpenMenuManagerId(null); + setAnchorEl(null); + }} + /> + )} {user?.role === "admin" && (
diff --git a/client/src/components/SelectUser.tsx b/client/src/components/SelectUser.tsx index 5f2174a..e2f958c 100644 --- a/client/src/components/SelectUser.tsx +++ b/client/src/components/SelectUser.tsx @@ -1,64 +1,100 @@ -/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useRef } from "react"; +import { Transition } from "react-transition-group"; +import CheckIcon from "./icons/CheckIcon"; + interface SelectUserProps { shown: boolean; selectedManagerId?: string; managers: any[]; + loading?: boolean; handleClick: (managerId: string | null) => void; handleShown: () => void; } -import { Transition } from "react-transition-group"; -import CheckIcon from "./icons/CheckIcon"; -import useOutsideClick from "../hooks/useOutsideClick"; - function SelectUser({ shown, selectedManagerId, managers, + loading, handleClick, handleShown, }: SelectUserProps) { - const selectUserRef = useOutsideClick(handleShown); + const ref = useRef(null); + + useEffect(() => { + if (!shown) return; + + const handleMouseDown = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + handleShown(); + } + }; + + // defer to avoid catching the same click that opened the dropdown + const timer = setTimeout(() => { + document.addEventListener("mousedown", handleMouseDown); + }, 0); + + return () => { + clearTimeout(timer); + document.removeEventListener("mousedown", handleMouseDown); + }; + }, [shown, handleShown]); return ( {(state) => (
- {managers.map((manager) => ( - - ))} + ) : ( + <> + {managers.map((manager) => ( + + ))} - + + + )} +
)} diff --git a/client/src/components/Timeline.tsx b/client/src/components/Timeline.tsx index 7849704..112ec70 100644 --- a/client/src/components/Timeline.tsx +++ b/client/src/components/Timeline.tsx @@ -13,10 +13,11 @@ import { } from "date-fns"; import Button from "./Button"; import IScheduledSession from "../types/IScheduledSession"; -import useStore from "../stores/useStore"; +import IUser from "../types/IUser"; import CloseIcon from "./icons/CloseIcon"; import api from "../utils/api"; import toast from "react-hot-toast"; +import Card from "./Card"; interface Props { timelineEvents: IScheduledSession[]; @@ -28,6 +29,14 @@ interface Props { onChangeStartAt: (startAt: Date) => void; onChangeDuration: (duration: number) => void; onChangeSlot: (slot: number) => void; + companyId: string; + managers: IUser[]; + builds: { id: string; name: string }[]; + onScheduledSessionUpdate: ( + scheduledSessionId: string, + managerId: string | null + ) => void; + onScheduledSessionRemove?: () => void; } const timelineSlotHeight = 180; @@ -43,6 +52,11 @@ function Timeline({ onChangeStartAt, onChangeDuration, onChangeSlot, + companyId, + managers, + builds, + onScheduledSessionUpdate, + onScheduledSessionRemove, }: Props) { const [pressed, setPressed] = useState(false); const [startPosY, setStartPosY] = useState(); @@ -50,7 +64,6 @@ function Timeline({ const ref = useRef(null); const [startAt, setStartAt] = useState(); const [duration, setDuration] = useState(30); // min - const { builds } = useStore(); function handleMouseDown(e: MouseEvent) { if (draftMode) return; @@ -119,6 +132,7 @@ function Timeline({ } toast.success("Сеанс успешно удален!"); + onScheduledSessionRemove?.(); } catch (error) { toast.error("Что-то пошло не так"); } @@ -192,56 +206,72 @@ function Timeline({
- {timelineEvents.map((event) => ( -
-
-
-

- {format(new Date(event.startAt), "HH:mm")} -{" "} - {format(new Date(event.endAt), "HH:mm")} -

-

{builds.find((build) => build.id === event.buildId)?.name}

+ {timelineEvents.map((event) => { + const client = event.client as + | { name?: string; phone?: string; email?: string } + | undefined; + const manager = event.userId + ? managers.find((m) => m.id === event.userId) + : undefined; + + return ( +
+
+ b.id === event.buildId)?.name} + scheduledSessionId={event.id} + scheduleSessionStartAt={event.startAt} + scheduleSessionEndAt={event.endAt} + client={ + client + ? { + name: client.name ?? "", + phone: client.phone ?? "", + email: client.email ?? "", + } + : undefined + } + manager={manager} + managers={managers} + handleSelect={onScheduledSessionUpdate} + fitContainer + /> +
- - - -
-
- ))} + ); + })}
); } diff --git a/client/src/components/icons/EntryIcon.tsx b/client/src/components/icons/EntryIcon.tsx index 4457fa5..e3dab4f 100644 --- a/client/src/components/icons/EntryIcon.tsx +++ b/client/src/components/icons/EntryIcon.tsx @@ -1,8 +1,6 @@ function EntryIcon() { return ( + + + + + ); +} + +export default ShareIcon; diff --git a/client/src/components/modals/ShareModal.tsx b/client/src/components/modals/ShareModal.tsx new file mode 100644 index 0000000..ae6239f --- /dev/null +++ b/client/src/components/modals/ShareModal.tsx @@ -0,0 +1,87 @@ +import Button from "../Button"; +import CloseIcon from "../icons/CloseIcon"; +import useModalStore from "../../stores/useModalStore"; +import toast from "react-hot-toast"; + +interface Props { + scheduledSessionId: string; +} + +function ShareModal({ scheduledSessionId }: Props) { + const { setModal } = useModalStore(); + + const streamUrl = import.meta.env.VITE_STREAM_URL || ""; + const viewLink = `${streamUrl}/scheduled/${scheduledSessionId}`; + const conductLink = `${streamUrl}/scheduled/${scheduledSessionId}?admin=true`; + + async function copyToClipboard(text: string) { + try { + await navigator.clipboard.writeText(text); + toast.success("Ссылка скопирована"); + } catch { + toast.error("Не удалось скопировать"); + } + } + + return ( +
+
+

Поделиться

+
+ +
+
+

+ Ссылка для просмотра +

+ +

+ Без возможности управления демонстрацией +

+ +
+ +
+

+ Ссылка для проведения +

+ +

+ Полный доступ на управление демонстрацией +

+ +
+
+
+ ); +} + +export default ShareModal; diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 909810e..ae16989 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -131,6 +131,31 @@ function DashboardPage() { if (useLoader) setIsLoadingScheduledSessions(false); } + async function handleSelectSession( + scheduledSessionId: string, + managerId: string | null + ) { + if (!company) return; + + try { + await api + .put( + `companies/${company.id}/scheduledSessions/${scheduledSessionId}`, + { json: { userId: managerId } } + ) + .json(); + + await getScheduledSessions(); + toast.success( + managerId + ? "Менеджер назначен" + : "Менеджер снят с сессии" + ); + } catch (error) { + toast.error((error as Error).message); + } + } + useEffect(() => { getCompany(); @@ -212,6 +237,10 @@ function DashboardPage() { selectedDay={selectedDay} slots={company.sessionLimit} events={scheduledSessions} + companyId={company.id} + managers={managers} + onScheduledSessionUpdate={handleSelectSession} + onScheduledSessionRemove={getScheduledSessions} /> )}
diff --git a/server/src/routes/addManager.ts b/server/src/routes/addManager.ts index 206fd99..1b3187e 100644 --- a/server/src/routes/addManager.ts +++ b/server/src/routes/addManager.ts @@ -9,6 +9,14 @@ const router = Router(); router.post("/", async (req, res) => { const { companyId, username, name, role, buildIds } = req.body; + if (res.locals.user.role !== "admin") { + return res.status(403).json({ error: "Только администратор может добавлять менеджеров" }); + } + + if (companyId != res.locals.user.companyId) { + return res.status(403).json({ error: "Access denied" }); + } + try { const password = generate({ length: 8, @@ -49,9 +57,10 @@ router.post("/", async (req, res) => { await transporter.sendMail({ from: "stream@graff.tech", // sender address to: username, // list of receivers - subject: "Данные аккаунта - stream.graff.tech", // Subject line + subject: "Данные аккаунта - crm.stream.graff.tech", // Subject line html: `
- Пароль для входа в аккаунт: ${password} + Пароль для входа в аккаунт: ${password}
+ Ссылка для входа: https://crm.stream.graff.tech/
`, }); } catch (error) { diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index b462e03..c04736c 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -4,6 +4,7 @@ import Company from "../models/Company.js"; import ScheduledSession from "../models/ScheduledSession.js"; import Schedule from "../models/Schedule.js"; import User from "../models/User.js"; +import mongoose from "mongoose"; const router = Router(); @@ -15,8 +16,7 @@ const router = Router(); router.get("/:companyId", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } const company = await Company.findById(req.params.companyId); @@ -26,8 +26,7 @@ router.get("/:companyId", async (req, res) => { router.put("/:companyId", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } const company = await Company.findByIdAndUpdate( @@ -43,8 +42,7 @@ router.put("/:companyId", async (req, res) => { router.get("/:companyId/builds", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } const company: any = await Company.findById(req.params.companyId).populate( @@ -57,8 +55,7 @@ router.get("/:companyId/builds", async (req, res) => { router.get("/:companyId/users", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } const company: any = await Company.findById(req.params.companyId).populate( @@ -71,8 +68,7 @@ router.get("/:companyId/users", async (req, res) => { router.get("/:companyId/builds/:buildId/users", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } const buildUsers: any = await User.find({ @@ -91,8 +87,7 @@ router.get("/:companyId/builds/:buildId/users", async (req, res) => { router.get("/:companyId/scheduledSessions", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } const { startDate, endDate } = req.query; @@ -117,71 +112,147 @@ router.get("/:companyId/scheduledSessions", async (req, res) => { res.json(scheduledSessions); }); +router.get( + "/:companyId/builds/:buildId/scheduledSessions/:scheduledSessionId/availableManagers", + async (req, res) => { + if (req.params.companyId != res.locals.user.companyId) { + return res.status(403).json({ error: "Access denied" }); + } + + const { companyId, buildId, scheduledSessionId } = req.params; + const { startAt } = req.query as { startAt: string }; + + try { + const startAtDate = parseISO(startAt); + + // All managers of this company assigned to this build + const buildManagers = await User.find({ + companyId, + role: "manager", + buildIds: new mongoose.Types.ObjectId(buildId), + }); + + // Find managers already busy at this time (excluding current session) + const busySessions = await ScheduledSession.find({ + _id: { $ne: new mongoose.Types.ObjectId(scheduledSessionId) }, + userId: { $in: buildManagers.map((m) => m._id) }, + startAt: startAtDate, + }); + + const busyManagerIds = new Set( + busySessions.map((s) => s.userId?.toString()) + ); + + const available = buildManagers + .filter((m) => !busyManagerIds.has(m._id.toString())) + .map((m) => m._id.toString()); + + res.json(available); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } + } +); + router.put( "/:companyId/scheduledSessions/:scheduledSessionId", async (req, res) => { if (req.params.companyId != res.locals.user.companyId) { - res.json({ error: "Access denied" }); - return; + return res.status(403).json({ error: "Access denied" }); } + const { companyId, scheduledSessionId } = req.params; + const { userId: newManagerId } = req.body; + try { - const scheduledSession = await ScheduledSession.findById( - req.params.scheduledSessionId - ); + const scheduledSession = await ScheduledSession.findById(scheduledSessionId); + + if (!scheduledSession) { + return res.status(404).json({ error: "Сессия не найдена" }); + } + + if (scheduledSession.companyId.toString() !== companyId) { + return res.status(403).json({ error: "Access denied" }); + } + + if (res.locals.user.role === "manager") { + const currentUserId = res.locals.user._id.toString(); + if (newManagerId !== null && newManagerId !== currentUserId) { + return res.status(403).json({ + error: "Менеджер может назначать только себя на сессию", + }); + } + } const scheduledSessionAtSameTime = await ScheduledSession.findOne({ - startAt: scheduledSession?.startAt, - userId: req.body.userId, + startAt: scheduledSession.startAt, + userId: newManagerId, }); - if (scheduledSessionAtSameTime && req.body.userId !== null) { + if (scheduledSessionAtSameTime && newManagerId !== null) { await ScheduledSession.updateMany( { - userId: req.body.userId, - startAt: scheduledSession?.startAt, + userId: newManagerId, + startAt: scheduledSession.startAt, }, - { - userId: null, - } + { userId: null } ); await ScheduledSession.findByIdAndUpdate( - req.params.scheduledSessionId, - { - userId: req.body.userId, - } + scheduledSessionId, + { userId: newManagerId }, + { new: true } ); - res.json({ error: "Scheduled session at same time" }); - return; + return res.json({ error: "Scheduled session at same time" }); } const updatedScheduledSession = await ScheduledSession.findByIdAndUpdate( - req.params.scheduledSessionId, - req.body, - { - new: true, - upsert: true, - } + scheduledSessionId, + { userId: newManagerId }, + { new: true } ); res.json(updatedScheduledSession); } catch (error) { if (error instanceof Error) { - res.json({ error }); + res.status(500).json({ error: error.message }); } } } ); -router.get("/:companyId/users", async (req, res) => { - try { - const companyId = req.params.companyId; - const users = await User.find({ companyId }); +router.delete("/:companyId/users/:userId", async (req, res) => { + if (res.locals.user.role !== "admin") { + return res.status(403).json({ error: "Только администратор может удалять менеджеров" }); + } - return res.json(users); + if (req.params.companyId != res.locals.user.companyId) { + return res.status(403).json({ error: "Access denied" }); + } + + const { companyId, userId } = req.params; + + try { + const user = await User.findOne({ _id: userId, companyId }); + + if (!user) { + return res.status(404).json({ error: "Пользователь не найден" }); + } + + if (user.role === "admin") { + return res.status(403).json({ error: "Нельзя удалить администратора" }); + } + + await ScheduledSession.updateMany( + { companyId, userId: new mongoose.Types.ObjectId(userId) }, + { userId: null } + ); + + await User.findByIdAndRemove(userId); + + res.json({ success: true }); } catch (error) { - return res.json({ error: (error as Error).message }); + res.status(500).json({ error: (error as Error).message }); } }); diff --git a/server/src/routes/reset.ts b/server/src/routes/reset.ts index 194d2c1..594fa0f 100644 --- a/server/src/routes/reset.ts +++ b/server/src/routes/reset.ts @@ -37,7 +37,7 @@ router.post("/", async (req, res) => { await transporter.sendMail({ from: "stream@graff.tech", // sender address to: username, // list of receivers - subject: "Сброс пароля - stream.graff.tech", // Subject line + subject: "Сброс пароля - crm.stream.graff.tech", // Subject line html: `
Ссылка для сброса пароля: ${url}
`, diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts index 3a62862..e163b39 100644 --- a/server/src/routes/users.ts +++ b/server/src/routes/users.ts @@ -1,12 +1,12 @@ import { Router } from "express"; import User from "../models/User.js"; -// import User from "../models/User.js"; const router = Router(); router.get("/", async (req, res) => { try { - const result = await User.find(req.query); + const companyId = res.locals.user.companyId; + const result = await User.find({ companyId, ...req.query }); res.json(result); } catch (error) { @@ -16,7 +16,15 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { - const result = await User.findById(req.params); + const result = await User.findById(req.params.id); + + if (!result) { + return res.status(404).json({ error: "Пользователь не найден" }); + } + + if (result.companyId.toString() !== res.locals.user.companyId) { + return res.status(403).json({ error: "Access denied" }); + } res.json(result); } catch (error) {