From b907020d1977effa81d847a5a0bf65d59ca776de Mon Sep 17 00:00:00 2001 From: inmake Date: Sat, 1 Mar 2025 18:59:36 +0500 Subject: [PATCH] upd --- client/src/components/Button.tsx | 4 +- client/src/components/DatePicker.tsx | 40 ++-- client/src/components/MultiSelect.tsx | 84 +++++++ client/src/components/Schedule.tsx | 214 ++++++++++++------ client/src/components/Select3.tsx | 2 +- client/src/components/Timeline.tsx | 4 +- .../src/components/icons/CheckboxChecked.tsx | 31 +++ .../components/icons/CheckboxUnchecked.tsx | 24 ++ .../icons/InternetSpeedHighIcon.tsx | 20 ++ .../components/icons/InternetSpeedLowIcon.tsx | 28 +++ .../icons/InternetSpeedMediumIcon.tsx | 24 ++ client/src/components/icons/SpinnerIcon.tsx | 28 +-- .../src/components/modals/AddManagerModal.tsx | 41 +++- client/src/components/modals/CompanyModal.tsx | 2 +- server/src/routes/addManager.ts | 3 +- server/src/routes/scheduledSessions.ts | 208 +++++++++-------- 16 files changed, 544 insertions(+), 213 deletions(-) create mode 100644 client/src/components/MultiSelect.tsx create mode 100644 client/src/components/icons/CheckboxChecked.tsx create mode 100644 client/src/components/icons/CheckboxUnchecked.tsx create mode 100644 client/src/components/icons/InternetSpeedHighIcon.tsx create mode 100644 client/src/components/icons/InternetSpeedLowIcon.tsx create mode 100644 client/src/components/icons/InternetSpeedMediumIcon.tsx diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx index 0bf635c..dfc58f2 100644 --- a/client/src/components/Button.tsx +++ b/client/src/components/Button.tsx @@ -1,4 +1,4 @@ -import RotateIcon from "./icons/RotateIcon"; +import SpinnerIcon from "./icons/SpinnerIcon"; interface Props { type?: "button" | "submit" | "reset"; @@ -72,7 +72,7 @@ function Button({ > {loading ? ( - + ) : ( <> diff --git a/client/src/components/DatePicker.tsx b/client/src/components/DatePicker.tsx index 953e3d6..813763a 100644 --- a/client/src/components/DatePicker.tsx +++ b/client/src/components/DatePicker.tsx @@ -20,14 +20,16 @@ import { ru } from "date-fns/locale"; import { useClickAway } from "@uidotdev/usehooks"; import CalendarIcon from "./icons/CalendarIcon"; import ChevronDownIcon from "./icons/ChevronDownIcon"; +import Button from "./Button"; interface Props { + label?: string; defaultValue?: Date; startDate?: Date; onChange?: (date: Date) => void; } -function DatePicker({ defaultValue, startDate, onChange }: Props) { +function DatePicker({ label, defaultValue, startDate, onChange }: Props) { const [value, setValue] = useState( (defaultValue && startOfDay(defaultValue)) || (startDate && startOfDay(startDate)) || @@ -52,12 +54,14 @@ function DatePicker({ defaultValue, startDate, onChange }: Props) { function handleClick(date: Date) { setValue(date); onChange && onChange(date); + setIsShowCalendar(false); } return ( -
+
+ {label &&

{label}

}
setIsShowCalendar((prev) => { setSelectedMonth(startOfMonth(value)); @@ -66,8 +70,10 @@ function DatePicker({ defaultValue, startDate, onChange }: Props) { } >
-
- +
+ + + {format(value, "dd.MM.yyyy")}
@@ -76,31 +82,31 @@ function DatePicker({ defaultValue, startDate, onChange }: Props) {
{isShowCalendar && ( -
+
- + />

{_.capitalize( format(selectedMonth, "LLLL, yyyy", { locale: ru }) )}

- + />
{["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"].map((value, index) => (
{value}
diff --git a/client/src/components/MultiSelect.tsx b/client/src/components/MultiSelect.tsx new file mode 100644 index 0000000..2a65367 --- /dev/null +++ b/client/src/components/MultiSelect.tsx @@ -0,0 +1,84 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect, useState } from "react"; +import ChevronDownIcon from "./icons/ChevronDownIcon"; +import CheckboxChecked from "./icons/CheckboxChecked"; +import CheckboxUnchecked from "./icons/CheckboxUnchecked"; +import { useClickAway } from "@uidotdev/usehooks"; + +interface Props { + label?: string; + options: string[]; + defaultSelectedOptions?: string[]; + onChange: (options: string[]) => void; +} + +function MultiSelect({ + label, + options, + defaultSelectedOptions = [], + onChange, +}: Props) { + const [opened, setOpened] = useState(false); + const [selectedOptions, setSelectedOptions] = useState( + defaultSelectedOptions + ); + + const ref = useClickAway(() => setOpened(false)); + + function handleClickOption(option: string) { + if (!selectedOptions.includes(option)) { + setSelectedOptions((prev) => [...prev, option]); + } else { + setSelectedOptions((prev) => + prev.filter((selectedOption) => selectedOption !== option) + ); + } + } + + useEffect(() => { + onChange(selectedOptions); + }, [selectedOptions]); + + return ( +
+
+ {label &&

{label}

} +
setOpened((prev) => !prev)} + > + {selectedOptions.length > 0 ? ( +

{selectedOptions.join(", ")}

+ ) : ( +

Не выбрано

+ )} +
+ +
+
+
+ {opened && ( +
+ {options.map((option, index) => ( +
handleClickOption(option)} + > +
+ {selectedOptions.includes(option) ? ( + + ) : ( + + )} +
+

{option}

+
+ ))} +
+ )} +
+ ); +} + +export default MultiSelect; diff --git a/client/src/components/Schedule.tsx b/client/src/components/Schedule.tsx index 9866321..8dd948f 100644 --- a/client/src/components/Schedule.tsx +++ b/client/src/components/Schedule.tsx @@ -3,17 +3,18 @@ import { useEffect, useState } from "react"; import Timeline from "./Timeline"; import { + differenceInMinutes, format, getDate, getHours, getMonth, + parse, parseISO, setDate, setHours, setMonth, startOfDay, } from "date-fns"; -// import useEventStore from "../stores/useEventStore"; import Button from "./Button"; import CloseIcon from "./icons/CloseIcon"; import Input from "./Input"; @@ -23,6 +24,8 @@ import useStore from "../stores/useStore"; import IScheduledSession from "../types/IScheduledSession"; import toast from "react-hot-toast"; import Select3 from "./Select3"; +import DatePicker from "./DatePicker"; +import { HTTPError } from "ky"; interface Props { selectedDay: Date; @@ -33,13 +36,16 @@ interface Props { function Schedule({ selectedDay, slots, events }: Props) { const { company, builds, selectedBuild, setSelectedBuild } = useStore(); const [draftMode, setDraftMode] = useState(false); - const [slot, setSlot] = useState(); + const [slot, setSlot] = useState(1); const [startAt, setStartAt] = useState(); const [duration, setDuration] = useState(); const [email, setEmail] = useState(""); const [name, setName] = useState(""); const [phone, setPhone] = useState(""); const [dateForInstantStart, setDateForInstantStart] = useState(); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [instantDuration, setInstantDuration] = useState(30); function handleChangeSlot(slot: number) { setSlot(slot); @@ -53,8 +59,6 @@ function Schedule({ selectedDay, slots, events }: Props) { let newStartAt = setMonth(startAt, getMonth(selectedDay)); newStartAt = setDate(newStartAt, getDate(selectedDay)); - // console.log("newStartAt", newStartAt); - setStartAt(newStartAt); setDateForInstantStart(undefined); } @@ -69,19 +73,21 @@ function Schedule({ selectedDay, slots, events }: Props) { // console.log("duration", duration); if (!slot || !startAt || !duration) return; + if (!dateForInstantStart && !startAt && !duration) return; - await addSchesuledSession(); + try { + const result: any = await addSchesuledSession(); - // setEvents([ - // ...events, - // { - // slot, - // startAt, - // endAt: addMinutes(startAt, duration), - // }, - // ]); + if (!dateForInstantStart) { + toast.success(`Сеанс успешно запланирован!`, { duration: 3000 }); + } else { + window.open(`${result.url}?admin=true`); + } - setDraftMode(false); + setDraftMode(false); + } catch (error) { + toast.error((await (error as HTTPError).response.json()).message); + } } function handleClickCancel() { @@ -89,36 +95,22 @@ function Schedule({ selectedDay, slots, events }: Props) { } async function addSchesuledSession() { - // setIsLoading(true); - - try { - const result: any = await api - .post(`scheduledSessions`, { - json: { - companyId: company!.id, - buildId: selectedBuild?.id, - slot, - startAt, - duration, - client: { - email, - phone, - name, - }, + return await api + .post(`scheduledSessions`, { + json: { + companyId: company!.id, + buildId: selectedBuild?.id, + slot, + startAt, + duration, + client: { + email, + phone, + name, }, - }) - .json(); - - if (!dateForInstantStart) return; - - window.open(`${result.url}?admin=true`); - } catch (error) { - // console.log(error); - toast.error((error as Error).message); - } - - // setIsLoading(false); - // setModal(null); + }, + }) + .json(); } function countWorkingHours() { @@ -130,6 +122,12 @@ function Schedule({ selectedDay, slots, events }: Props) { return endHour - startHour; } + function handleClickScheduleDemo() { + setDateForInstantStart(undefined); + setStartAt(new Date()); + setDraftMode(true); + } + useEffect(() => { if (!builds) return; @@ -137,13 +135,48 @@ function Schedule({ selectedDay, slots, events }: Props) { }, [builds]); useEffect(() => { - if (!dateForInstantStart) return; + if (!dateForInstantStart) { + setDuration(undefined); + return; + } setSlot(1); setStartAt(dateForInstantStart); - setDuration(30); + setDuration(instantDuration); setDraftMode(true); - }, [dateForInstantStart]); + }, [dateForInstantStart, instantDuration]); + + useEffect(() => { + if (!startTime || !endTime || !startAt) return; + + setStartAt(parse(startTime, "HH:mm", startAt)); + + const duration = differenceInMinutes( + parse(endTime, "HH:mm", startAt), + parse(startTime, "HH:mm", startAt) + ); + + if (duration < 10) { + setDuration(undefined); + return; + } + + setDuration(duration); + + console.log(duration, startAt); + }, [startTime, endTime]); + + useEffect(() => { + console.log(startAt, duration, slot); + + if (!startAt || !startTime) return; + + const startAtTime = format(startAt, "HH:mm:ss"); + + if (startAtTime !== "00:00:00") return; + + setStartAt(parse(startTime, "HH:mm", startAt)); + }, [startAt]); useEffect(() => { function handleEscKey(e: KeyboardEvent) { @@ -235,26 +268,11 @@ function Schedule({ selectedDay, slots, events }: Props) { onClick={handleClickCancel} />
-
-

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

-
+
+

Детали демонстрации

+
-

Выберите ЖК

- {/* ({ - value: build.build, - text: build.name, - }))} - onChange={(e: ChangeEvent) => - setSelectedBuild( - builds.find( - (build) => - build.build === e.target.selectedOptions[0].value - )! - ) - } - /> */} +

Проект

-
-

Дата и время

-

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

-
-
-

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

-

{duration} мин.

+
+

Сервер

+ + (i + 1).toString() + )} + onSelect={(option) => setSlot(+option)} + />
+ {dateForInstantStart && ( +
+

Длительность (минут)

+ setInstantDuration(Number(option))} + /> +
+ )} + {!dateForInstantStart && ( + <> + setStartAt(date)} + /> +
+
+

Начало

+ setStartTime(value)} + className="w-full" + /> +
+

-

+
+

Конец

+ setEndTime(value)} + className="w-full" + /> +
+
+ + )}
{!dateForInstantStart && ( -
+

Клиент

@@ -331,13 +390,20 @@ function Schedule({ selectedDay, slots, events }: Props) {
)} -
+
+
); diff --git a/client/src/components/Select3.tsx b/client/src/components/Select3.tsx index 3911dad..7d591f3 100644 --- a/client/src/components/Select3.tsx +++ b/client/src/components/Select3.tsx @@ -51,7 +51,7 @@ function Select3({ required, defaultOption, options, onSelect }: Props) {
{showOptions && ( -
+
{options.map((option, index) => (