upd
This commit is contained in:
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# VITE_API_URL=http://localhost:3001
|
# VITE_API_URL=http://localhost:3001
|
||||||
# VITE_API_URL=http://192.168.1.171:3001
|
VITE_API_URL=http://192.168.1.171:3001
|
||||||
VITE_API_URL=https://crm.stream.graff.tech/api
|
# VITE_API_URL=https://crm.stream.graff.tech/api
|
||||||
VITE_STREAM_URL=https://stream.graff.tech
|
VITE_STREAM_URL=https://stream.graff.tech
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"ky": "^1.0.1",
|
"ky": "^1.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-calendar": "^5.0.0",
|
||||||
"react-datepicker": "^4.20.0",
|
"react-datepicker": "^4.20.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
@@ -21,6 +24,7 @@
|
|||||||
"zustand": "^4.4.1"
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.4",
|
||||||
"@types/node": "^20.8.7",
|
"@types/node": "^20.8.7",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-datepicker": "^4.19.0",
|
"@types/react-datepicker": "^4.19.0",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function Button({
|
|||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
type={type}
|
type={type}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={`relative outline-none rounded-lg transition-colors font-semibold flex justify-center items-center gap-1 active:bg-[#49A1F5] disabled:bg-[#F2F2F2] disabled:text-[#CCCCCC] ${
|
className={`relative outline-none rounded-lg transition-all font-semibold flex justify-center items-center gap-1 active:bg-[#49A1F5] disabled:bg-[#F2F2F2] disabled:text-[#CCCCCC] ${
|
||||||
(color === "primary" && "bg-[#49A1F5] text-white hover:bg-[#4190DB]") ||
|
(color === "primary" && "bg-[#49A1F5] text-white hover:bg-[#4190DB]") ||
|
||||||
(color === "secondary" &&
|
(color === "secondary" &&
|
||||||
"bg-[#F0F1F2] text-[#77828C] hover:bg-[#E6ECF2] active:text-white") ||
|
"bg-[#F0F1F2] text-[#77828C] hover:bg-[#E6ECF2] active:text-white") ||
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: string[];
|
||||||
|
selected?: string[];
|
||||||
|
onChange: (options: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChoiceChips({ options, selected, onChange }: Props) {
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<string[]>(
|
||||||
|
selected && selected.length > 0
|
||||||
|
? options.filter((option) => selected.includes(option))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClick(option: string) {
|
||||||
|
let newSelectedOptions: string[];
|
||||||
|
|
||||||
|
if (!selectedOptions.includes(option)) {
|
||||||
|
newSelectedOptions = [...selectedOptions, option];
|
||||||
|
} else {
|
||||||
|
newSelectedOptions = selectedOptions.filter(
|
||||||
|
(selectedOption) => selectedOption !== option
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedOptions(newSelectedOptions);
|
||||||
|
onChange(newSelectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 flex-wrap ">
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={` text-xs font-semibold px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
!selectedOptions.includes(option)
|
||||||
|
? "text-[#77828C] bg-[#F0F1F2] hover:text-[#4C5359] hover:bg-[#E6ECF2]"
|
||||||
|
: "text-white bg-[#49A1F5] hover:bg-[#4190DB]"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleClick(option)}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChoiceChips;
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import {
|
||||||
|
addMonths,
|
||||||
|
format,
|
||||||
|
getDaysInMonth,
|
||||||
|
getISODay,
|
||||||
|
isAfter,
|
||||||
|
isEqual,
|
||||||
|
isToday,
|
||||||
|
setDate,
|
||||||
|
startOfDay,
|
||||||
|
startOfMonth,
|
||||||
|
subMonths,
|
||||||
|
} from "date-fns";
|
||||||
|
import { useState } from "react";
|
||||||
|
import ChevronLeftIcon from "./icons/ChevronLeftIcon";
|
||||||
|
import ChevronRightIcon from "./icons/ChevronRightIcon";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { ru } from "date-fns/locale";
|
||||||
|
import { useClickAway } from "@uidotdev/usehooks";
|
||||||
|
import CalendarIcon from "./icons/CalendarIcon";
|
||||||
|
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
defaultValue?: Date;
|
||||||
|
startDate?: Date;
|
||||||
|
onChange?: (date: Date) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DatePicker({ defaultValue, startDate, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState<Date>(
|
||||||
|
startDate && isAfter(startOfDay(startDate), new Date())
|
||||||
|
? startOfDay(startDate)
|
||||||
|
: defaultValue || startOfDay(new Date())
|
||||||
|
);
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState(startOfMonth(value));
|
||||||
|
const [isShowCalendar, setIsShowCalendar] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const ref = useClickAway<HTMLDivElement>(() => {
|
||||||
|
setIsShowCalendar(false);
|
||||||
|
setSelectedMonth(startOfMonth(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectPrevMonth() {
|
||||||
|
setSelectedMonth(subMonths(selectedMonth, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNextMonth() {
|
||||||
|
setSelectedMonth(addMonths(selectedMonth, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(date: Date) {
|
||||||
|
setValue(date);
|
||||||
|
onChange && onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="w-[296px] relative space-y-1">
|
||||||
|
<div
|
||||||
|
className="p-2 bg-white border border-[#DAE0E5] rounded-lg cursor-pointer text-[#77828C]"
|
||||||
|
onClick={() =>
|
||||||
|
setIsShowCalendar((prev) => {
|
||||||
|
setSelectedMonth(startOfMonth(value));
|
||||||
|
return !prev;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CalendarIcon />
|
||||||
|
{format(value, "dd.MM.yyyy")}
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isShowCalendar && (
|
||||||
|
<div className="px-4 py-3 bg-white rounded-lg shadow-md absolute">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
className="text-[#77828C] p-1 hover:bg-[#E6ECF2] rounded-lg transition-colors"
|
||||||
|
onClick={selectPrevMonth}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{_.capitalize(
|
||||||
|
format(selectedMonth, "LLLL, yyyy", { locale: ru })
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="text-[#77828C] p-1 hover:bg-[#E6ECF2] rounded-lg transition-colors"
|
||||||
|
onClick={selectNextMonth}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-7 gap-2">
|
||||||
|
{["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"].map((value, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-8 h-8 flex items-center justify-center text-xs font-semibold"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-7 gap-2">
|
||||||
|
{Array.from({ length: getISODay(selectedMonth) - 1 }).map(
|
||||||
|
(_, index) => (
|
||||||
|
<div key={index} className="w-8 h-8"></div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{Array.from({ length: getDaysInMonth(selectedMonth) }).map(
|
||||||
|
(_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
disabled={
|
||||||
|
startDate &&
|
||||||
|
isAfter(
|
||||||
|
startOfDay(startDate),
|
||||||
|
setDate(selectedMonth, index + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs disabled:text-[#DAE0E5] ${
|
||||||
|
isEqual(setDate(selectedMonth, index + 1), value)
|
||||||
|
? "bg-[#49A1F5] text-white transition-colors"
|
||||||
|
: "enabled:hover:bg-[#E6ECF2]"
|
||||||
|
} ${
|
||||||
|
isToday(setDate(selectedMonth, index + 1))
|
||||||
|
? "border border-[#49A1F5]"
|
||||||
|
: ""
|
||||||
|
} `}
|
||||||
|
onClick={() => handleClick(setDate(selectedMonth, index + 1))}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatePicker;
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
|
import Button from "./Button";
|
||||||
|
import PlusIcon from "./icons/PlusIcon";
|
||||||
|
|
||||||
function EmptyCard() {
|
function EmptyCard() {
|
||||||
return (
|
return (
|
||||||
<div className="w-[264px] h-[128px] text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5]"></div>
|
<div className="w-[264px] h-[128px] text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5] flex items-center justify-center group">
|
||||||
|
<Button
|
||||||
|
color="tertiary"
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
className="group-hover:opacity-100 opacity-0"
|
||||||
|
>
|
||||||
|
Добавить сеанс
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Input from "./Input";
|
|||||||
import CheckIcon from "./icons/CheckIcon";
|
import CheckIcon from "./icons/CheckIcon";
|
||||||
import { Transition } from "react-transition-group";
|
import { Transition } from "react-transition-group";
|
||||||
import useOutsideClick from "../hooks/useOutsideClick";
|
import useOutsideClick from "../hooks/useOutsideClick";
|
||||||
import ChevronDown from "./icons/ChevronDown";
|
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
@@ -40,7 +40,7 @@ function Select({ defaultValue, options, handleChange }: SelectProps) {
|
|||||||
onClick={() => setIsShow(true)}
|
onClick={() => setIsShow(true)}
|
||||||
className="absolute right-0 top-0 px-3 py-2 text-[#77828C]"
|
className="absolute right-0 top-0 px-3 py-2 text-[#77828C]"
|
||||||
>
|
>
|
||||||
<ChevronDown />
|
<ChevronDownIcon />
|
||||||
</button>
|
</button>
|
||||||
<Transition in={isShow} timeout={150} mountOnEnter unmountOnExit>
|
<Transition in={isShow} timeout={150} mountOnEnter unmountOnExit>
|
||||||
{(state) => (
|
{(state) => (
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const SVGComponent = () => (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x={4.75}
|
||||||
|
y={4.75}
|
||||||
|
width={14.5}
|
||||||
|
height={15.5}
|
||||||
|
rx={2.25}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={4.75}
|
||||||
|
y={4.75}
|
||||||
|
width={14.5}
|
||||||
|
height={4.5}
|
||||||
|
rx={2.25}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
<rect x={8} y={15} width={2} height={2} rx={0.5} fill="currentColor" />
|
||||||
|
<rect x={14} y={12} width={2} height={2} rx={0.5} fill="currentColor" />
|
||||||
|
<rect x={8} y={12} width={2} height={2} rx={0.5} fill="currentColor" />
|
||||||
|
<rect x={11} y={12} width={2} height={2} rx={0.5} fill="currentColor" />
|
||||||
|
<rect x={11} y={15} width={2} height={2} rx={0.5} fill="currentColor" />
|
||||||
|
<rect x={14} y={15} width={2} height={2} rx={0.5} fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
export default SVGComponent;
|
||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
function ChevronDown() {
|
function ChevronDownIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width={24}
|
||||||
height="24"
|
height={24}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -10,7 +10,7 @@ function ChevronDown() {
|
|||||||
<path
|
<path
|
||||||
d="M7 11L12 16L17 11"
|
d="M7 11L12 16L17 11"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth={1.5}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
@@ -18,4 +18,4 @@ function ChevronDown() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChevronDown;
|
export default ChevronDownIcon;
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
function ChevronLeftIcon() {
|
const SVGComponent = () => (
|
||||||
return (
|
<svg
|
||||||
<svg
|
width={24}
|
||||||
width="24"
|
height={24}
|
||||||
height="24"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
>
|
<path
|
||||||
<path
|
d="M13 7L8 12L13 17"
|
||||||
d="M13 7L8 12L13 17"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth={1.5}
|
||||||
strokeWidth="2"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
/>
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
);
|
||||||
);
|
export default SVGComponent;
|
||||||
}
|
|
||||||
|
|
||||||
export default ChevronLeftIcon;
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
function PlusIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.0001 11.9998H18.0001M12.0001 11.9998L12.0001 5.99997M12.0001 11.9998L12.0001 18M12.0001 11.9998H6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlusIcon;
|
||||||
+104
-128
@@ -1,73 +1,72 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
/* eslint-disable no-irregular-whitespace */
|
/* eslint-disable no-irregular-whitespace */
|
||||||
import { ru } from "date-fns/locale";
|
|
||||||
import Button from "../Button";
|
import Button from "../Button";
|
||||||
import CloseIcon from "../icons/CloseIcon";
|
import CloseIcon from "../icons/CloseIcon";
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
|
||||||
import Label from "../Label";
|
import Label from "../Label";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Select from "../Select";
|
import Select from "../Select";
|
||||||
import useModalStore from "../../stores/useModalStore";
|
import useModalStore from "../../stores/useModalStore";
|
||||||
import Input from "../Input";
|
import Input from "../Input";
|
||||||
import {
|
import {
|
||||||
addMonths,
|
|
||||||
addWeeks,
|
|
||||||
areIntervalsOverlapping,
|
|
||||||
differenceInDays,
|
|
||||||
eachMinuteOfInterval,
|
eachMinuteOfInterval,
|
||||||
endOfDay,
|
isAfter,
|
||||||
format,
|
isBefore,
|
||||||
parse,
|
parse,
|
||||||
parseISO,
|
parseISO,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
subDays,
|
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import api from "../../utils/api";
|
import api from "../../utils/api";
|
||||||
|
import ChoiceChips from "../ChoiceChips";
|
||||||
|
import ISchedule from "../../types/ISchedule";
|
||||||
|
import DatePicker from "../DatePicker";
|
||||||
|
import IScheduledSession from "../../types/IScheduledSession";
|
||||||
|
|
||||||
interface CreateScheduleProps {
|
interface Props {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
buildId: string;
|
buildId: string;
|
||||||
schedules?: any[];
|
schedules: ISchedule[];
|
||||||
handleCreate: () => void;
|
handleCreate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateSchedule({
|
function CreateScheduleModal({
|
||||||
companyId,
|
companyId,
|
||||||
buildId,
|
buildId,
|
||||||
schedules,
|
schedules,
|
||||||
handleCreate,
|
handleCreate,
|
||||||
}: CreateScheduleProps) {
|
}: Props) {
|
||||||
const setModal = useModalStore((state) => state.setModal);
|
const setModal = useModalStore((state) => state.setModal);
|
||||||
const [date, setDate] = useState<Date | null>(new Date());
|
const [date, setDate] = useState<Date>(new Date());
|
||||||
const [scheduleDuration, setScheduleDuration] = useState<number>(3);
|
|
||||||
const [startDate, setStartDate] = useState<Date>();
|
const [startDate, setStartDate] = useState<Date>();
|
||||||
const [endDate, setEndDate] = useState<Date>();
|
|
||||||
const [sessionDuration, setSessionDuration] = useState<number>(30);
|
const [sessionDuration, setSessionDuration] = useState<number>(30);
|
||||||
const [sessionBreak, setSessionBreak] = useState<number>(5);
|
const [sessionBreak, setSessionBreak] = useState<number>(5);
|
||||||
const [startTime, setStartTime] = useState<string>("10:00");
|
const [startTime, setStartTime] = useState<string>("10:00");
|
||||||
const [endTime, setEndTime] = useState<string>("20:00");
|
const [endTime, setEndTime] = useState<string>("20:00");
|
||||||
const [sessionCount, setSessionCount] = useState<number>();
|
const [weekends, setWeekends] = useState(["Сб", "Вс"]);
|
||||||
|
const [sessionsPerDay, setSessionsPerDay] = useState<number>();
|
||||||
|
|
||||||
|
async function getLastScheduledSessionDate() {
|
||||||
|
try {
|
||||||
|
const { startAt }: IScheduledSession = await api
|
||||||
|
.get(`companies/${companyId}/builds/${buildId}/last_scheduled_session`)
|
||||||
|
.json();
|
||||||
|
|
||||||
|
if (!startAt) return;
|
||||||
|
|
||||||
|
setStartDate(startOfDay(parseISO(startAt)));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!date || !scheduleDuration) return;
|
getLastScheduledSessionDate();
|
||||||
|
}, []);
|
||||||
|
|
||||||
setStartDate(startOfDay(date));
|
function calculateSessionsPerDay() {
|
||||||
setEndDate(
|
if (!startDate) return;
|
||||||
endOfDay(
|
|
||||||
subDays(
|
|
||||||
scheduleDuration !== 3
|
|
||||||
? addWeeks(date, scheduleDuration)
|
|
||||||
: addMonths(date, 1),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [date, scheduleDuration]);
|
|
||||||
|
|
||||||
function calculateSessionCount() {
|
|
||||||
if (!startDate || !endDate) return;
|
|
||||||
|
|
||||||
const sessionsPerDay = eachMinuteOfInterval(
|
const sessionsPerDay = eachMinuteOfInterval(
|
||||||
{
|
{
|
||||||
@@ -77,20 +76,10 @@ function CreateSchedule({
|
|||||||
{ step: sessionDuration + sessionBreak }
|
{ step: sessionDuration + sessionBreak }
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const days = differenceInDays(endDate, startDate);
|
setSessionsPerDay(sessionsPerDay);
|
||||||
|
|
||||||
setSessionCount(days * sessionsPerDay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangedate(value: Date) {
|
function changeEndTime(value: string) {
|
||||||
if (differenceInDays(value, new Date()) >= 0) {
|
|
||||||
setDate(value);
|
|
||||||
} else {
|
|
||||||
setDate(new Date());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeendTime(value: string) {
|
|
||||||
if (value.split(":")[0] > startTime.split(":")[0]) {
|
if (value.split(":")[0] > startTime.split(":")[0]) {
|
||||||
setEndTime(value);
|
setEndTime(value);
|
||||||
}
|
}
|
||||||
@@ -100,35 +89,18 @@ function CreateSchedule({
|
|||||||
await api.post(`companies/${companyId}/builds/${buildId}/schedules`, {
|
await api.post(`companies/${companyId}/builds/${buildId}/schedules`, {
|
||||||
json: {
|
json: {
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
|
weekends,
|
||||||
sessionDuration,
|
sessionDuration,
|
||||||
sessionBreak,
|
sessionBreak,
|
||||||
sessionCount,
|
sessionsPerDay,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClickCreateSchedule() {
|
async function handleClickCreateSchedule() {
|
||||||
if (!startDate || !endDate) return;
|
if (!startDate) return;
|
||||||
|
|
||||||
if (
|
|
||||||
schedules?.some((schedule) =>
|
|
||||||
areIntervalsOverlapping(
|
|
||||||
{ start: startDate, end: endDate },
|
|
||||||
{
|
|
||||||
start: parseISO(schedule.startDate),
|
|
||||||
end: parseISO(schedule.endDate),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
alert(
|
|
||||||
"Данные даты пересекаются с другим расписанием! Выберите другие даты."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createSchedule();
|
await createSchedule();
|
||||||
handleCreate();
|
handleCreate();
|
||||||
@@ -136,8 +108,8 @@ function CreateSchedule({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateSessionCount();
|
calculateSessionsPerDay();
|
||||||
}, [startDate, endDate, endTime, sessionDuration, sessionBreak]);
|
}, [startDate, endTime, sessionDuration, sessionBreak, weekends]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white text-sm relative z-10 rounded-lg w-[896px] shadow-md">
|
<div className="bg-white text-sm relative z-10 rounded-lg w-[896px] shadow-md">
|
||||||
@@ -155,68 +127,73 @@ function CreateSchedule({
|
|||||||
<div className="border-b border-[#DAE0E5] flex">
|
<div className="border-b border-[#DAE0E5] flex">
|
||||||
<div className="w-full px-6 border-r border-[#DAE0E5]">
|
<div className="w-full px-6 border-r border-[#DAE0E5]">
|
||||||
<div className="py-8 border-b border-[#DAE0E5] flex justify-between">
|
<div className="py-8 border-b border-[#DAE0E5] flex justify-between">
|
||||||
<div className="">
|
<div className="space-y-2">
|
||||||
<p className="font-semibold">Срок действия расписания</p>
|
<p className="font-semibold">Начало действия расписания</p>
|
||||||
<div className=""></div>
|
<div className="w-[240px]">
|
||||||
|
<p className="text-xs text-[#77828C]">
|
||||||
|
Максимальный доступный клиенту промежуток для записи — две
|
||||||
|
календарные недели
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[296px] flex flex-col gap-3">
|
<div className="w-[296px] flex flex-col gap-3">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Label value="Начало" />
|
<Label value="Начало" />
|
||||||
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
placeholderText="Выберите дату"
|
startDate={startDate || new Date()}
|
||||||
selected={date}
|
onChange={(date) => setDate(date)}
|
||||||
onChange={handleChangedate}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Label value="Продолжительность" />
|
|
||||||
<Select
|
|
||||||
defaultValue={scheduleDuration}
|
|
||||||
options={[
|
|
||||||
{ value: 1, text: "1 неделя" },
|
|
||||||
{ value: 2, text: "2 недели" },
|
|
||||||
{ value: 3, text: "1 месяц" },
|
|
||||||
]}
|
|
||||||
handleChange={(value) => setScheduleDuration(value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-8 border-b border-[#DAE0E5] flex justify-between">
|
<div className="py-8 border-b border-[#DAE0E5] space-y-4">
|
||||||
<div className="">
|
<div className="flex justify-between">
|
||||||
<p className="font-semibold">Рабочее время</p>
|
<div className="">
|
||||||
<div className=""></div>
|
<p className="font-semibold">Рабочее время</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-[296px]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Label value="Начало" />
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
defaultValue={startTime}
|
||||||
|
handleChange={(value) => setStartTime(value)}
|
||||||
|
className="w-[137px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-[#77828C]">-</p>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Label value="Конец" />
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
defaultValue={endTime}
|
||||||
|
handleChange={changeEndTime}
|
||||||
|
className="w-[137px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
<div className="w-[296px]">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<p className="font-semibold">Выходные дни</p>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="w-[240px]">
|
||||||
<Label value="Начало" />
|
<p className="text-xs font-[#77828C]">
|
||||||
<Input
|
В выбранные дни запись на демонстрацию для клиента будет
|
||||||
type="time"
|
недоступна
|
||||||
defaultValue={startTime}
|
</p>
|
||||||
handleChange={(value) => setStartTime(value)}
|
|
||||||
className="w-[137px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4 text-[#77828C]">-</p>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Label value="Конец" />
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
defaultValue={endTime}
|
|
||||||
handleChange={changeendTime}
|
|
||||||
className="w-[137px]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-[296px]">
|
||||||
|
<ChoiceChips
|
||||||
|
options={["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]}
|
||||||
|
selected={["Сб", "Вс"]}
|
||||||
|
onChange={(options) => setWeekends(options)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -290,19 +267,10 @@ function CreateSchedule({
|
|||||||
<div className="p-4 flex flex-col gap-4">
|
<div className="p-4 flex flex-col gap-4">
|
||||||
<p className="text-sm font-semibold">Предварительный просмотр</p>
|
<p className="text-sm font-semibold">Предварительный просмотр</p>
|
||||||
<div className="flex flex-col gap-3 text-xs">
|
<div className="flex flex-col gap-3 text-xs">
|
||||||
{startDate && endDate && (
|
|
||||||
<p className="font-semibold flex gap-1">
|
|
||||||
<span>{format(startDate, "dd.MM.yyyy")}</span>
|
|
||||||
<span>-</span>
|
|
||||||
<span>{format(endDate, "dd.MM.yyyy")}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-3">
|
||||||
<p className="col-span-2 text-[#77828C]">
|
<p className="col-span-2 text-[#77828C]">Cеансов в день</p>
|
||||||
Общее кол-во сеансов
|
<p>{sessionsPerDay}</p>
|
||||||
</p>
|
|
||||||
<p>{sessionCount}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-3">
|
||||||
<p className="col-span-2 text-[#77828C]">
|
<p className="col-span-2 text-[#77828C]">
|
||||||
@@ -320,6 +288,14 @@ function CreateSchedule({
|
|||||||
{startTime} - {endTime}
|
{startTime} - {endTime}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div className="grid grid-cols-3">
|
||||||
|
<p className="col-span-2 text-[#77828C]">Выходные дни</p>
|
||||||
|
<p className="flex gap-1 flex-wrap">
|
||||||
|
{weekends.map((value) => (
|
||||||
|
<span>{value}</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,4 +312,4 @@ function CreateSchedule({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateSchedule;
|
export default CreateScheduleModal;
|
||||||
@@ -42,17 +42,3 @@ body {
|
|||||||
.exited {
|
.exited {
|
||||||
opacity: 0;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
parse,
|
parse,
|
||||||
addDays,
|
addDays,
|
||||||
subDays,
|
subDays,
|
||||||
parseISO,
|
|
||||||
isWithinInterval,
|
isWithinInterval,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import Button from "../components/Button";
|
import Button from "../components/Button";
|
||||||
@@ -24,8 +23,9 @@ import { Transition } from "react-transition-group";
|
|||||||
import SpinnerIcon from "../components/icons/SpinnerIcon";
|
import SpinnerIcon from "../components/icons/SpinnerIcon";
|
||||||
import useModalStore from "../stores/useModalStore";
|
import useModalStore from "../stores/useModalStore";
|
||||||
import ModalContainer from "../components/ModalContainer";
|
import ModalContainer from "../components/ModalContainer";
|
||||||
import CreateSchedule from "../components/modals/CreateSchedule";
|
import CreateScheduleModal from "../components/modals/CreateScheduleModal";
|
||||||
import MoreIcon from "../components/icons/MoreIcon";
|
import MoreIcon from "../components/icons/MoreIcon";
|
||||||
|
import ISchedule from "../types/ISchedule";
|
||||||
|
|
||||||
function DashboardPage() {
|
function DashboardPage() {
|
||||||
const [user, setAccessToken] = useAuthStore((state) => [
|
const [user, setAccessToken] = useAuthStore((state) => [
|
||||||
@@ -36,7 +36,7 @@ function DashboardPage() {
|
|||||||
const [selectedBuildManagers, setSelectedBuildManagers] = useState<any[]>();
|
const [selectedBuildManagers, setSelectedBuildManagers] = useState<any[]>();
|
||||||
const [builds, setBuilds] = useState<any[]>();
|
const [builds, setBuilds] = useState<any[]>();
|
||||||
const [selectedBuild, setSelectedBuild] = useState<{ [key: string]: any }>();
|
const [selectedBuild, setSelectedBuild] = useState<{ [key: string]: any }>();
|
||||||
const [schedules, setSchedules] = useState<any[]>();
|
const [schedules, setSchedules] = useState<ISchedule[]>();
|
||||||
const [scheduledSessions, setScheduledSessions] = useState<any[]>();
|
const [scheduledSessions, setScheduledSessions] = useState<any[]>();
|
||||||
const [generatedScheduledSessions, setGeneratedScheduledSessions] =
|
const [generatedScheduledSessions, setGeneratedScheduledSessions] =
|
||||||
useState<any[][]>();
|
useState<any[][]>();
|
||||||
@@ -67,16 +67,20 @@ function DashboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("schedules", schedules);
|
||||||
|
|
||||||
if (!selectedDate || !selectedBuild || !schedules?.length) return;
|
if (!selectedDate || !selectedBuild || !schedules?.length) return;
|
||||||
|
|
||||||
const foundSchedule = schedules.find(
|
const foundSchedule = schedules.find(
|
||||||
(schedule) =>
|
(schedule) =>
|
||||||
isWithinInterval(selectedDate, {
|
isWithinInterval(selectedDate, {
|
||||||
start: parseISO(schedule.startDate),
|
start: new Date(schedule.startDate),
|
||||||
end: parseISO(schedule.endDate),
|
end: addDays(new Date(schedule.startDate), 14),
|
||||||
}) && selectedBuild.id === schedule.buildId
|
}) && selectedBuild.id === schedule.buildId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("foundSchedule", foundSchedule);
|
||||||
|
|
||||||
if (foundSchedule) {
|
if (foundSchedule) {
|
||||||
const startDateTime = parse(
|
const startDateTime = parse(
|
||||||
foundSchedule.startTime,
|
foundSchedule.startTime,
|
||||||
@@ -477,20 +481,19 @@ function DashboardPage() {
|
|||||||
<div key={schedule.id} className="flex flex-col gap-3 text-xs">
|
<div key={schedule.id} className="flex flex-col gap-3 text-xs">
|
||||||
<p className="font-semibold flex gap-1">
|
<p className="font-semibold flex gap-1">
|
||||||
<span>
|
<span>
|
||||||
{format(parseISO(schedule.startDate), "dd.MM.yyyy")}
|
Действует с{" "}
|
||||||
|
{format(new Date(schedule.startDate), "dd.MM.yyyy")}
|
||||||
</span>
|
</span>
|
||||||
<span>-</span>
|
{/* <span>-</span> */}
|
||||||
<span>
|
<span>
|
||||||
{format(parseISO(schedule.endDate), "dd.MM.yyyy")}
|
{/* {format(parseISO(schedule.endDate), "dd.MM.yyyy")} */}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-3">
|
||||||
<p className="col-span-2 text-[#77828C]">
|
<p className="col-span-2 text-[#77828C]">Сеансов в день</p>
|
||||||
Общее кол-во сеансов
|
<p>{schedule.sessionsPerDay}</p>
|
||||||
</p>
|
|
||||||
<p>{schedule.sessionCount}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-3">
|
||||||
<p className="col-span-2 text-[#77828C]">
|
<p className="col-span-2 text-[#77828C]">
|
||||||
@@ -517,7 +520,7 @@ function DashboardPage() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
handleClick={() =>
|
handleClick={() =>
|
||||||
setModal(
|
setModal(
|
||||||
<CreateSchedule
|
<CreateScheduleModal
|
||||||
companyId={company?.id}
|
companyId={company?.id}
|
||||||
buildId={selectedBuild?.id}
|
buildId={selectedBuild?.id}
|
||||||
schedules={schedules}
|
schedules={schedules}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
interface ISchedule {
|
||||||
|
id: string;
|
||||||
|
companyId: string;
|
||||||
|
buildId: string;
|
||||||
|
startDate: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
weekends: string[];
|
||||||
|
sessionDuration: number;
|
||||||
|
sessionBreak: number;
|
||||||
|
sessionsPerDay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ISchedule;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
interface IScheduledSession {
|
||||||
|
id: string;
|
||||||
|
companyId: string;
|
||||||
|
buildId: string;
|
||||||
|
startAt: string;
|
||||||
|
endAt: string;
|
||||||
|
client?: unknown;
|
||||||
|
userId?: string;
|
||||||
|
activeSessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IScheduledSession;
|
||||||
+68
-1
@@ -321,6 +321,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz"
|
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz"
|
||||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
||||||
|
|
||||||
|
"@types/lodash@^4.17.4":
|
||||||
|
version "4.17.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7"
|
||||||
|
integrity sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==
|
||||||
|
|
||||||
"@types/node@^20.8.7":
|
"@types/node@^20.8.7":
|
||||||
version "20.8.7"
|
version "20.8.7"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz"
|
||||||
@@ -468,6 +473,11 @@
|
|||||||
"@typescript-eslint/types" "6.6.0"
|
"@typescript-eslint/types" "6.6.0"
|
||||||
eslint-visitor-keys "^3.4.1"
|
eslint-visitor-keys "^3.4.1"
|
||||||
|
|
||||||
|
"@uidotdev/usehooks@^2.4.1":
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf"
|
||||||
|
integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==
|
||||||
|
|
||||||
"@vitejs/plugin-react-swc@^3.3.2":
|
"@vitejs/plugin-react-swc@^3.3.2":
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz"
|
resolved "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz"
|
||||||
@@ -475,6 +485,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@swc/core" "^1.3.61"
|
"@swc/core" "^1.3.61"
|
||||||
|
|
||||||
|
"@wojtekmaj/date-utils@^1.1.3":
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz#c3cd67177ac781cfa5736219d702a55a2aea5f2b"
|
||||||
|
integrity sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==
|
||||||
|
|
||||||
acorn-jsx@^5.3.2:
|
acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||||
@@ -625,6 +640,11 @@ classnames@^2.2.6:
|
|||||||
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
|
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
|
||||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||||
|
|
||||||
|
clsx@^2.0.0:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||||
|
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||||
|
|
||||||
color-convert@^2.0.1:
|
color-convert@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
|
||||||
@@ -948,6 +968,13 @@ function-bind@^1.1.1:
|
|||||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
|
||||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||||
|
|
||||||
|
get-user-locale@^2.2.1:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-2.3.2.tgz#d37ae6e670c2b57d23a96fb4d91e04b2059d52cf"
|
||||||
|
integrity sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==
|
||||||
|
dependencies:
|
||||||
|
mem "^8.0.0"
|
||||||
|
|
||||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||||
@@ -1175,6 +1202,11 @@ lodash.merge@^4.6.2:
|
|||||||
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||||
|
|
||||||
|
lodash@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
|
||||||
@@ -1189,6 +1221,21 @@ lru-cache@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
map-age-cleaner@^0.1.3:
|
||||||
|
version "0.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
|
||||||
|
integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
|
||||||
|
dependencies:
|
||||||
|
p-defer "^1.0.0"
|
||||||
|
|
||||||
|
mem@^8.0.0:
|
||||||
|
version "8.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122"
|
||||||
|
integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==
|
||||||
|
dependencies:
|
||||||
|
map-age-cleaner "^0.1.3"
|
||||||
|
mimic-fn "^3.1.0"
|
||||||
|
|
||||||
merge2@^1.3.0, merge2@^1.4.1:
|
merge2@^1.3.0, merge2@^1.4.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
||||||
@@ -1202,6 +1249,11 @@ micromatch@^4.0.4, micromatch@^4.0.5:
|
|||||||
braces "^3.0.2"
|
braces "^3.0.2"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
|
mimic-fn@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74"
|
||||||
|
integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==
|
||||||
|
|
||||||
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||||
@@ -1277,6 +1329,11 @@ optionator@^0.9.3:
|
|||||||
prelude-ls "^1.2.1"
|
prelude-ls "^1.2.1"
|
||||||
type-check "^0.4.0"
|
type-check "^0.4.0"
|
||||||
|
|
||||||
|
p-defer@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
|
||||||
|
integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==
|
||||||
|
|
||||||
p-limit@^3.0.2:
|
p-limit@^3.0.2:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
|
||||||
@@ -1428,6 +1485,16 @@ react-async-script@^1.2.0:
|
|||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
prop-types "^15.5.0"
|
prop-types "^15.5.0"
|
||||||
|
|
||||||
|
react-calendar@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-5.0.0.tgz#cf302db689ee1bba53d92644f7543e12164a7c0e"
|
||||||
|
integrity sha512-bHcE5e5f+VUKLd4R19BGkcSQLpuwjKBVG0fKz74cwPW5xDfNsReHdDbfd4z3mdjuUuZzVtw4Q920mkwK5/ZOEg==
|
||||||
|
dependencies:
|
||||||
|
"@wojtekmaj/date-utils" "^1.1.3"
|
||||||
|
clsx "^2.0.0"
|
||||||
|
get-user-locale "^2.2.1"
|
||||||
|
warning "^4.0.0"
|
||||||
|
|
||||||
react-datepicker@^4.20.0:
|
react-datepicker@^4.20.0:
|
||||||
version "4.20.0"
|
version "4.20.0"
|
||||||
resolved "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.20.0.tgz"
|
resolved "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.20.0.tgz"
|
||||||
@@ -1765,7 +1832,7 @@ vite@^4.4.5:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
warning@^4.0.2:
|
warning@^4.0.0, warning@^4.0.2:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz"
|
||||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: "crm.stream.graff.tech-server",
|
name: "crm.stream.graff.tech-server:3001",
|
||||||
exec_mode: "cluster",
|
exec_mode: "cluster",
|
||||||
script: "yarn",
|
script: "yarn",
|
||||||
args: "start",
|
args: "start",
|
||||||
|
|||||||
+4
-4
@@ -19,17 +19,17 @@ const port = process.env.PORT || 3000;
|
|||||||
connectDB();
|
connectDB();
|
||||||
|
|
||||||
app.use(json());
|
app.use(json());
|
||||||
app.use(cors());
|
app.use(cors({ origin: "*" }));
|
||||||
|
|
||||||
|
app.use("/app", authMiddleware, appRouter);
|
||||||
|
app.use("/companies", authMiddleware, companiesRouter);
|
||||||
|
app.use("/users", authMiddleware, usersRouter);
|
||||||
app.use("/login", loginRouter);
|
app.use("/login", loginRouter);
|
||||||
app.use("/registration", registrationRouter);
|
app.use("/registration", registrationRouter);
|
||||||
app.use("/actions", actionsRouter);
|
app.use("/actions", actionsRouter);
|
||||||
app.use("/builds", buildsRouter);
|
app.use("/builds", buildsRouter);
|
||||||
app.use("/scheduled_sessions", scheduledSessionsRouter);
|
app.use("/scheduled_sessions", scheduledSessionsRouter);
|
||||||
app.use("/schedules", schedulesRouter);
|
app.use("/schedules", schedulesRouter);
|
||||||
app.use("/app", authMiddleware, appRouter);
|
|
||||||
app.use("/companies", authMiddleware, companiesRouter);
|
|
||||||
app.use("/users", authMiddleware, usersRouter);
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server listening on port ${port}`);
|
console.log(`Server listening on port ${port}`);
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ const scheduleSchema = new Schema(
|
|||||||
type: Date,
|
type: Date,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
endDate: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
startTime: {
|
startTime: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -28,6 +24,9 @@ const scheduleSchema = new Schema(
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
weekends: {
|
||||||
|
type: [String],
|
||||||
|
},
|
||||||
sessionDuration: {
|
sessionDuration: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -36,7 +35,7 @@ const scheduleSchema = new Schema(
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
sessionCount: {
|
sessionsPerDay: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|||||||
+100
-78
@@ -6,15 +6,15 @@ import Schedule from "../models/Schedule";
|
|||||||
import BuildUser from "../models/BuildUser";
|
import BuildUser from "../models/BuildUser";
|
||||||
import User from "../models/User";
|
import User from "../models/User";
|
||||||
|
|
||||||
const companiesRouter = Router();
|
const router = Router();
|
||||||
|
|
||||||
// companiesRouter.get("/", async (_req, res) => {
|
// router.get("/", async (_req, res) => {
|
||||||
// const companies = await Company.find();
|
// const companies = await Company.find();
|
||||||
|
|
||||||
// res.json(companies);
|
// res.json(companies);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
companiesRouter.get("/:id", async (req, res) => {
|
router.get("/:id", async (req, res) => {
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
res.json({ error: "Access denied" });
|
res.json({ error: "Access denied" });
|
||||||
return;
|
return;
|
||||||
@@ -25,7 +25,7 @@ companiesRouter.get("/:id", async (req, res) => {
|
|||||||
res.json(company);
|
res.json(company);
|
||||||
});
|
});
|
||||||
|
|
||||||
companiesRouter.get("/:id/builds", async (req, res) => {
|
router.get("/:id/builds", async (req, res) => {
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
res.json({ error: "Access denied" });
|
res.json({ error: "Access denied" });
|
||||||
return;
|
return;
|
||||||
@@ -37,7 +37,7 @@ companiesRouter.get("/:id/builds", async (req, res) => {
|
|||||||
res.json(builds);
|
res.json(builds);
|
||||||
});
|
});
|
||||||
|
|
||||||
companiesRouter.get("/:id/users", async (req, res) => {
|
router.get("/:id/users", async (req, res) => {
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
res.json({ error: "Access denied" });
|
res.json({ error: "Access denied" });
|
||||||
return;
|
return;
|
||||||
@@ -49,7 +49,7 @@ companiesRouter.get("/:id/users", async (req, res) => {
|
|||||||
res.json(users);
|
res.json(users);
|
||||||
});
|
});
|
||||||
|
|
||||||
companiesRouter.get("/:id/builds/:buildId/users", async (req, res) => {
|
router.get("/:id/builds/:buildId/users", async (req, res) => {
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
res.json({ error: "Access denied" });
|
res.json({ error: "Access denied" });
|
||||||
return;
|
return;
|
||||||
@@ -69,44 +69,41 @@ companiesRouter.get("/:id/builds/:buildId/users", async (req, res) => {
|
|||||||
res.json(users);
|
res.json(users);
|
||||||
});
|
});
|
||||||
|
|
||||||
companiesRouter.get(
|
router.get("/:id/builds/:buildId/scheduled_sessions", async (req, res) => {
|
||||||
"/:id/builds/:buildId/scheduled_sessions",
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
async (req, res) => {
|
res.json({ error: "Access denied" });
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
return;
|
||||||
res.json({ error: "Access denied" });
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.query.date) {
|
if (!req.query.date) {
|
||||||
res.json({ error: "Query parameter `date` is required" });
|
res.json({ error: "Query parameter `date` is required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = parseISO(req.query.date as string);
|
const date = parseISO(req.query.date as string);
|
||||||
|
|
||||||
const company: any = await Company.findById(req.params.id).populate({
|
const company: any = await Company.findById(req.params.id).populate({
|
||||||
path: "builds",
|
path: "builds",
|
||||||
|
match: {
|
||||||
|
_id: req.params.buildId,
|
||||||
|
},
|
||||||
|
populate: {
|
||||||
|
path: "scheduledSessions",
|
||||||
match: {
|
match: {
|
||||||
_id: req.params.buildId,
|
startAt: {
|
||||||
},
|
$gte: startOfDay(date),
|
||||||
populate: {
|
$lte: endOfDay(date),
|
||||||
path: "scheduledSessions",
|
|
||||||
match: {
|
|
||||||
startAt: {
|
|
||||||
$gte: startOfDay(date),
|
|
||||||
$lte: endOfDay(date),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { scheduledSessions } = company.builds[0];
|
const { scheduledSessions } = company.builds[0];
|
||||||
|
|
||||||
res.json(scheduledSessions);
|
res.json(scheduledSessions);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// companiesRouter.post(
|
// router.post(
|
||||||
// "/:id/builds/:buildId/scheduled_sessions",
|
// "/:id/builds/:buildId/scheduled_sessions",
|
||||||
// async (req, res) => {
|
// async (req, res) => {
|
||||||
// if (req.params.id != res.locals.user.companyId) {
|
// if (req.params.id != res.locals.user.companyId) {
|
||||||
@@ -129,48 +126,45 @@ companiesRouter.get(
|
|||||||
// }
|
// }
|
||||||
// );
|
// );
|
||||||
|
|
||||||
companiesRouter.put(
|
router.put("/:id/scheduled_sessions/:scheduledSessionId", async (req, res) => {
|
||||||
"/:id/scheduled_sessions/:scheduledSessionId",
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
async (req, res) => {
|
res.json({ error: "Access denied" });
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
return;
|
||||||
res.json({ error: "Access denied" });
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scheduledSession = await ScheduledSession.findById(
|
||||||
|
req.params.scheduledSessionId
|
||||||
|
);
|
||||||
|
|
||||||
|
const scheduledSessionAtSameTime = await ScheduledSession.findOne({
|
||||||
|
startAt: scheduledSession?.startAt,
|
||||||
|
userId: req.body.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scheduledSessionAtSameTime) {
|
||||||
|
res.json({ error: "Scheduled session at same time" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const updatedScheduledSession = await ScheduledSession.findByIdAndUpdate(
|
||||||
const scheduledSession = await ScheduledSession.findById(
|
req.params.scheduledSessionId,
|
||||||
req.params.scheduledSessionId
|
req.body,
|
||||||
);
|
{
|
||||||
|
new: true,
|
||||||
const scheduledSessionAtSameTime = await ScheduledSession.findOne({
|
upsert: true,
|
||||||
startAt: scheduledSession?.startAt,
|
|
||||||
userId: req.body.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (scheduledSessionAtSameTime) {
|
|
||||||
res.json({ error: "Scheduled session at same time" });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const updatedScheduledSession = await ScheduledSession.findByIdAndUpdate(
|
res.json(updatedScheduledSession);
|
||||||
req.params.scheduledSessionId,
|
} catch (error) {
|
||||||
req.body,
|
if (error instanceof Error) {
|
||||||
{
|
res.json({ error });
|
||||||
new: true,
|
|
||||||
upsert: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(updatedScheduledSession);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
res.json({ error });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
companiesRouter.get(
|
router.get(
|
||||||
"/:id/builds/:buildId/scheduled_sessions/:scheduledSessionId/availableManagers",
|
"/:id/builds/:buildId/scheduled_sessions/:scheduledSessionId/availableManagers",
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
if (!req.query.startAt) {
|
if (!req.query.startAt) {
|
||||||
@@ -225,7 +219,7 @@ companiesRouter.get(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
companiesRouter.get("/:id/builds/:buildId/schedules", async (req, res) => {
|
router.get("/:id/builds/:buildId/schedules", async (req, res) => {
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
res.json({ error: "Access denied" });
|
res.json({ error: "Access denied" });
|
||||||
return;
|
return;
|
||||||
@@ -239,19 +233,47 @@ companiesRouter.get("/:id/builds/:buildId/schedules", async (req, res) => {
|
|||||||
res.json(schedules);
|
res.json(schedules);
|
||||||
});
|
});
|
||||||
|
|
||||||
companiesRouter.post("/:id/builds/:buildId/schedules", async (req, res) => {
|
router.post("/:id/builds/:buildId/schedules", async (req, res) => {
|
||||||
if (req.params.id != res.locals.user.companyId) {
|
if (req.params.id != res.locals.user.companyId) {
|
||||||
res.json({ error: "Access denied" });
|
res.json({ error: "Access denied" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schedule = await Schedule.create({
|
try {
|
||||||
companyId: req.params.id,
|
const schedule = await Schedule.create({
|
||||||
buildId: req.params.buildId,
|
companyId: req.params.id,
|
||||||
...req.body,
|
buildId: req.params.buildId,
|
||||||
});
|
...req.body,
|
||||||
|
});
|
||||||
res.json(schedule);
|
res.json(schedule);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
res.json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:companyId/builds/:buildId/last_scheduled_session",
|
||||||
|
async (req, res) => {
|
||||||
|
const { companyId, buildId } = req.params;
|
||||||
|
|
||||||
|
console.log("companyId", companyId);
|
||||||
|
console.log("buildId", buildId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lastScheduledSession = await ScheduledSession.findOne({
|
||||||
|
companyId,
|
||||||
|
buildId,
|
||||||
|
}).sort({ startAt: -1 });
|
||||||
|
|
||||||
|
res.json(lastScheduledSession);
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const companiesRouter = router;
|
||||||
|
|
||||||
export default companiesRouter;
|
export default companiesRouter;
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import Schedule from "../models/Schedule";
|
|||||||
import {
|
import {
|
||||||
addMinutes,
|
addMinutes,
|
||||||
areIntervalsOverlapping,
|
areIntervalsOverlapping,
|
||||||
|
differenceInMinutes,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
isValid,
|
isValid,
|
||||||
parseISO,
|
parseISO,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
|
import { isValidObjectId } from "mongoose";
|
||||||
|
|
||||||
const scheduledSessionsRouter = Router();
|
const scheduledSessionsRouter = Router();
|
||||||
|
|
||||||
@@ -23,6 +25,15 @@ scheduledSessionsRouter.get("/", async (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
scheduledSessionsRouter.get("/:id", async (req, res) => {
|
scheduledSessionsRouter.get("/:id", async (req, res) => {
|
||||||
|
const scheduledSessionId = req.params.id;
|
||||||
|
|
||||||
|
if (!isValidObjectId(scheduledSessionId)) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid session ID value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const scheduledSession = await ScheduledSession.findById(req.params.id);
|
const scheduledSession = await ScheduledSession.findById(req.params.id);
|
||||||
|
|
||||||
res.json(scheduledSession);
|
res.json(scheduledSession);
|
||||||
@@ -41,6 +52,14 @@ scheduledSessionsRouter.get("/builds/:buildId", async (req, res) => {
|
|||||||
|
|
||||||
const buildId = req.params.buildId;
|
const buildId = req.params.buildId;
|
||||||
const date = req.query.date as string;
|
const date = req.query.date as string;
|
||||||
|
|
||||||
|
if (!isValidObjectId(buildId)) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid build ID value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const scheduledSessions = await ScheduledSession.find({
|
const scheduledSessions = await ScheduledSession.find({
|
||||||
buildId,
|
buildId,
|
||||||
startAt: {
|
startAt: {
|
||||||
@@ -77,13 +96,19 @@ scheduledSessionsRouter.get("/:buildId", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
scheduledSessionsRouter.post("/", async (req, res) => {
|
scheduledSessionsRouter.post("/", async (req, res) => {
|
||||||
const { buildId, startAt, client } = req.body;
|
const { buildId, startAt, client, duration } = req.body;
|
||||||
|
|
||||||
|
if (!isValidObjectId(buildId)) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid build ID value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!buildId || !startAt) {
|
if (!buildId || !startAt) {
|
||||||
return res.json({
|
return res.json({
|
||||||
status: "error",
|
status: "error",
|
||||||
message:
|
message: "Parameters `buildId`, `startAt` are required!", // Параметры `compamyId`, `buildId`, `startAt`, `client` обязательны!
|
||||||
"Parameters `buildId`, `startAt` are required!", // Параметры `compamyId`, `buildId`, `startAt`, `client` обязательны!
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +121,65 @@ scheduledSessionsRouter.post("/", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const build = await Build.findById(buildId);
|
||||||
|
|
||||||
|
if (!build) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "An assembly with such a `buildId` was not found", // Сборка с таким `buildId` не найдена
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration) {
|
||||||
|
const scheduledSessions = await ScheduledSession.find({
|
||||||
|
buildId,
|
||||||
|
startAt: {
|
||||||
|
$gte: startOfDay(startAtISO),
|
||||||
|
$lte: endOfDay(startAtISO),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const endAtISO = addMinutes(startAtISO, duration);
|
||||||
|
|
||||||
|
if (scheduledSessions.length) {
|
||||||
|
const overlappingSessions = [];
|
||||||
|
|
||||||
|
for (const session of scheduledSessions) {
|
||||||
|
if (
|
||||||
|
areIntervalsOverlapping(
|
||||||
|
{
|
||||||
|
start: session.startAt,
|
||||||
|
end: addMinutes(session.endAt, duration),
|
||||||
|
},
|
||||||
|
{ start: startAtISO, end: endAtISO }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
overlappingSessions.push(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlappingSessions.length >= build.sessionLimit) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message:
|
||||||
|
"It is not possible to create a session because it overlaps with the time of another session", // Невозможно создать сеанс, поскольку он перекрывается со временем другого сеанса.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledSession = await ScheduledSession.create({
|
||||||
|
buildId,
|
||||||
|
startAt: startAtISO,
|
||||||
|
endAt: endAtISO,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
status: "success",
|
||||||
|
scheduledSessionId: scheduledSession.id,
|
||||||
|
url: `https://stream.graff.tech/scheduled/${scheduledSession.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const schedule = await Schedule.findOne({
|
const schedule = await Schedule.findOne({
|
||||||
buildId,
|
buildId,
|
||||||
startDate: { $lte: startAtISO },
|
startDate: { $lte: startAtISO },
|
||||||
@@ -109,15 +193,6 @@ scheduledSessionsRouter.post("/", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = await Build.findById(buildId);
|
|
||||||
|
|
||||||
if (!build) {
|
|
||||||
return res.json({
|
|
||||||
status: "error",
|
|
||||||
message: "An assembly with such a `buildId` was not found", // Сборка с таким `buildId` не найдена
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduledSessions = await ScheduledSession.find({
|
const scheduledSessions = await ScheduledSession.find({
|
||||||
buildId,
|
buildId,
|
||||||
startAt: {
|
startAt: {
|
||||||
@@ -163,18 +238,103 @@ scheduledSessionsRouter.post("/", async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status: "success",
|
status: "success",
|
||||||
|
scheduledSessionId: scheduledSession.id,
|
||||||
url: `https://stream.graff.tech/scheduled/${scheduledSession.id}`,
|
url: `https://stream.graff.tech/scheduled/${scheduledSession.id}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
scheduledSessionsRouter.put("/:id", async (req, res) => {
|
scheduledSessionsRouter.put("/:id", async (req, res) => {
|
||||||
const scheduledSession = await ScheduledSession.findByIdAndUpdate(
|
const scheduledSessionId = req.params.id;
|
||||||
req.params.id,
|
|
||||||
req.body,
|
if (!isValidObjectId(scheduledSessionId)) {
|
||||||
{ new: true, upsert: true }
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid session ID value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let { startAt, duration }: { startAt: string; duration: number } = req.body;
|
||||||
|
|
||||||
|
if (!startAt && !duration) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Parameter `startAt` or `duration` is required, or both",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startAt && !isValid(parseISO(startAt))) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid value of the `startAt` parameter",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInteger(num: number) {
|
||||||
|
return (num ^ 0) === num;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration && !isInteger(duration)) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Parameter `duration` is not an integer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledSession = await ScheduledSession.findById(scheduledSessionId);
|
||||||
|
|
||||||
|
if (!scheduledSession) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Session with this ID not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!duration) {
|
||||||
|
duration = differenceInMinutes(
|
||||||
|
scheduledSession.endAt,
|
||||||
|
scheduledSession.startAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endAt = addMinutes(
|
||||||
|
(startAt && parseISO(startAt.toString())) || scheduledSession.startAt,
|
||||||
|
duration
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(scheduledSession);
|
try {
|
||||||
|
const scheduledSession = await ScheduledSession.findByIdAndUpdate(
|
||||||
|
scheduledSessionId,
|
||||||
|
{ startAt, endAt },
|
||||||
|
{ new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ status: "success", scheduledSession });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
res.json({ status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduledSessionsRouter.delete("/:id", async (req, res) => {
|
||||||
|
const scheduledSessionId = req.params.id;
|
||||||
|
|
||||||
|
if (!isValidObjectId(scheduledSessionId)) {
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Invalid session ID value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ScheduledSession.findByIdAndDelete(scheduledSessionId);
|
||||||
|
|
||||||
|
res.json({ status: "success" });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
res.json({ status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default scheduledSessionsRouter;
|
export default scheduledSessionsRouter;
|
||||||
|
|||||||
Reference in New Issue
Block a user