first commit
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useState } from "react";
|
||||
import Card from "../components/Card";
|
||||
import EmptyCard from "../components/EmptyCard";
|
||||
import TabButton from "../components/TabButton";
|
||||
import BurgerIcon from "../components/icons/BurgerIcon";
|
||||
import ChevronLeftIcon from "../components/icons/ChevronLeftIcon";
|
||||
import ChevronRightIcon from "../components/icons/ChevronRightIcon";
|
||||
import api from "../utils/api";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import {
|
||||
eachMinuteOfInterval,
|
||||
format,
|
||||
parse,
|
||||
addDays,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import Button from "../components/Button";
|
||||
import { ru } from "date-fns/locale";
|
||||
import { Transition } from "react-transition-group";
|
||||
import SpinnerIcon from "../components/icons/SpinnerIcon";
|
||||
|
||||
function DashboardPage() {
|
||||
const [user, setAccessToken] = useAuthStore((state) => [
|
||||
state.user,
|
||||
state.setAccessToken,
|
||||
]);
|
||||
const [company, setCompany] = useState<{ [key: string]: any }>();
|
||||
const [managers, setManagers] = useState<any[]>();
|
||||
const [builds, setBuilds] = useState<any[]>();
|
||||
const [selectedBuild, setSelectedBuild] = useState<{ [key: string]: any }>();
|
||||
const [scheduledSessions, setScheduledSessions] = useState<any[]>();
|
||||
const [generatedScheduledSessions, setGeneratedScheduledSessions] =
|
||||
useState<any[][]>();
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const startDateTime = parse("10:00", "HH:mm", selectedDate);
|
||||
const endDateTime = parse("20:00", "HH:mm", selectedDate);
|
||||
const dateTimes: Date[] = eachMinuteOfInterval(
|
||||
{
|
||||
start: startDateTime,
|
||||
end: endDateTime,
|
||||
},
|
||||
{ step: 60 }
|
||||
);
|
||||
const [currentTime, setCurrentTime] = useState<string>(
|
||||
format(new Date(), "HH:mm")
|
||||
);
|
||||
const [isLoadingScheduledSessions, setIsLoadingScheduledSessions] =
|
||||
useState(true);
|
||||
|
||||
// const [selectedDate, setSelectedDate] = useState(
|
||||
// format(new Date(), "d MMMM, yyyy", { locale: ru })
|
||||
// );
|
||||
|
||||
function selectNextDay() {
|
||||
setSelectedDate((prev) => addDays(prev, 1));
|
||||
}
|
||||
|
||||
function selectPrevDay() {
|
||||
setSelectedDate((prev) => subDays(prev, 1));
|
||||
}
|
||||
|
||||
function selectCurrentDate() {
|
||||
setSelectedDate(new Date());
|
||||
}
|
||||
|
||||
async function getCompany() {
|
||||
if (!user) {
|
||||
console.log("No User", user);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: any = await api.get(`companies/${user.companyId}`).json();
|
||||
console.log("getCompany result: ", result);
|
||||
setCompany(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuilds() {
|
||||
if (!user) {
|
||||
console.log("No User", user);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: any = await api
|
||||
.get(`companies/${user.companyId}/builds`)
|
||||
.json();
|
||||
console.log("getBuilds result: ", result);
|
||||
setBuilds(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getManagers() {
|
||||
if (!company) {
|
||||
console.log("No Managers", managers);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: any = await api.get(`companies/${company.id}/users`).json();
|
||||
console.log("getManagers result: ", result);
|
||||
setManagers(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getScheduledSessions() {
|
||||
if (!company) {
|
||||
console.log("No ScheduledSessions");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("selectedBuild", selectedBuild);
|
||||
|
||||
if (!selectedBuild || !selectedBuild.id) {
|
||||
console.log("No selectedBuild");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingScheduledSessions(true);
|
||||
|
||||
try {
|
||||
const result: any = await api
|
||||
.get(
|
||||
`companies/${company.id}/builds/${
|
||||
selectedBuild.id
|
||||
}/scheduled_sessions?date=${selectedDate.toISOString()}`
|
||||
)
|
||||
.json();
|
||||
console.log("getScheduledSessions result: ", result);
|
||||
|
||||
setScheduledSessions(result);
|
||||
setIsLoadingScheduledSessions(false);
|
||||
} catch (error) {
|
||||
setIsLoadingScheduledSessions(false);
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
setAccessToken(null);
|
||||
}
|
||||
|
||||
function generateScheduledSessions() {
|
||||
if (!scheduledSessions || !selectedBuild) return;
|
||||
|
||||
const arr: any[][] = [];
|
||||
|
||||
dateTimes.forEach((dateTime) => {
|
||||
const arr2 = [];
|
||||
|
||||
const findedSessionsCount = scheduledSessions.filter(
|
||||
(session) => session.startAt === dateTime.toISOString()
|
||||
);
|
||||
|
||||
arr2.push(dateTime);
|
||||
|
||||
for (let i = 0; i < selectedBuild.sessionLimit; i++) {
|
||||
if (findedSessionsCount[i]) {
|
||||
arr2.push(findedSessionsCount[i]);
|
||||
} else {
|
||||
arr2.push({});
|
||||
}
|
||||
}
|
||||
|
||||
arr.push(arr2);
|
||||
});
|
||||
|
||||
setGeneratedScheduledSessions(arr);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getCompany();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(format(new Date(), "HH:mm"));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!company) return;
|
||||
getBuilds();
|
||||
}, [company]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!builds) return;
|
||||
setSelectedBuild(builds[0]);
|
||||
getManagers();
|
||||
}, [builds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!managers || !selectedDate || !selectedBuild) return;
|
||||
getScheduledSessions();
|
||||
|
||||
console.log(selectedBuild);
|
||||
}, [managers, selectedDate, selectedBuild]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scheduledSessions) return;
|
||||
generateScheduledSessions();
|
||||
|
||||
console.log(scheduledSessions);
|
||||
}, [scheduledSessions]);
|
||||
|
||||
return (
|
||||
<div className="main h-screen flex">
|
||||
<div className="left flex flex-col w-full">
|
||||
<div className="flex bg-[#F0F1F2] ">
|
||||
<button
|
||||
onClick={() => alert("Чё тыкаешь? В разработке еще!!")}
|
||||
className="p-3 transition-colors hover:bg-neutral-200"
|
||||
>
|
||||
<BurgerIcon />
|
||||
</button>
|
||||
{builds?.map((build) => (
|
||||
<TabButton
|
||||
key={build.id}
|
||||
handleClick={() => setSelectedBuild(build)}
|
||||
active={selectedBuild && build.id === selectedBuild.id}
|
||||
>
|
||||
{build.name}
|
||||
</TabButton>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center px-4 py-2 h-12 border-r border-b border-[#DAE0E5]">
|
||||
<p className="text-sm font-semibold">Расписание</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-sm font-semibold">
|
||||
{format(selectedDate, "PPPP", { locale: ru })}
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
handleClick={selectPrevDay}
|
||||
icon={<ChevronLeftIcon />}
|
||||
color="secondary"
|
||||
/>
|
||||
<Button
|
||||
handleClick={selectNextDay}
|
||||
icon={<ChevronRightIcon />}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button handleClick={selectCurrentDate}>Сегодня</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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]">
|
||||
{currentTime}
|
||||
</div>
|
||||
|
||||
{selectedBuild &&
|
||||
[...Array(selectedBuild.sessionLimit)].map((_, index) => (
|
||||
<div key={index}>
|
||||
<div className="w-[264px] h-[40px] px-3 flex items-center text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5]">
|
||||
Слот {index + 1}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`overflow-y-auto overflow-x-hidden flex-1 bg-[#F2F2F2] border-r border-[#DAE0E5]`}
|
||||
>
|
||||
<Transition
|
||||
in={isLoadingScheduledSessions}
|
||||
timeout={150}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
>
|
||||
{(state) => (
|
||||
<div
|
||||
className={`fixed z-10 top-0 left-0 w-full h-full bg-black bg-opacity-80 transition-opacity flex justify-center items-center text-white ${state}`}
|
||||
>
|
||||
<SpinnerIcon />
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
{!isLoadingScheduledSessions &&
|
||||
generatedScheduledSessions?.map(
|
||||
(generatedScheduledSession, index) => (
|
||||
<div key={index} className="flex">
|
||||
{generatedScheduledSession.map((scheduledSession, index2) => {
|
||||
if (index2 === 0) {
|
||||
return (
|
||||
<div
|
||||
key={index2}
|
||||
className="w-[84px] h-[128px] flex justify-center items-center text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5]"
|
||||
>
|
||||
{format(scheduledSession, "HH:mm")}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (Object.keys(scheduledSession).length) {
|
||||
return (
|
||||
<Card
|
||||
key={index2}
|
||||
client={{
|
||||
name: scheduledSession.clientName,
|
||||
phone: scheduledSession.clientPhone,
|
||||
email: scheduledSession.clientEmail,
|
||||
}}
|
||||
manager={managers?.find(
|
||||
(manager) =>
|
||||
manager.id === scheduledSession.userId
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <EmptyCard key={index2} />;
|
||||
}
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="right w-[320px] flex flex-col">
|
||||
<div className="min-h-[48px] bg-[#F0F1F2] flex justify-between items-center">
|
||||
<p className="text-xs font-semibold px-3 text-green-600">
|
||||
{company?.name}
|
||||
</p>
|
||||
<p className="text-xs font-semibold px-3">{user?.username}</p>
|
||||
<TabButton handleClick={logout} className="text-red-600">
|
||||
Выйти
|
||||
</TabButton>
|
||||
</div>
|
||||
<div className="flex gap-2 h-[48px] border-b border-[#DAE0E5]">
|
||||
<button className="p-4 text-sm font-semibold">Параметры</button>
|
||||
<button
|
||||
disabled
|
||||
className="p-4 text-sm font-semibold disabled:text-[#77828C]"
|
||||
>
|
||||
Статистика
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden">
|
||||
<p className="p-4">...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../components/Button";
|
||||
|
||||
function HomePage() {
|
||||
return (
|
||||
<div className="p-8 flex flex-col gap-4 w-fit">
|
||||
<p>HomePage</p>
|
||||
<Link to="/login">
|
||||
<Button type="button">Login</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useRef, useState } from "react";
|
||||
import ky from "ky";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import Form from "../components/Form";
|
||||
import Label from "../components/Label";
|
||||
import Input from "../components/Input";
|
||||
import Button from "../components/Button";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
function LoginPage() {
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const recaptchaRef = useRef(null);
|
||||
const [setAccessToken, setUser] = useAuthStore((state) => [
|
||||
state.setAccessToken,
|
||||
state.setUser,
|
||||
]);
|
||||
const [isLoading, setisLoading] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function login() {
|
||||
setisLoading(true);
|
||||
|
||||
const result: any = await ky
|
||||
.post(import.meta.env.VITE_SERVER_URL + "/login", {
|
||||
json: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
if (result.error) {
|
||||
alert(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setAccessToken(result.accessToken);
|
||||
setUser(result.user);
|
||||
navigate("/dashboard");
|
||||
|
||||
setTimeout(() => {
|
||||
setisLoading(false);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center p-8 flex-1 ">
|
||||
<div className=" bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div className="flex flex-col gap-6 p-12">
|
||||
<p className="text-2xl font-semibold">Вход</p>
|
||||
<Form handleSubmit={login}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label value="Email" />
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="example@gmail.com"
|
||||
autoFocus
|
||||
required
|
||||
handleChange={(value) => setUsername(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label value="Пароль" />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="********"
|
||||
required
|
||||
handleChange={(value) => setPassword(value)}
|
||||
/>
|
||||
</div>
|
||||
<ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey="6LdKPH4oAAAAAM8cyMoCkmNvbnBbe2UIrwRwQ425"
|
||||
className="mt-3"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="medium"
|
||||
className="mt-10"
|
||||
loading={isLoading}
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
</Form>
|
||||
<div className="self-center flex gap-6">
|
||||
<Link to="/registration" className="text-xs text-[#49A1F5]">
|
||||
Нет аккаунта?
|
||||
</Link>
|
||||
{/* <Link to="" className="text-xs text-[#49A1F5]">
|
||||
Забыли пароль?
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../components/Button";
|
||||
|
||||
function RegistrationCompanyPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center p-8 flex-1">
|
||||
<div className=" bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div className="flex flex-col gap-6 p-12 w-[528px]">
|
||||
<p className="text-2xl font-semibold">Вы представитель компании</p>
|
||||
|
||||
<p>
|
||||
Для получения данных для входа обратитесь в компанию GRAFF
|
||||
interactive любым удобным для вас способом
|
||||
</p>
|
||||
|
||||
<div className="flex gap-6">
|
||||
<a href="tel:88007700067" className="text-blue-500">
|
||||
8 800 770 00 67
|
||||
</a>
|
||||
<a href="mailto:support@graff.tech" className="text-blue-500">
|
||||
support@graff.tech
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Link to="/login" className="mt-4">
|
||||
<Button size="medium">На главную</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RegistrationCompanyPage;
|
||||
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../components/Button";
|
||||
|
||||
function RegistrationManagerPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center p-8 flex-1">
|
||||
<div className=" bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div className="flex flex-col gap-6 p-12 w-[528px]">
|
||||
<p className="text-2xl font-semibold">Получение данных для входа</p>
|
||||
|
||||
<p>
|
||||
Чтобы получить данные для входа обратитесь к представителю вашей
|
||||
организации, для подключения вас к системе.
|
||||
</p>
|
||||
|
||||
<Link to="/login" className="mt-4">
|
||||
<Button size="medium">На главную</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RegistrationManagerPage;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../components/Button";
|
||||
|
||||
function RegistrationPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center p-8 flex-1">
|
||||
<div className=" bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div className="flex flex-col gap-6 p-12 w-[528px]">
|
||||
<p className="text-2xl font-semibold">Получение данных для входа</p>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link to="company">
|
||||
<button className="h-[56px] px-6 py-4 rounded-lg border border-[#DAE0E5] text-[#77828C] text-left w-full">
|
||||
Я представитель компании
|
||||
</button>
|
||||
</Link>
|
||||
<Link to="manager">
|
||||
<button className="h-[56px] px-6 py-4 rounded-lg border border-[#DAE0E5] text-[#77828C] text-left w-full">
|
||||
Я менеджер отдела продаж
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Link to="/login">
|
||||
<Button size="medium" color="secondary">
|
||||
Назад
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RegistrationPage;
|
||||
Reference in New Issue
Block a user