From cb3d3e5ab422ea127fac21806ba94ff345f76dfe Mon Sep 17 00:00:00 2001 From: inmake Date: Wed, 2 Oct 2024 14:03:15 +0500 Subject: [PATCH] upd --- client/src/components/Schedule.tsx | 226 +++++++++++++++++++++++++ client/src/components/Timeline.tsx | 203 ++++++++++++++++++++++ client/src/components/TimelineSlot.tsx | 14 ++ client/src/pages/DashboardPage.tsx | 92 +++++----- client/src/stores/useEventStore.ts | 14 ++ client/src/types/IEvent.ts | 7 + server/src/models/ScheduledSession.ts | 8 + server/src/routes/scheduledSessions.ts | 9 +- 8 files changed, 523 insertions(+), 50 deletions(-) create mode 100644 client/src/components/Schedule.tsx create mode 100644 client/src/components/Timeline.tsx create mode 100644 client/src/components/TimelineSlot.tsx create mode 100644 client/src/stores/useEventStore.ts create mode 100644 client/src/types/IEvent.ts diff --git a/client/src/components/Schedule.tsx b/client/src/components/Schedule.tsx new file mode 100644 index 0000000..cbc14c5 --- /dev/null +++ b/client/src/components/Schedule.tsx @@ -0,0 +1,226 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from "react"; +import Timeline from "./Timeline"; +import { format, setHours, startOfDay } from "date-fns"; +// import useEventStore from "../stores/useEventStore"; +import Button from "./Button"; +import CloseIcon from "./icons/CloseIcon"; +import Input from "./Input"; +import Label from "./Label"; +import api from "../utils/api"; +import useStore from "../stores/useStore"; + +interface Props { + slots: number; + events: any[]; +} + +function Schedule({ slots, events }: Props) { + const { selectedBuild } = useStore(); + const [draftMode, setDraftMode] = useState(false); + const [slot, setSlot] = useState(); + const [startAt, setStartAt] = useState(); + const [duration, setDuration] = useState(); + const [email, setEmail] = useState(""); + const [name, setName] = useState(""); + const [phone, setPhone] = useState(""); + + function handleChangeSlot(slot: number) { + setSlot(slot); + } + + function handleChangeDraftMode(draftMode: boolean) { + setDraftMode(draftMode); + } + + function handleChangeStartAt(startAt: Date) { + setStartAt(startAt); + } + + function handleChangeDuration(duration: number) { + setDuration(duration); + } + + async function handleClickSave() { + if (!slot || !startAt || !duration) return; + + await addSchesuledSession(); + + // setEvents([ + // ...events, + // { + // slot, + // startAt, + // endAt: addMinutes(startAt, duration), + // }, + // ]); + + setDraftMode(false); + } + + function handleClickCancel() { + setDraftMode(false); + } + + async function addSchesuledSession() { + // setIsLoading(true); + + try { + await api + .post(`scheduled_sessions`, { + json: { + buildId: selectedBuild?.id, + slot, + startAt, + duration, + client: { + email, + phone, + name, + }, + }, + }) + .json(); + } catch (error) { + alert((error as Error).message); + } + + // setIsLoading(false); + // setModal(null); + } + + useEffect(() => { + console.log("events", events); + }, []); + + return ( +
+
+
+

{format(new Date(), "HH:mm")}

