This commit is contained in:
2024-10-02 14:03:15 +05:00
parent 87a7021482
commit cb3d3e5ab4
8 changed files with 523 additions and 50 deletions
+226
View File
@@ -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<boolean>(false);
const [slot, setSlot] = useState<number>();
const [startAt, setStartAt] = useState<Date>();
const [duration, setDuration] = useState<number>();
const [email, setEmail] = useState<string>("");
const [name, setName] = useState<string>("");
const [phone, setPhone] = useState<string>("");
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 (
<div className="relative h-screen overflow-y-auto bg-[#F2F2F2] text-sm">
<div className="fixed z-10 flex">
<div className="bg-white h-10 w-[84px] flex items-center justify-center border-r border-b border-[#DAE0E5]">
<p className="font-semibold">{format(new Date(), "HH:mm")}</p>
</div>
<div className="flex">
{slots &&
Array.from({ length: slots }).map((_, index) => (
<div
key={index}
className="border-r border-b border-[#DAE0E5] w-[264px] h-10 flex items-center pl-3 bg-[#F0F1F2]"
>
<p className="font-semibold">Слот {index + 1}</p>
</div>
))}
</div>
</div>
<div className="flex mt-10">
<div className="">
{Array.from({ length: 24 }).map((_, index) => (
<div
key={index}
className="flex items-center justify-center h-[180px] w-[84px] border-r border-b border-[#DAE0E5]"
>
<p className="font-semibold">
{format(setHours(startOfDay(new Date()), index), "HH:mm")}
</p>
</div>
))}
</div>
{slots &&
Array.from({ length: slots }).map((_, index) => (
<Timeline
key={index}
slot={index + 1}
timelineEvents={events.filter(
(event) => event.slot === index + 1
)}
draftMode={draftMode}
onChangeSlot={handleChangeSlot}
onChangeDraftMode={handleChangeDraftMode}
onChangeStartAt={handleChangeStartAt}
onChangeDuration={handleChangeDuration}
/>
))}
</div>
{draftMode && startAt && (
<div className="fixed top-0 right-0 flex flex-col justify-between h-screen overflow-y-auto bg-white shadow w-[320px]">
<div className="space-y-4">
<div className="p-2 pl-4 flex items-center justify-between border-b border-[#DAE0E5]">
<p className="font-semibold">Запланировать демонстрацию</p>
<Button
color="tertiary"
icon={<CloseIcon />}
onlyIcon
handleClick={handleClickCancel}
/>
</div>
<div className="px-4 space-y-2">
<p className="font-semibold">Демонстрация</p>
<div className="">
<div className="grid items-center grid-cols-2 gap-4 py-1 text-xs">
<p className="text-[#77828C]">Дата и время</p>
<p className="">{format(startAt, "dd.MM.yyyy HH:mm")}</p>
</div>
<div className="grid items-center grid-cols-2 gap-4 py-1 text-xs">
<p className="text-[#77828C]">Длительность сеанса</p>
<p className="">{duration} мин.</p>
</div>
</div>
</div>
<div className="px-4 space-y-2">
<p className="font-semibold">Клиент</p>
<div className="space-y-4">
<div className="space-y-1">
<Label value="Email" />
<Input
type="email"
className="w-full h-10"
value={email}
handleChange={(value) => setEmail(value)}
/>
<span className="text-[#77828C] text-xs">
На указанный почтовый адрес придет необходимая для
подключения информация
</span>
</div>
<div className="space-y-1">
<Label value="Телефон" />
<Input
type="tel"
className="w-full h-10"
value={phone}
handleChange={(value) => setPhone(value)}
/>
</div>
<div className="space-y-1">
<Label value="Имя" />
<Input
className="w-full h-10"
value={name}
handleChange={(value) => setName(value)}
/>
</div>
</div>
</div>
</div>
<div className="flex gap-2 p-4">
<Button type="submit" handleClick={handleClickSave}>
Запланировать
</Button>
<Button
type="button"
color="secondary"
handleClick={handleClickCancel}
>
Отмена
</Button>
</div>
</div>
)}
</div>
);
}
export default Schedule;
+203
View File
@@ -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<number>();
const [currentPosY, setCurrentPosY] = useState<number>();
const ref = useRef<HTMLDivElement>(null);
const [startAt, setStartAt] = useState<string>();
const [duration, setDuration] = useState<number>(30); // min
function handleMouseDown(e: MouseEvent<HTMLDivElement>) {
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 (
<div className="relative text-xs select-none">
<div
className={`${pressed ? "cursor-move" : ""}`}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
{Array.from({ length: 24 }).map((_, index) => (
<TimelineSlot
key={index}
height={timelineSlotHeight}
// date={setHours(setMinutes(new Date(), 0), index)}
/>
))}
<div ref={ref} className="absolute w-full">
{ref.current?.clientHeight !== 0 && (
<div className="h-full bg-black/20 animate-pulse border border-[#DAE0E5] p-3">
{startAt && format(new Date(startAt), "HH:mm")} -{" "}
{startAt &&
duration &&
format(addMinutes(new Date(startAt), duration), "HH:mm")}
</div>
)}
</div>
</div>
{timelineEvents.map((event) => (
<div
key={event.id}
data-type="event"
className="absolute w-full bg-white border-b border-r border-[#DAE0E5] p-3"
style={{
top: `${
differenceInMinutes(
new Date(event.startAt),
startOfDay(new Date(event.startAt))
) * minutePx
}px`,
height: `${
differenceInMinutes(
new Date(event.endAt),
new Date(event.startAt)
) * minutePx
}px`,
}}
>
<div className="flex flex-col justify-between h-full">
<p>
{format(new Date(event.startAt), "HH:mm")} -{" "}
{format(new Date(event.endAt), "HH:mm")}
</p>
<a
href={`https://stream.graff.tech/scheduled/${event.id}?admin=true`}
target="_blank"
className="self-end"
>
<Button className="">Начать</Button>
</a>
</div>
</div>
))}
</div>
);
}
export default Timeline;
+14
View File
@@ -0,0 +1,14 @@
interface Props {
height: number; // px
}
function TimelineSlot({ height }: Props) {
return (
<div
style={{ height: `${height}px` }}
className="border-r border-b border-[#DAE0E5] w-[264px]"
></div>
);
}
export default TimelineSlot;
+45 -47
View File
@@ -9,26 +9,21 @@ import {
isEqual, isEqual,
} from "date-fns"; } from "date-fns";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Card from "../components/Card";
import EmptyCard from "../components/EmptyCard";
import TabButton from "../components/TabButton"; import TabButton from "../components/TabButton";
import api from "../utils/api"; import api from "../utils/api";
import { ru } from "date-fns/locale"; import { ru } from "date-fns/locale";
import { Transition } from "react-transition-group";
import _ from "lodash";
import useStore from "../stores/useStore"; import useStore from "../stores/useStore";
import useAuthStore from "../stores/useAuthStore"; import useAuthStore from "../stores/useAuthStore";
import Menu from "../components/Menu"; import Menu from "../components/Menu";
import Button from "../components/Button"; import Button from "../components/Button";
import Calendar from "../components/Calendar"; import Calendar from "../components/Calendar";
import Managers from "../components/Managers"; import Managers from "../components/Managers";
import Schedules from "../components/Schedules";
import SpinnerIcon from "../components/icons/SpinnerIcon";
import ModalContainer from "../components/ModalContainer"; import ModalContainer from "../components/ModalContainer";
import ChevronLeftIcon from "../components/icons/ChevronLeftIcon"; import ChevronLeftIcon from "../components/icons/ChevronLeftIcon";
import ChevronRightIcon from "../components/icons/ChevronRightIcon"; import ChevronRightIcon from "../components/icons/ChevronRightIcon";
import IUser from "../types/IUser"; import IUser from "../types/IUser";
import IError from "../types/IError"; import IError from "../types/IError";
import Schedule from "../components/Schedule";
function DashboardPage() { function DashboardPage() {
const { user } = useAuthStore(); const { user } = useAuthStore();
@@ -46,16 +41,12 @@ function DashboardPage() {
selectedDay, selectedDay,
setSelectedDay, setSelectedDay,
} = useStore(); } = useStore();
const [duration, setDuration] = useState<number>(); const [, setDuration] = useState<number>();
const [scheduledSessions, setScheduledSessions] = useState<any[]>(); const [scheduledSessions, setScheduledSessions] = useState<any[]>();
const [generatedScheduledSessions, setGeneratedScheduledSessions] = const [, setGeneratedScheduledSessions] = useState<any[]>();
useState<any[]>();
const [dateTimes, setDateTimes] = useState<Date[]>(); const [dateTimes, setDateTimes] = useState<Date[]>();
const [currentTime, setCurrentTime] = useState<string>( const [, setCurrentTime] = useState<string>(format(new Date(), "HH:mm"));
format(new Date(), "HH:mm") const [, setIsLoadingScheduledSessions] = useState(true);
);
const [isLoadingScheduledSessions, setIsLoadingScheduledSessions] =
useState(true);
const scheduledSessionsRef = useRef<HTMLDivElement>(null); const scheduledSessionsRef = useRef<HTMLDivElement>(null);
function selectNextDay() { function selectNextDay() {
@@ -231,33 +222,33 @@ function DashboardPage() {
if (useLoader) setIsLoadingScheduledSessions(false); if (useLoader) setIsLoadingScheduledSessions(false);
} }
async function updateScheduledSessionManager( // async function updateScheduledSessionManager(
scheduledSessionId: string, // scheduledSessionId: string,
managerId: string | null // managerId: string | null
) { // ) {
if (!company || !scheduledSessions) return; // if (!company || !scheduledSessions) return;
try { // try {
const result: any = await api // const result: any = await api
.put( // .put(
`companies/${company.id}/scheduled_sessions/${scheduledSessionId}`, // `companies/${company.id}/scheduled_sessions/${scheduledSessionId}`,
{ // {
json: { userId: managerId }, // json: { userId: managerId },
} // }
) // )
.json(); // .json();
setScheduledSessions( // setScheduledSessions(
scheduledSessions.map((scheduledSession) => // scheduledSessions.map((scheduledSession) =>
scheduledSession.id === result.id ? result : scheduledSession // scheduledSession.id === result.id ? result : scheduledSession
) // )
); // );
} catch (error) { // } catch (error) {
if (error instanceof Error) { // if (error instanceof Error) {
console.log("Error: ", error.message); // console.log("Error: ", error.message);
} // }
} // }
} // }
async function getSchedules() { async function getSchedules() {
if (!company || !selectedBuild) return; if (!company || !selectedBuild) return;
@@ -316,8 +307,8 @@ function DashboardPage() {
}, [selectedDay, selectedBuild]); }, [selectedDay, selectedBuild]);
return ( return (
<div className="main h-screen flex"> <div className="flex h-screen main">
<div className="left flex flex-col w-full"> <div className="flex flex-col w-full left">
<div className="flex bg-[#F0F1F2]"> <div className="flex bg-[#F0F1F2]">
<Menu /> <Menu />
{builds?.map((build) => ( {builds?.map((build) => (
@@ -358,7 +349,7 @@ function DashboardPage() {
</div> </div>
</div> </div>
<div className="flex bg-[#F2F2F2]"> {/* <div className="flex bg-[#F2F2F2]">
<div className="w-[84px] h-[40px] flex justify-center items-center text-sm font-semibold bg-white border-r border-b border-[#DAE0E5]"> <div className="w-[84px] h-[40px] flex justify-center items-center text-sm font-semibold bg-white border-r border-b border-[#DAE0E5]">
{currentTime} {currentTime}
</div> </div>
@@ -371,9 +362,9 @@ function DashboardPage() {
</div> </div>
</div> </div>
))} ))}
</div> </div> */}
<div {/* <div
ref={scheduledSessionsRef} ref={scheduledSessionsRef}
className={`overflow-y-auto overflow-x-hidden flex-1 bg-[#F2F2F2] border-r border-[#DAE0E5]`} className={`overflow-y-auto overflow-x-hidden flex-1 bg-[#F2F2F2] border-r border-[#DAE0E5]`}
> >
@@ -443,7 +434,14 @@ function DashboardPage() {
</div> </div>
) )
)} )}
</div> </div> */}
{selectedBuild?.sessionLimit && scheduledSessions && (
<Schedule
slots={selectedBuild.sessionLimit}
events={scheduledSessions}
/>
)}
</div> </div>
<div className="right w-[384px] flex flex-col"> <div className="right w-[384px] flex flex-col">
@@ -457,9 +455,9 @@ function DashboardPage() {
Статистика Статистика
</button> </button>
</div> </div>
<div className="overflow-y-auto overflow-x-hidden"> <div className="overflow-x-hidden overflow-y-auto">
<Calendar /> <Calendar />
<Schedules /> {/* <Schedules /> */}
<Managers /> <Managers />
</div> </div>
</div> </div>
+14
View File
@@ -0,0 +1,14 @@
import { create } from "zustand";
import IEvent from "../types/IEvent";
interface EventState {
events: IEvent[];
setEvents: (events: IEvent[]) => void;
}
const useEventStore = create<EventState>()((set) => ({
events: [],
setEvents: (events) => set({ events }),
}));
export default useEventStore;
+7
View File
@@ -0,0 +1,7 @@
interface IEvent {
slot: number;
startAt: Date;
endAt: Date;
}
export default IEvent;
+8
View File
@@ -16,10 +16,18 @@ const scheduledSessionSchema = new Schema(
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User",
}, },
slot: {
type: Number,
required: true,
},
startAt: { startAt: {
type: Date, type: Date,
required: true, required: true,
}, },
duration: {
type: Number,
required: true,
},
endAt: { endAt: {
type: Date, type: Date,
required: true, required: true,
+6 -3
View File
@@ -75,12 +75,12 @@ router.get("/:buildId", async (req, res) => {
}); });
router.post("/", 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({ return res.json({
status: "error", 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) { if (duration) {
const scheduledSessions = await ScheduledSession.find({ const scheduledSessions = await ScheduledSession.find({
buildId, buildId,
slot,
startAt: { startAt: {
$gte: startOfDay(startAtISO), $gte: startOfDay(startAtISO),
$lte: endOfDay(startAtISO), $lte: endOfDay(startAtISO),
@@ -141,7 +142,9 @@ router.post("/", async (req, res) => {
const scheduledSession = await ScheduledSession.create({ const scheduledSession = await ScheduledSession.create({
buildId, buildId,
slot,
startAt: startAtISO, startAt: startAtISO,
duration,
endAt: endAtISO, endAt: endAtISO,
}); });