upd
This commit is contained in:
@@ -51,21 +51,21 @@ function Calendar() {
|
||||
{isShowCalendar && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="text-[#77828C] p-1 hover:bg-[#E6ECF2] rounded-lg transition-colors"
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<ChevronLeftIcon />}
|
||||
onlyIcon
|
||||
onClick={selectPrevMonth}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</button>
|
||||
/>
|
||||
<p className="text-sm font-semibold">
|
||||
{_.capitalize(format(currentMonth, "LLLL, yyyy", { locale: ru }))}
|
||||
</p>
|
||||
<button
|
||||
className="text-[#77828C] p-1 hover:bg-[#E6ECF2] rounded-lg transition-colors"
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<ChevronRightIcon />}
|
||||
onlyIcon
|
||||
onClick={selectNextMonth}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"].map((value, index) => (
|
||||
|
||||
@@ -30,8 +30,7 @@ function Managers() {
|
||||
<div key={manager.id} className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full text-[10px] font-semibold flex items-center justify-center bg-[#E6ECF2]">
|
||||
{user?.name.split(" ")[0][0]}
|
||||
{user?.name.split(" ")[1][0]}
|
||||
{manager?.name.split(" ")[0][0]}
|
||||
</div>
|
||||
<p className="text-sm">{manager.name}</p>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,6 @@ function Menu() {
|
||||
<div className="rounded-full bg-[#E6ECF2] w-[88px] h-[88px] flex justify-center items-center">
|
||||
<p className="text-2xl font-semibold ml-0.5 mt-0.5">
|
||||
{user?.name.split(" ")[0][0]}
|
||||
{user?.name.split(" ")[1][0]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1 text-center">
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useState } from "react";
|
||||
import Timeline from "./Timeline";
|
||||
import { format, getDate, setDate, setHours, startOfDay } from "date-fns";
|
||||
import {
|
||||
format,
|
||||
getDate,
|
||||
getMonth,
|
||||
setDate,
|
||||
setHours,
|
||||
setMonth,
|
||||
startOfDay,
|
||||
} from "date-fns";
|
||||
// import useEventStore from "../stores/useEventStore";
|
||||
import Button from "./Button";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
@@ -40,7 +48,12 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
}
|
||||
|
||||
function handleChangeStartAt(startAt: Date) {
|
||||
setStartAt(setDate(startAt, getDate(selectedDay)));
|
||||
let newStartAt = setMonth(startAt, getMonth(selectedDay));
|
||||
newStartAt = setDate(newStartAt, getDate(selectedDay));
|
||||
|
||||
// console.log("newStartAt", newStartAt);
|
||||
|
||||
setStartAt(newStartAt);
|
||||
setDateForInstantStart(undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import { useClickAway } from "@uidotdev/usehooks";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
|
||||
interface Props {
|
||||
required?: boolean;
|
||||
defaultOption?: string;
|
||||
options: string[];
|
||||
onSelect: (option: string) => void;
|
||||
}
|
||||
|
||||
function Select3({ defaultOption, options, onSelect }: Props) {
|
||||
function Select3({ required, defaultOption, options, onSelect }: Props) {
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||
const [selectedOption, setSelectedOption] = useState<string>();
|
||||
|
||||
@@ -37,9 +38,10 @@ function Select3({ defaultOption, options, onSelect }: Props) {
|
||||
onClick={() => setShowOptions((prev) => !prev)}
|
||||
>
|
||||
<input
|
||||
required={required}
|
||||
type="text"
|
||||
readOnly
|
||||
value={defaultOption || selectedOption}
|
||||
value={selectedOption || defaultOption}
|
||||
className="text-sm px-3 py-2.5 h-10 ring-1 ring-inset ring-[#DAE0E5] rounded-lg w-full outline-none pr-8 cursor-pointer"
|
||||
/>
|
||||
<div className="absolute pr-2 cursor-pointer">
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FormEvent, useState } from "react";
|
||||
import useModalStore from "../../stores/useModalStore";
|
||||
import Button from "../Button";
|
||||
import CloseIcon from "../icons/CloseIcon";
|
||||
import ModalTabButton from "../ModalTabButton";
|
||||
import Label from "../Label";
|
||||
import Input from "../Input";
|
||||
import Select3 from "../Select3";
|
||||
import IError from "../../types/IError";
|
||||
import api from "../../utils/api";
|
||||
import toast from "react-hot-toast";
|
||||
import useAuthStore from "../../stores/useAuthStore";
|
||||
|
||||
function AddManagerModal() {
|
||||
const { setModal } = useModalStore();
|
||||
const { user } = useAuthStore();
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
const [role, setRole] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmitAddManager(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
await addManager();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function addManager() {
|
||||
try {
|
||||
const result: any | IError = await api
|
||||
.post("addManager", {
|
||||
json: {
|
||||
companyId: user?.companyId,
|
||||
username,
|
||||
name,
|
||||
role:
|
||||
(role === "Менеджер" && "manager") ||
|
||||
(role === "Администратор" && "admin"),
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
if ("error" in result) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setModal(null);
|
||||
toast.success("Пользователь успешно добавлен");
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg w-[400px]">
|
||||
<div className="border-b border-[#DAE0E5] flex items-center justify-between pl-6 pr-2">
|
||||
<ModalTabButton active>Добавить сотрудника</ModalTabButton>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<CloseIcon />}
|
||||
onlyIcon
|
||||
onClick={() => setModal(null)}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={handleSubmitAddManager}>
|
||||
<div className="border-b border-[#DAE0E5] p-6 space-y-4">
|
||||
<p className="text-sm text-[#77828C]">
|
||||
Информация для входа будет отправлена на почтовый адрес
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<Label value="Email*" />
|
||||
<Input
|
||||
type="email"
|
||||
className="w-full"
|
||||
autoFocus
|
||||
required
|
||||
value={username}
|
||||
handleChange={(value) => setUsername(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Имя*" />
|
||||
<Input
|
||||
className="w-full"
|
||||
required
|
||||
value={name}
|
||||
handleChange={(value) => setName(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Роль*" />
|
||||
<Select3
|
||||
required
|
||||
defaultOption="Менеджер"
|
||||
options={["Менеджер", "Администратор"]}
|
||||
onSelect={(option) => setRole(option)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-3">
|
||||
<Button disabled={loading} type="submit">
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddManagerModal;
|
||||
@@ -7,6 +7,7 @@ import useAuthStore from "../../stores/useAuthStore";
|
||||
import MoreIcon from "../icons/MoreIcon";
|
||||
import useStore from "../../stores/useStore";
|
||||
import ModalTabButton from "../ModalTabButton";
|
||||
import AddManagerModal from "./AddManagerModal";
|
||||
|
||||
function CompanyModal() {
|
||||
const [selectedTab, setSelectedTab] = useState<"company" | "employees">(
|
||||
@@ -99,8 +100,7 @@ function CompanyModal() {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[#E6ECF2] rounded-full flex items-center justify-center">
|
||||
<p className="text-[10px] font-semibold">
|
||||
{user?.name.split(" ")[0][0]}
|
||||
{user?.name.split(" ")[1][0]}
|
||||
{manager?.name.split(" ")[0][0]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="">
|
||||
@@ -129,11 +129,13 @@ function CompanyModal() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<Button disabled variant="secondary">
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
{user?.role === "admin" && (
|
||||
<div className="px-6 py-4">
|
||||
<Button onClick={() => setModal(<AddManagerModal />)}>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,6 @@ function SettingsModal() {
|
||||
<div className="min-w-[88px] min-h-[88px] flex items-center justify-center bg-[#E6ECF2] rounded-full">
|
||||
<p className="text-2xl font-semibold">
|
||||
{user?.name.split(" ")[0][0]}
|
||||
{user?.name.split(" ")[1][0]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
|
||||
@@ -10,6 +10,8 @@ import RegistrationCompanyPage from "./pages/RegistrationCompanyPage.tsx";
|
||||
import RegistrationManagerPage from "./pages/RegistrationManagerPage.tsx";
|
||||
import AdminPage from "./pages/AdminPage.tsx";
|
||||
import AdminCompanyPage from "./pages/AdminCompanyPage.tsx";
|
||||
import ResetPasswordPage from "./pages/ResetPasswordPage.tsx";
|
||||
import ResetPasswordConfirmPage from "./pages/ResetPasswordConfirmPage.tsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -23,6 +25,14 @@ const router = createBrowserRouter([
|
||||
path: "login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "reset",
|
||||
element: <ResetPasswordPage />,
|
||||
},
|
||||
{
|
||||
path: "resetConfirm",
|
||||
element: <ResetPasswordConfirmPage />,
|
||||
},
|
||||
{
|
||||
path: "registration",
|
||||
children: [
|
||||
|
||||
@@ -18,6 +18,7 @@ import IError from "../types/IError";
|
||||
import Schedule from "../components/Schedule";
|
||||
import IScheduledSession from "../types/IScheduledSession";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import Calendar from "../components/Calendar";
|
||||
|
||||
function DashboardPage() {
|
||||
const { user } = useAuthStore();
|
||||
@@ -228,7 +229,7 @@ function DashboardPage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-hidden overflow-y-auto">
|
||||
{/* <Calendar /> */}
|
||||
<Calendar />
|
||||
{/* <Schedules /> */}
|
||||
<Managers />
|
||||
</div>
|
||||
|
||||
@@ -70,11 +70,6 @@ function LoginPage() {
|
||||
handleChange={(value) => setPassword(value)}
|
||||
/>
|
||||
</div>
|
||||
{/* <ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey="6LdKPH4oAAAAAM8cyMoCkmNvbnBbe2UIrwRwQ425"
|
||||
className="mt-3"
|
||||
/> */}
|
||||
<Button
|
||||
type="submit"
|
||||
size="medium"
|
||||
@@ -89,6 +84,9 @@ function LoginPage() {
|
||||
<Link to="/registration" className="text-xs text-[#49A1F5]">
|
||||
Нет аккаунта?
|
||||
</Link>
|
||||
<Link to="/reset" className="text-xs text-[#49A1F5]">
|
||||
Забыли пароль?
|
||||
</Link>
|
||||
{/* <Link to="" className="text-xs text-[#49A1F5]">
|
||||
Забыли пароль?
|
||||
</Link> */}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useState } from "react";
|
||||
import Form from "../components/Form";
|
||||
import Label from "../components/Label";
|
||||
import Input from "../components/Input";
|
||||
import Button from "../components/Button";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import IError from "../types/IError";
|
||||
import api from "../utils/api";
|
||||
|
||||
function ResetPasswordConfirmPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function resetPasswordConfirm() {
|
||||
try {
|
||||
const result: any | IError = await api
|
||||
.post("resetConfirm", {
|
||||
credentials: "include",
|
||||
json: { resetCode: searchParams.get("code"), password },
|
||||
})
|
||||
.json();
|
||||
|
||||
if ("error" in result) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Пароль успешно изменен");
|
||||
|
||||
setTimeout(() => {
|
||||
navigate("/login");
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitResetPassword() {
|
||||
setLoading(true);
|
||||
await resetPasswordConfirm();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("searchParams [code]:", searchParams.get("code"));
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-1 min-h-screen p-8">
|
||||
<div className="overflow-hidden bg-white rounded-lg shadow-md max-w-[528px]">
|
||||
<div className="flex flex-col gap-6 p-12">
|
||||
<p className="text-2xl font-semibold">Изменение пароля</p>
|
||||
<Form handleSubmit={handleSubmitResetPassword} className="space-y-12">
|
||||
<div className="space-y-6">
|
||||
<p className="text-[#77828C] text-sm">Введите новый пароль</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label value="Новый пароль" />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder=""
|
||||
autoFocus
|
||||
required
|
||||
value={password}
|
||||
handleChange={(value) => setPassword(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
widthFull
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
На главную
|
||||
</Button>
|
||||
<Button type="submit" size="medium" loading={loading} widthFull>
|
||||
Продолжить
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetPasswordConfirmPage;
|
||||
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState } from "react";
|
||||
import Form from "../components/Form";
|
||||
import Label from "../components/Label";
|
||||
import Input from "../components/Input";
|
||||
import Button from "../components/Button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import IError from "../types/IError";
|
||||
import api from "../utils/api";
|
||||
|
||||
function ResetPasswordPage() {
|
||||
const [step, setStep] = useState<number>(1);
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function resetPassword() {
|
||||
try {
|
||||
const result: any | IError = await api
|
||||
.post("reset", {
|
||||
credentials: "include",
|
||||
json: { username },
|
||||
})
|
||||
.json();
|
||||
|
||||
if ("error" in result) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep(2);
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitResetPassword() {
|
||||
setLoading(true);
|
||||
await resetPassword();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-1 min-h-screen p-8">
|
||||
<div className="overflow-hidden bg-white rounded-lg shadow-md max-w-[528px]">
|
||||
<div className="flex flex-col gap-6 p-12">
|
||||
<p className="text-2xl font-semibold">Сброс пароля</p>
|
||||
{step === 1 && (
|
||||
<Form
|
||||
handleSubmit={handleSubmitResetPassword}
|
||||
className="space-y-12"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<p className="text-[#77828C] text-sm">
|
||||
На указанный почтовый адрес будет отправлена информация для
|
||||
восстановления пароля
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label value="Email" />
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="mail@example.com"
|
||||
autoFocus
|
||||
required
|
||||
handleChange={(value) => setUsername(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
widthFull
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
<Button type="submit" size="medium" loading={loading} widthFull>
|
||||
Продолжить
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="">
|
||||
Мы отправили вам письмо на адрес <b>{username}</b> с
|
||||
инструкциями для восстановления пароля.
|
||||
</p>
|
||||
<p className="">
|
||||
Если вы не получите его в ближайшее время, проверьте папку
|
||||
«Спам».
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[#77828C] text-sm">
|
||||
Если же письма нигде не будет, пожалуйста, свяжитесь с нами по
|
||||
адресу support@graff.tech
|
||||
</p>
|
||||
</div>
|
||||
<Button size="medium" onClick={() => navigate("/login")}>
|
||||
На главную
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetPasswordPage;
|
||||
@@ -34,6 +34,8 @@ const api = ky.extend({
|
||||
...prev,
|
||||
user: { ...prev.user!, accessToken: result.accessToken },
|
||||
}));
|
||||
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user