+
+
+ {slots && + Array.from({ length: slots }).map((_, index) => ( +
+

Слот {index + 1}

+
+ ))} +
+
+ +
+
+ {Array.from({ length: 24 }).map((_, index) => ( +
+

+ {format(setHours(startOfDay(new Date()), index), "HH:mm")} +

+
+ ))} +
+ + {slots && + Array.from({ length: slots }).map((_, index) => ( + event.slot === index + 1 + )} + draftMode={draftMode} + onChangeSlot={handleChangeSlot} + onChangeDraftMode={handleChangeDraftMode} + onChangeStartAt={handleChangeStartAt} + onChangeDuration={handleChangeDuration} + /> + ))} +
+ + {draftMode && startAt && ( +
+
+
+

Запланировать демонстрацию

+
+
+

Демонстрация

+
+
+

Дата и время

+

{format(startAt, "dd.MM.yyyy HH:mm")}

+
+
+

Длительность сеанса

+

{duration} мин.

+
+
+
+
+

Клиент

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ )} +
+ ); +} + +export default Schedule; diff --git a/client/src/components/Timeline.tsx b/client/src/components/Timeline.tsx new file mode 100644 index 0000000..afc0f3a --- /dev/null +++ b/client/src/components/Timeline.tsx @@ -0,0 +1,203 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useState, MouseEvent, useEffect, useRef } from "react"; +import TimelineSlot from "./TimelineSlot"; +import { addMinutes, differenceInMinutes, format, startOfDay } from "date-fns"; +import Button from "./Button"; + +interface Props { + timelineEvents: any[]; + slot: number; + draftMode: boolean; + onChangeDraftMode: (draftMode: boolean) => void; + onChangeStartAt: (startAt: Date) => void; + onChangeDuration: (duration: number) => void; + onChangeSlot: (slot: number) => void; +} + +const timelineSlotHeight = 180; +const minutePx = timelineSlotHeight / 60; + +function Timeline({ + timelineEvents, + slot, + draftMode, + onChangeDraftMode, + onChangeStartAt, + onChangeDuration, + onChangeSlot, +}: Props) { + const [pressed, setPressed] = useState(false); + const [startPosY, setStartPosY] = useState(); + const [currentPosY, setCurrentPosY] = useState(); + const ref = useRef(null); + const [startAt, setStartAt] = useState(); + const [duration, setDuration] = useState(30); // min + + function handleMouseDown(e: MouseEvent) { + if (draftMode) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const y = e.clientY - rect.top; + const roundedY = Math.floor(y / (minutePx * 10)) * (minutePx * 10); + + ref.current!.style.top = `${roundedY}px`; + + setStartPosY(roundedY); + setCurrentPosY(roundedY + minutePx * duration); + setPressed(true); + + if (!duration) { + setDuration(30); + } + + setStartAt( + addMinutes(startOfDay(new Date()), roundedY / minutePx).toISOString() + ); + + onChangeSlot(slot); + } + + function handleMouseMove(e: MouseEvent) { + if (!pressed || startPosY === undefined) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const y = e.clientY - rect.top; + + // if (y < startPosY + minutePx * 30) return; + + // if (y < startPosY + minutePx * 30) return; + + if (y < startPosY + minutePx * 30) { + setDuration(30); + } else { + const roundedY = + Math.round((y - startPosY) / (minutePx * 10)) * (minutePx * 10); + + setCurrentPosY(y); + setDuration(roundedY / minutePx); + + ref.current!.style.height = `${roundedY}px`; + } + } + + function handleMouseUp() { + if (!pressed) return; + + setPressed(false); + onChangeDraftMode(true); + } + + useEffect(() => { + if (!startPosY) return; + + console.log("startPosY", startPosY); + }, [startPosY]); + + useEffect(() => { + if (!currentPosY) return; + + console.log("currentPosY", currentPosY); + }, [currentPosY]); + + // useEffect(() => { + // setTimelineEvents( + // timelineEvents.filter((event: IEvent) => event.slot === slot) + // ); + // }, [timelineEvents]); + + useEffect(() => { + console.log("startAt", startAt); + + if (!startAt) return; + + onChangeStartAt(new Date(startAt)); + }, [startAt]); + + useEffect(() => { + if (!duration || !ref.current) return; + + if (ref.current.clientHeight === 0) { + ref.current.style.height = `${minutePx * 30}px`; + } + + onChangeDuration(duration); + }, [duration]); + + useEffect(() => { + if (draftMode || !ref.current) return; + + setStartAt(undefined); + setDuration(0); + ref.current.style.height = "0px"; + console.log(ref.current.clientHeight); + }, [draftMode]); + + return ( +
+
+ {Array.from({ length: 24 }).map((_, index) => ( + + ))} + +
+ {ref.current?.clientHeight !== 0 && ( +
+ {startAt && format(new Date(startAt), "HH:mm")} -{" "} + {startAt && + duration && + format(addMinutes(new Date(startAt), duration), "HH:mm")} +
+ )} +
+
+ + {timelineEvents.map((event) => ( +
+
+

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

+ + + +
+
+ ))} +
+ ); +} + +export default Timeline; diff --git a/client/src/components/TimelineSlot.tsx b/client/src/components/TimelineSlot.tsx new file mode 100644 index 0000000..11dc36b --- /dev/null +++ b/client/src/components/TimelineSlot.tsx @@ -0,0 +1,14 @@ +interface Props { + height: number; // px +} + +function TimelineSlot({ height }: Props) { + return ( +
+ ); +} + +export default TimelineSlot; diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 9f57945..3faf970 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -9,26 +9,21 @@ import { isEqual, } from "date-fns"; import { useEffect, useRef, useState } from "react"; -import Card from "../components/Card"; -import EmptyCard from "../components/EmptyCard"; import TabButton from "../components/TabButton"; import api from "../utils/api"; import { ru } from "date-fns/locale"; -import { Transition } from "react-transition-group"; -import _ from "lodash"; import useStore from "../stores/useStore"; import useAuthStore from "../stores/useAuthStore"; import Menu from "../components/Menu"; import Button from "../components/Button"; import Calendar from "../components/Calendar"; import Managers from "../components/Managers"; -import Schedules from "../components/Schedules"; -import SpinnerIcon from "../components/icons/SpinnerIcon"; import ModalContainer from "../components/ModalContainer"; import ChevronLeftIcon from "../components/icons/ChevronLeftIcon"; import ChevronRightIcon from "../components/icons/ChevronRightIcon"; import IUser from "../types/IUser"; import IError from "../types/IError"; +import Schedule from "../components/Schedule"; function DashboardPage() { const { user } = useAuthStore(); @@ -46,16 +41,12 @@ function DashboardPage() { selectedDay, setSelectedDay, } = useStore(); - const [duration, setDuration] = useState(); + const [, setDuration] = useState(); const [scheduledSessions, setScheduledSessions] = useState(); - const [generatedScheduledSessions, setGeneratedScheduledSessions] = - useState(); + const [, setGeneratedScheduledSessions] = useState(); const [dateTimes, setDateTimes] = useState(); - const [currentTime, setCurrentTime] = useState( - format(new Date(), "HH:mm") - ); - const [isLoadingScheduledSessions, setIsLoadingScheduledSessions] = - useState(true); + const [, setCurrentTime] = useState(format(new Date(), "HH:mm")); + const [, setIsLoadingScheduledSessions] = useState(true); const scheduledSessionsRef = useRef(null); function selectNextDay() { @@ -231,33 +222,33 @@ function DashboardPage() { if (useLoader) setIsLoadingScheduledSessions(false); } - async function updateScheduledSessionManager( - scheduledSessionId: string, - managerId: string | null - ) { - if (!company || !scheduledSessions) return; + // async function updateScheduledSessionManager( + // scheduledSessionId: string, + // managerId: string | null + // ) { + // if (!company || !scheduledSessions) return; - try { - const result: any = await api - .put( - `companies/${company.id}/scheduled_sessions/${scheduledSessionId}`, - { - json: { userId: managerId }, - } - ) - .json(); + // try { + // const result: any = await api + // .put( + // `companies/${company.id}/scheduled_sessions/${scheduledSessionId}`, + // { + // json: { userId: managerId }, + // } + // ) + // .json(); - setScheduledSessions( - scheduledSessions.map((scheduledSession) => - scheduledSession.id === result.id ? result : scheduledSession - ) - ); - } catch (error) { - if (error instanceof Error) { - console.log("Error: ", error.message); - } - } - } + // setScheduledSessions( + // scheduledSessions.map((scheduledSession) => + // scheduledSession.id === result.id ? result : scheduledSession + // ) + // ); + // } catch (error) { + // if (error instanceof Error) { + // console.log("Error: ", error.message); + // } + // } + // } async function getSchedules() { if (!company || !selectedBuild) return; @@ -316,8 +307,8 @@ function DashboardPage() { }, [selectedDay, selectedBuild]); return ( -
-
+
+
{builds?.map((build) => ( @@ -358,7 +349,7 @@ function DashboardPage() {
-
+ {/*
{currentTime}
@@ -371,9 +362,9 @@ function DashboardPage() {
))} -
+
*/} -
@@ -443,7 +434,14 @@ function DashboardPage() {
) )} -
+ */} + + {selectedBuild?.sessionLimit && scheduledSessions && ( + + )}
@@ -457,9 +455,9 @@ function DashboardPage() { Статистика
-
+
- + {/* */}
diff --git a/client/src/stores/useEventStore.ts b/client/src/stores/useEventStore.ts new file mode 100644 index 0000000..7de5575 --- /dev/null +++ b/client/src/stores/useEventStore.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import IEvent from "../types/IEvent"; + +interface EventState { + events: IEvent[]; + setEvents: (events: IEvent[]) => void; +} + +const useEventStore = create()((set) => ({ + events: [], + setEvents: (events) => set({ events }), +})); + +export default useEventStore; diff --git a/client/src/types/IEvent.ts b/client/src/types/IEvent.ts new file mode 100644 index 0000000..18940b6 --- /dev/null +++ b/client/src/types/IEvent.ts @@ -0,0 +1,7 @@ +interface IEvent { + slot: number; + startAt: Date; + endAt: Date; +} + +export default IEvent; diff --git a/server/src/models/ScheduledSession.ts b/server/src/models/ScheduledSession.ts index 695e6d8..891d85a 100644 --- a/server/src/models/ScheduledSession.ts +++ b/server/src/models/ScheduledSession.ts @@ -16,10 +16,18 @@ const scheduledSessionSchema = new Schema( type: Schema.Types.ObjectId, ref: "User", }, + slot: { + type: Number, + required: true, + }, startAt: { type: Date, required: true, }, + duration: { + type: Number, + required: true, + }, endAt: { type: Date, required: true, diff --git a/server/src/routes/scheduledSessions.ts b/server/src/routes/scheduledSessions.ts index 31b2afe..6d355ad 100644 --- a/server/src/routes/scheduledSessions.ts +++ b/server/src/routes/scheduledSessions.ts @@ -75,12 +75,12 @@ router.get("/:buildId", async (req, res) => { }); router.post("/", async (req, res) => { - const { buildId, startAt, client, duration } = req.body; + const { buildId, slot, startAt, client, duration } = req.body; - if (!buildId || !startAt) { + if (!buildId || !startAt || !slot) { return res.json({ status: "error", - message: "Parameters `buildId`, `startAt` are required!", // Параметры `buildId`, `startAt` обязательны! + message: "Parameters `buildId`, `startAt`, `slot` are required!", // Параметры `buildId`, `startAt`, `slot` обязательны! }); } @@ -105,6 +105,7 @@ router.post("/", async (req, res) => { if (duration) { const scheduledSessions = await ScheduledSession.find({ buildId, + slot, startAt: { $gte: startOfDay(startAtISO), $lte: endOfDay(startAtISO), @@ -141,7 +142,9 @@ router.post("/", async (req, res) => { const scheduledSession = await ScheduledSession.create({ buildId, + slot, startAt: startAtISO, + duration, endAt: endAtISO, });