diff --git a/client/package.json b/client/package.json
index 2fe8100..17bf2f3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -13,6 +13,7 @@
"date-fns": "^2.30.0",
"ky": "^1.0.1",
"react": "^18.2.0",
+ "react-datepicker": "^4.20.0",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-router-dom": "^6.15.0",
@@ -21,6 +22,7 @@
},
"devDependencies": {
"@types/react": "^18.2.15",
+ "@types/react-datepicker": "^4.19.0",
"@types/react-dom": "^18.2.7",
"@types/react-google-recaptcha": "^2.1.6",
"@types/react-transition-group": "^4.4.7",
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index 3b21200..ea1de92 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -6,6 +6,7 @@ interface ButtonProps {
size?: "small" | "medium";
disabled?: boolean;
loading?: boolean;
+ onlyIcon?: boolean;
icon?: React.ReactNode;
children?: React.ReactNode;
className?: string;
@@ -18,6 +19,7 @@ function Button({
size = "small",
disabled,
loading = false,
+ onlyIcon,
icon,
children,
className,
@@ -33,8 +35,14 @@ function Button({
(color === "secondary" &&
"bg-[#F0F1F2] text-[#77828C] active:text-white")
} ${
- (size === "small" && `h-[32px] px-3 py-2.5 text-xs`) ||
- (size === "medium" && "h-[40px] px-6 py-3 text-sm")
+ (size === "small" &&
+ `h-8 ${
+ onlyIcon ? "p-1" : icon ? "pl-2 pr-4 py-1" : "px-3 py-2.5"
+ } text-xs`) ||
+ (size === "medium" &&
+ `h-10 ${
+ onlyIcon ? "p-2" : icon ? "pl-4 pr-6 py-2" : "px-6 py-3"
+ } text-sm`)
} ${className}`}
>
{loading ? : icon} {children}
diff --git a/client/src/components/Card.tsx b/client/src/components/Card.tsx
index 10846a9..de06748 100644
--- a/client/src/components/Card.tsx
+++ b/client/src/components/Card.tsx
@@ -44,8 +44,8 @@ function Card({
)
.json();
- const filteredManagers = managers.filter(
- (manager) => !result.includes(manager.id)
+ const filteredManagers = managers.filter((manager) =>
+ result.includes(manager.id)
);
setAvailableManagers(filteredManagers);
@@ -62,10 +62,10 @@ function Card({
{client.name}
{manager ? (
-
-
-
- Сеанс начат
+
) : (
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index 388caa4..352cd57 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -1,11 +1,16 @@
+/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
interface InputProps {
- type?: "text" | "email" | "password";
+ type?: "text" | "email" | "password" | "time";
placeholder?: string;
autoFocus?: boolean;
required?: boolean;
+ readOnly?: boolean;
+ defaultValue?: string;
+ className?: string;
handleChange?: (value: string) => void;
+ handleFocus?: () => void;
}
function Input({
@@ -13,15 +18,18 @@ function Input({
placeholder,
autoFocus,
required,
+ readOnly,
+ defaultValue,
+ className,
handleChange,
+ handleFocus,
}: InputProps) {
- const [value, setValue] = useState
("");
+ const [value, setValue] = useState(defaultValue);
useEffect(() => {
- if (handleChange) {
+ if (value && handleChange) {
handleChange(value);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
@@ -30,9 +38,11 @@ function Input({
placeholder={placeholder}
autoFocus={autoFocus}
required={required}
- value={value}
+ readOnly={readOnly}
+ value={defaultValue}
onChange={(e) => setValue(e.target.value)}
- className="px-3 py-2.5 outline-none rounded-lg border border-[#DAE0E5]"
+ onFocus={handleFocus}
+ className={`px-3 py-2.5 outline-none rounded-lg border border-[#DAE0E5] focus:border-[#49A1F5] transition-colors ${className}`}
/>
);
}
diff --git a/client/src/components/ModalContainer.tsx b/client/src/components/ModalContainer.tsx
new file mode 100644
index 0000000..aad54e6
--- /dev/null
+++ b/client/src/components/ModalContainer.tsx
@@ -0,0 +1,31 @@
+import { Transition } from "react-transition-group";
+import useModalStore from "../stores/useModalStore";
+
+function ModalContainer() {
+ const [modal, setModal] = useModalStore((state) => [
+ state.modal,
+ state.setModal,
+ ]);
+
+ return (
+
+ {(state) => (
+ setModal(null)}
+ className={`min-h-screen p-8 absolute top-0 left-0 w-full flex justify-center items-center bg-black bg-opacity-30 overflow-auto cursor-pointer transition-opacity ${state}`}
+ >
+
e.stopPropagation()} className="cursor-default">
+ {modal}
+
+
+ )}
+
+ );
+}
+
+export default ModalContainer;
diff --git a/client/src/components/Select.tsx b/client/src/components/Select.tsx
new file mode 100644
index 0000000..bef933b
--- /dev/null
+++ b/client/src/components/Select.tsx
@@ -0,0 +1,73 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import { useEffect, useState } from "react";
+import Input from "./Input";
+import CheckIcon from "./icons/CheckIcon";
+import { Transition } from "react-transition-group";
+import useOutsideClick from "../hooks/useOutsideClick";
+import ChevronDown from "./icons/ChevronDown";
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+interface SelectProps {
+ defaultValue?: number;
+ options?: any[];
+ handleChange: (value: number) => void;
+}
+
+function Select({ defaultValue, options, handleChange }: SelectProps) {
+ const [value, setValue] = useState(defaultValue);
+ const [isShow, setIsShow] = useState(false);
+ const selectRef = useOutsideClick(() => setIsShow(false));
+
+ function handleClick(option: { [key: string]: any }) {
+ setValue(option.value);
+ setIsShow(false);
+ }
+
+ useEffect(() => {
+ if (!value) return;
+ handleChange(value);
+ }, [value]);
+
+ return (
+
+
setIsShow(true)}
+ defaultValue={options?.find((option) => option.value === value).text}
+ className="w-full cursor-pointer"
+ />
+
+
+ {(state) => (
+
+ {options?.map((option, index) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export default Select;
diff --git a/client/src/components/icons/CloseIcon.tsx b/client/src/components/icons/CloseIcon.tsx
new file mode 100644
index 0000000..a8016ac
--- /dev/null
+++ b/client/src/components/icons/CloseIcon.tsx
@@ -0,0 +1,21 @@
+function CloseIcon() {
+ return (
+
+ );
+}
+
+export default CloseIcon;
diff --git a/client/src/components/modals/CreateSchedule.tsx b/client/src/components/modals/CreateSchedule.tsx
new file mode 100644
index 0000000..0e3176c
--- /dev/null
+++ b/client/src/components/modals/CreateSchedule.tsx
@@ -0,0 +1,274 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable no-irregular-whitespace */
+import { ru } from "date-fns/locale";
+import Button from "../Button";
+import CloseIcon from "../icons/CloseIcon";
+import DatePicker from "react-datepicker";
+import "react-datepicker/dist/react-datepicker.css";
+import Label from "../Label";
+import { useEffect, useState } from "react";
+import Select from "../Select";
+import useModalStore from "../../stores/useModalStore";
+import Input from "../Input";
+import {
+ addMonths,
+ addWeeks,
+ differenceInDays,
+ eachMinuteOfInterval,
+ format,
+ parse,
+} from "date-fns";
+
+function CreateSchedule() {
+ const setModal = useModalStore((state) => state.setModal);
+ const [selectedScheduleDate, setSelectedScheduleDate] = useState(
+ new Date()
+ );
+ const [selectedScheduleDuration, setSelectedScheduleDuration] =
+ useState(3);
+ const [scheduleStartDate, setScheduleStartDate] = useState();
+ const [scheduleEndDate, setScheduleEndDate] = useState();
+ const [selectedSessionDuration, setSelectedSessionDuration] =
+ useState(30);
+ const [selectedSessionsBreak, setSelectedSessionsBreak] = useState(5);
+ const [selectedWorkTimeStart, setSelectedWorkTimeStart] =
+ useState("10:00");
+ const [selectedWorkTimeEnd, setSelectedWorkTimeEnd] =
+ useState("20:00");
+ const [sessionsCount, setSessionsCount] = useState();
+
+ useEffect(() => {
+ if (!selectedScheduleDate || !selectedScheduleDuration) return;
+
+ setScheduleStartDate(selectedScheduleDate);
+ setScheduleEndDate(
+ selectedScheduleDuration !== 3
+ ? addWeeks(selectedScheduleDate, selectedScheduleDuration)
+ : addMonths(selectedScheduleDate, 1)
+ );
+ }, [selectedScheduleDate, selectedScheduleDuration]);
+
+ function calculateSessionCount() {
+ if (!scheduleStartDate || !scheduleEndDate) return;
+
+ const sessionsPerDay = eachMinuteOfInterval(
+ {
+ start: parse(selectedWorkTimeStart, "HH:mm", new Date()),
+ end: parse(selectedWorkTimeEnd, "HH:mm", new Date()),
+ },
+ { step: selectedSessionDuration + selectedSessionsBreak }
+ ).length;
+
+ const days = differenceInDays(scheduleEndDate, scheduleStartDate);
+
+ setSessionsCount(days * sessionsPerDay);
+ }
+
+ useEffect(() => {
+ calculateSessionCount();
+ }, [
+ scheduleStartDate,
+ scheduleEndDate,
+ selectedWorkTimeEnd,
+ selectedSessionDuration,
+ selectedSessionsBreak,
+ ]);
+
+ return (
+
+
+
Создание расписания
+
}
+ className="bg-transparent"
+ handleClick={() => setModal(null)}
+ />
+
+
+
+
+
+
+
Срок действия расписания
+
+
+
+
+
+
+
+ setSelectedScheduleDate(value)}
+ locale={ru}
+ dateFormat="dd.MM.yyyy"
+ className="mt-1 font-normal px-3 py-2.5 rounded-lg border border-[#DAE0E5] text-black w-full outline-none focus:border-[#49A1F5] transition-colors"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setSelectedWorkTimeStart(value)}
+ className="w-[137px]"
+ />
+
+
-
+
+
+ setSelectedWorkTimeEnd(value)}
+ className="w-[137px]"
+ />
+
+
+
+
+
+
+
+
+
Продолжительность сеанса
+
+ Влияет на количество ежедневно проводимых сессий
+
+
+
+
+
+
+
+
+
+
Перерыв между сеансами
+
+ Нужен, чтобы менеджеры успевали завершать один сеанс и
+ переходить к следующему
+
+
+
+
+
+
+
+
+
+ {/*
*/}
+
+
Предварительный просмотр
+
+ {scheduleStartDate && scheduleEndDate && (
+
+ {format(scheduleStartDate, "dd.MM.yyyy")}
+ -
+ {format(scheduleEndDate, "dd.MM.yyyy")}
+
+ )}
+
+
+
+ Количество сеансов
+
+
{sessionsCount}
+
+
+
+ Длительность сеанса
+
+
{selectedSessionDuration} мин.
+
+
+
Между сеансами
+
{selectedSessionsBreak} мин.
+
+
+
Время работы
+
+ {selectedWorkTimeStart} - {selectedWorkTimeEnd}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CreateSchedule;
diff --git a/client/src/index.css b/client/src/index.css
index fdf6f44..9fa9c09 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -1,4 +1,3 @@
-/* @import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;600&display=swap"); */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap");
@tailwind base;
@@ -6,11 +5,12 @@
@tailwind utilities;
body {
- /* font-family: "Manrope", sans-serif; */
font-family: "Inter", sans-serif;
color: #111c26;
}
+/* Scrollbar */
+
*::-webkit-scrollbar {
width: 8px;
}
@@ -25,6 +25,8 @@ body {
border-width: 2px;
}
+/* Transition */
+
.entering {
opacity: 1;
}
@@ -40,3 +42,17 @@ body {
.exited {
opacity: 0;
}
+
+/* DatePicker */
+
+.react-datepicker {
+ font-family: inherit !important;
+}
+
+.react-datepicker__day--selected {
+ background-color: #49a1f5 !important;
+}
+
+.react-datepicker__day--selected:hover {
+ background-color: #4190db !important;
+}
diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx
index 843e796..2ce250b 100644
--- a/client/src/pages/DashboardPage.tsx
+++ b/client/src/pages/DashboardPage.tsx
@@ -20,6 +20,9 @@ import Button from "../components/Button";
import { ru } from "date-fns/locale";
import { Transition } from "react-transition-group";
import SpinnerIcon from "../components/icons/SpinnerIcon";
+import useModalStore from "../stores/useModalStore";
+import ModalContainer from "../components/ModalContainer";
+import CreateSchedule from "../components/modals/CreateSchedule";
function DashboardPage() {
const [user, setAccessToken] = useAuthStore((state) => [
@@ -49,6 +52,7 @@ function DashboardPage() {
const [isLoadingScheduledSessions, setIsLoadingScheduledSessions] =
useState(true);
const scheduledSessionsRef = useRef(null);
+ const setModal = useModalStore((state) => state.setModal);
// const [selectedDate, setSelectedDate] = useState(
// format(new Date(), "d MMMM, yyyy", { locale: ru })
@@ -288,11 +292,13 @@ function DashboardPage() {
handleClick={selectPrevDay}
icon={}
color="secondary"
+ onlyIcon
/>
}
color="secondary"
+ onlyIcon
/>
@@ -375,7 +381,6 @@ function DashboardPage() {
}}
manager={selectedManager}
managers={managers}
- // managers={managers}
handleSelect={(scheduledSessionId, managerId) =>
updateScheduledSessionManager(
scheduledSessionId,
@@ -417,9 +422,27 @@ function DashboardPage() {
-
...
+
+
+
+
Расписание
+
+
+
+
+
+
+
);
}
diff --git a/client/src/pages/RegistrationPage.tsx b/client/src/pages/RegistrationPage.tsx
index b4c2ed0..20ac518 100644
--- a/client/src/pages/RegistrationPage.tsx
+++ b/client/src/pages/RegistrationPage.tsx
@@ -10,12 +10,12 @@ function RegistrationPage() {
-