upd
This commit is contained in:
@@ -1,53 +1,85 @@
|
||||
import SpinnerIcon from "./icons/SpinnerIcon";
|
||||
import RotateIcon from "./icons/RotateIcon";
|
||||
|
||||
interface ButtonProps {
|
||||
type?: "button" | "reset" | "submit";
|
||||
color?: "primary" | "secondary" | "tertiary";
|
||||
size?: "small" | "medium";
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
interface Props {
|
||||
type?: "button" | "submit" | "reset";
|
||||
variant?: "primary" | "secondary" | "tertiary";
|
||||
size?: "large" | "medium" | "small" | "xsmall";
|
||||
roundedFull?: boolean;
|
||||
widthFull?: boolean;
|
||||
icon?: JSX.Element;
|
||||
onlyIcon?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
handleClick?: () => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"bg-[#49A1F5] text-white hover:bg-[#4190DB] disabled:bg-[#F2F2F2] disabled:text-[#CCCCCC]",
|
||||
secondary:
|
||||
"bg-[#F0F1F2] text-[#77828C] hover:bg-[#E6ECF2] hover:text-[#4C5359] disabled:bg-[#F2F2F2] disabled:text-[#CCCCCC]",
|
||||
tertiary:
|
||||
"bg-transparent text-[#77828C] hover:bg-[#E6ECF2] hover:text-[#4C5359] disabled:bg-transparent disabled:text-[#CCCCCC]",
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
large: "",
|
||||
medium: "px-6 py-3 text-sm font-semibold leading-[16px] h-10",
|
||||
small: "px-4 py-2 text-xs font-semibold h-8",
|
||||
xsmall: "",
|
||||
};
|
||||
|
||||
const onlyIconSizeClasses = {
|
||||
large: "",
|
||||
medium: "",
|
||||
small: "p-1.5",
|
||||
xsmall: "",
|
||||
};
|
||||
|
||||
const iconClasses = {
|
||||
large: "",
|
||||
medium: "w-6 h-6",
|
||||
small: "w-5 h-5",
|
||||
xsmall: "",
|
||||
};
|
||||
|
||||
function Button({
|
||||
type = "button",
|
||||
color = "primary",
|
||||
variant = "primary",
|
||||
size = "small",
|
||||
disabled,
|
||||
loading = false,
|
||||
onlyIcon,
|
||||
roundedFull = false,
|
||||
widthFull = false,
|
||||
icon,
|
||||
onlyIcon = false,
|
||||
children,
|
||||
className,
|
||||
handleClick,
|
||||
}: ButtonProps) {
|
||||
className = "",
|
||||
disabled,
|
||||
loading,
|
||||
onClick,
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled || loading}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
className={`outline-none rounded-lg transition-all font-semibold flex justify-center items-center gap-1 disabled:bg-[#F2F2F2] disabled:text-[#CCCCCC] ${
|
||||
(color === "primary" && "bg-[#49A1F5] text-white hover:bg-[#4190DB]") ||
|
||||
(color === "secondary" &&
|
||||
"bg-[#F0F1F2] text-[#77828C] hover:bg-[#E6ECF2]") ||
|
||||
(color === "tertiary" &&
|
||||
"text-[#77828C] hover:bg-[#E6ECF2]")
|
||||
} ${
|
||||
(size === "small" &&
|
||||
`h-8 ${
|
||||
onlyIcon ? "p-1" : icon ? "pl-2 pr-4 py-1" : "px-3 py-2.5"
|
||||
} text-xs`) ||
|
||||
(size === "medium" &&
|
||||
`h-10 ${
|
||||
onlyIcon ? "p-2" : icon ? "pl-4 pr-6 py-2" : "px-6 py-3"
|
||||
} text-sm`)
|
||||
} ${className}`}
|
||||
disabled={disabled || loading}
|
||||
className={`flex items-center justify-center transition-all outline-none ${
|
||||
variantClasses[variant]
|
||||
} ${onlyIcon ? onlyIconSizeClasses[size] : sizeClasses[size]} ${
|
||||
roundedFull ? "rounded-full" : "rounded-lg"
|
||||
} ${widthFull ? "w-full" : "w-fit"} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{loading ? <SpinnerIcon /> : icon} {children}
|
||||
{loading ? (
|
||||
<span className={`${iconClasses[size]}`}>
|
||||
<RotateIcon className="animate-spin" />
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{icon && <span className={`${iconClasses[size]}`}>{icon}</span>}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ function Calendar() {
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-2 border-b border-[#DAE0E5]">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold">Календарь</p>
|
||||
<Button
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
icon={isShowCalendar ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
onlyIcon
|
||||
handleClick={() => setIsShowCalendar(!isShowCalendar)}
|
||||
onClick={() => setIsShowCalendar(!isShowCalendar)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ function Calendar() {
|
||||
{["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"].map((value, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-8 h-8 flex items-center justify-center text-xs font-semibold"
|
||||
className="flex items-center justify-center w-8 h-8 text-xs font-semibold"
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
|
||||
@@ -127,8 +127,8 @@ function Card({
|
||||
) : (
|
||||
manager.id === user.id && (
|
||||
<Button
|
||||
color="secondary"
|
||||
handleClick={() => handleSelect(scheduledSessionId, null)}
|
||||
variant="secondary"
|
||||
onClick={() => handleSelect(scheduledSessionId, null)}
|
||||
>
|
||||
Отменить
|
||||
</Button>
|
||||
@@ -136,8 +136,8 @@ function Card({
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
color="secondary"
|
||||
handleClick={() => handleSelect(scheduledSessionId, user.id)}
|
||||
variant="secondary"
|
||||
onClick={() => handleSelect(scheduledSessionId, user.id)}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
@@ -152,7 +152,7 @@ function Card({
|
||||
}/scheduled/${scheduledSessionId}?admin=true`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button color="secondary" icon={<EntryIcon />} onlyIcon />
|
||||
<Button variant="secondary" icon={<EntryIcon />} onlyIcon />
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useCopyToClipboard } from "@uidotdev/usehooks";
|
||||
import Button from "./Button";
|
||||
import CopyIcon from "./icons/CopyIcon";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value?: string | false;
|
||||
copyIcon?: boolean;
|
||||
}
|
||||
|
||||
function DataField({ label, value, copyIcon }: Props) {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
async function handleClickCopy() {
|
||||
if (!value) return;
|
||||
await copyToClipboard(value);
|
||||
toast.success("Скопировано в буфер обмена");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-[#77828C]">{label}</p>
|
||||
<p className="text-sm">{value}</p>
|
||||
</div>
|
||||
{copyIcon && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<CopyIcon className="w-5 h-5" />}
|
||||
onlyIcon
|
||||
className="text-[#77828C]"
|
||||
onClick={handleClickCopy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataField;
|
||||
@@ -18,10 +18,10 @@ function EmptyCard({ buildId, startAt, duration }: Props) {
|
||||
<div className="w-[264px] h-[164px] text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5] flex items-center justify-center group">
|
||||
{isAfter(startAt, new Date()) && (
|
||||
<Button
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
icon={<PlusIcon />}
|
||||
className="group-hover:opacity-100 opacity-0"
|
||||
handleClick={() =>
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<CreateScheduledSessionModal
|
||||
buildId={buildId}
|
||||
|
||||
@@ -25,7 +25,7 @@ function Header() {
|
||||
{user?.accessToken ? (
|
||||
<Button
|
||||
type="button"
|
||||
handleClick={logout}
|
||||
onClick={logout}
|
||||
className="text-black bg-transparent hover:bg-neutral-200"
|
||||
>
|
||||
Logout
|
||||
|
||||
@@ -40,7 +40,7 @@ function Input({
|
||||
value={value}
|
||||
onChange={(e) => handleChange && handleChange(e.target.value)}
|
||||
onFocus={handleFocus}
|
||||
className={`px-3 py-2.5 outline-none rounded-lg border border-[#DAE0E5] focus:border-[#49A1F5] transition-colors text-sm ${className}`}
|
||||
className={`px-3 py-2.5 outline-none rounded-lg ring-1 ring-[#DAE0E5] focus:ring-[#49A1F5] ring-inset transition-all text-sm ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ function Managers() {
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-col gap-4 border-b border-[#DAE0E5]">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold">Менеджеры</p>
|
||||
<Button
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
icon={isShowManagers ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
onlyIcon
|
||||
handleClick={() => setIsShowManagers(!isShowManagers)}
|
||||
onClick={() => setIsShowManagers(!isShowManagers)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,32 +29,25 @@ function Managers() {
|
||||
{managers.map((manager) => (
|
||||
<div key={manager.id} className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={manager.avatar || "/images/no-avatar.png"}
|
||||
alt=""
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<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]}
|
||||
</div>
|
||||
<p className="text-sm">{manager.name}</p>
|
||||
</div>
|
||||
{user?.role === "admin" && (
|
||||
<Button
|
||||
disabled
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
onlyIcon
|
||||
icon={<MoreIcon />}
|
||||
handleClick={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{user?.role === "admin" && (
|
||||
<Button
|
||||
disabled
|
||||
color="secondary"
|
||||
className="w-full"
|
||||
handleClick={() => {}}
|
||||
>
|
||||
<Button disabled variant="secondary" widthFull>
|
||||
Добавить
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,9 @@ import WorkIcon from "./icons/WorkIcon";
|
||||
import ExitIcon from "./icons/ExitIcon";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import useModalStore from "../stores/useModalStore";
|
||||
import SettingsModal from "./modals/SettingsModal";
|
||||
import CompanyModal from "./modals/CompanyModal";
|
||||
|
||||
function Menu() {
|
||||
const [isShow, setIsShow] = useState<boolean>(false);
|
||||
@@ -15,10 +18,22 @@ function Menu() {
|
||||
setIsShow(false);
|
||||
});
|
||||
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
function logout() {
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
function handleClickSettings() {
|
||||
setIsShow(false);
|
||||
setModal(<SettingsModal />);
|
||||
}
|
||||
|
||||
function handleClickCompany() {
|
||||
setIsShow(false);
|
||||
setModal(<CompanyModal />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="relative z-20 cursor-pointer">
|
||||
@@ -36,8 +51,8 @@ function Menu() {
|
||||
|
||||
<Transition in={isShow} timeout={150} mountOnEnter unmountOnExit>
|
||||
{(state) => (
|
||||
<div className={`transition-opacity ${state}`}>
|
||||
<div className="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-10"></div>
|
||||
<div className={`${state}`}>
|
||||
<div className="absolute top-0 left-0 z-20 w-full h-full bg-black bg-opacity-10"></div>
|
||||
<div ref={ref} className="absolute z-20 ml-2 mt-3.5">
|
||||
<div className="relative">
|
||||
<svg
|
||||
@@ -57,12 +72,13 @@ function Menu() {
|
||||
<div className="border-b border-[#DAE0E5] p-6 flex flex-col items-center justify-center gap-4">
|
||||
<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[0]}
|
||||
{user?.name.split(" ")[0][0]}
|
||||
{user?.name.split(" ")[1][0]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="text-sm">{user?.name}</p>
|
||||
<p className="text-[#77828C] text-xs">{user?.role}</p>
|
||||
<p className="text-[#77828C] text-xs">{user?.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-[#DAE0E5] py-3 space-y-2">
|
||||
@@ -76,8 +92,8 @@ function Menu() {
|
||||
Уведомления
|
||||
</button>
|
||||
<button
|
||||
disabled
|
||||
className="text-sm flex items-center gap-2 px-4 w-full hover:bg-[#E6ECF2] transition-colors disabled:hover:bg-inherit disabled:opacity-50"
|
||||
onClick={handleClickSettings}
|
||||
>
|
||||
<span className="text-[#77828C] py-1">
|
||||
<ParamsIcon />
|
||||
@@ -87,8 +103,8 @@ function Menu() {
|
||||
</div>
|
||||
<div className="border-b border-[#DAE0E5] py-2">
|
||||
<button
|
||||
disabled
|
||||
className="text-sm flex items-center gap-2 px-4 w-full hover:bg-[#E6ECF2] transition-colors disabled:hover:bg-inherit disabled:opacity-50"
|
||||
onClick={handleClickCompany}
|
||||
>
|
||||
<span className="text-[#77828C] py-1">
|
||||
<WorkIcon />
|
||||
|
||||
@@ -17,7 +17,7 @@ function ModalContainer() {
|
||||
{(state) => (
|
||||
<div
|
||||
onClick={() => setModal(null)}
|
||||
className={`min-h-screen p-8 absolute top-0 left-0 w-full flex justify-center items-center bg-black bg-opacity-30 overflow-auto cursor-pointer transition-opacity ${state}`}
|
||||
className={`min-h-screen p-8 absolute z-20 top-0 left-0 w-full flex justify-center items-center bg-black bg-opacity-30 overflow-auto cursor-pointer transition-opacity ${state}`}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} className="cursor-default">
|
||||
{modal}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
interface Props {
|
||||
active?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function ModalTabButton({ active, children, onClick }: Props) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`py-3.5 text-sm font-semibold transition-[color] ${active ? "" : "text-[#77828C]"}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalTabButton;
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Timeline from "./Timeline";
|
||||
import { format, getDate, setDate, setHours, startOfDay } from "date-fns";
|
||||
// import useEventStore from "../stores/useEventStore";
|
||||
@@ -10,8 +10,9 @@ import Input from "./Input";
|
||||
import Label from "./Label";
|
||||
import api from "../utils/api";
|
||||
import useStore from "../stores/useStore";
|
||||
import Select2 from "./Select2";
|
||||
import IScheduledSession from "../types/IScheduledSession";
|
||||
import toast from "react-hot-toast";
|
||||
import Select3 from "./Select3";
|
||||
|
||||
interface Props {
|
||||
selectedDay: Date;
|
||||
@@ -48,9 +49,9 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
}
|
||||
|
||||
async function handleClickSave() {
|
||||
console.log("slot", slot);
|
||||
console.log("startAt", startAt);
|
||||
console.log("duration", duration);
|
||||
// console.log("slot", slot);
|
||||
// console.log("startAt", startAt);
|
||||
// console.log("duration", duration);
|
||||
|
||||
if (!slot || !startAt || !duration) return;
|
||||
|
||||
@@ -97,7 +98,8 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
|
||||
window.open(`${result.url}?admin=true`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// console.log(error);
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
|
||||
// setIsLoading(false);
|
||||
@@ -108,14 +110,8 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
if (!builds) return;
|
||||
|
||||
setSelectedBuild(builds[0]);
|
||||
|
||||
console.log(builds);
|
||||
}, [builds]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("events", events);
|
||||
}, [events]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dateForInstantStart) return;
|
||||
|
||||
@@ -199,10 +195,10 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
: "Запланировать демонстрацию"}
|
||||
</p>
|
||||
<Button
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
icon={<CloseIcon />}
|
||||
onlyIcon
|
||||
handleClick={handleClickCancel}
|
||||
onClick={handleClickCancel}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 space-y-2">
|
||||
@@ -210,7 +206,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
<div className="">
|
||||
<div className="py-1 space-y-1 text-xs">
|
||||
<p className="text-[#77828C]">Выберите ЖК</p>
|
||||
<Select2
|
||||
{/* <Select2
|
||||
defaultValue={selectedBuild?.build}
|
||||
options={builds.map((build) => ({
|
||||
value: build.build,
|
||||
@@ -224,6 +220,18 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
)!
|
||||
)
|
||||
}
|
||||
/> */}
|
||||
<Select3
|
||||
defaultOption={
|
||||
selectedBuild?.name ||
|
||||
builds.find((build) => build.name)?.name
|
||||
}
|
||||
options={builds.map((build) => build.name)}
|
||||
onSelect={(option) =>
|
||||
setSelectedBuild(
|
||||
builds.find((build) => build.name === option)!
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid items-center grid-cols-2 gap-4 py-1 text-xs">
|
||||
@@ -275,13 +283,13 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 p-4">
|
||||
<Button type="submit" handleClick={handleClickSave}>
|
||||
<Button type="submit" onClick={handleClickSave}>
|
||||
{dateForInstantStart ? "Начать" : "Запланировать"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
handleClick={handleClickCancel}
|
||||
variant="secondary"
|
||||
onClick={handleClickCancel}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
@@ -292,7 +300,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
|
||||
<div className="fixed top-28 right-[336px] z-10">
|
||||
<Button
|
||||
disabled={draftMode}
|
||||
handleClick={() => setDateForInstantStart(new Date())}
|
||||
onClick={() => setDateForInstantStart(new Date())}
|
||||
>
|
||||
Начать демонстрацию с мгновенным запуском
|
||||
</Button>
|
||||
|
||||
@@ -16,13 +16,13 @@ function Schedules() {
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-col gap-4 border-b border-[#DAE0E5]">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold">Расписание</p>
|
||||
<Button
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
icon={isShowSchedules ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
onlyIcon
|
||||
handleClick={() => setIsShowSchedules(!isShowSchedules)}
|
||||
onClick={() => setIsShowSchedules(!isShowSchedules)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ function Schedules() {
|
||||
<div className="space-y-4">
|
||||
{schedules?.map((schedule) => (
|
||||
<div key={schedule.id} className="flex flex-col gap-3 text-xs">
|
||||
<p className="font-semibold flex gap-1">
|
||||
<p className="flex gap-1 font-semibold">
|
||||
<span>
|
||||
Действует с{" "}
|
||||
{format(new Date(schedule.startDate), "dd.MM.yyyy")}
|
||||
@@ -68,9 +68,9 @@ function Schedules() {
|
||||
|
||||
{user?.role === "admin" && (
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
handleClick={() => setModal(<CreateScheduleModal />)}
|
||||
onClick={() => setModal(<CreateScheduleModal />)}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useState } from "react";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
|
||||
interface Props {
|
||||
defaultOption?: string;
|
||||
options: string[];
|
||||
onSelect: (option: string) => void;
|
||||
}
|
||||
|
||||
function Select3({ defaultOption, options, onSelect }: Props) {
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||
const [selectedOption, setSelectedOption] = useState<string>();
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => {
|
||||
setShowOptions(false);
|
||||
});
|
||||
|
||||
function handleSelect(option: string) {
|
||||
setSelectedOption(option);
|
||||
onSelect(option);
|
||||
setShowOptions(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultOption) return;
|
||||
|
||||
handleSelect(defaultOption);
|
||||
}, [defaultOption]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<div
|
||||
className="relative flex items-center justify-end"
|
||||
onClick={() => setShowOptions((prev) => !prev)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={defaultOption || selectedOption}
|
||||
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">
|
||||
<div className="w-5 h-5">
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showOptions && (
|
||||
<div className="absolute w-full py-2 mt-2 bg-white rounded-lg shadow">
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="px-4 py-2 hover:bg-[#E6ECF2] transition-colors w-full flex items-center justify-between"
|
||||
onClick={() => handleSelect(option)}
|
||||
>
|
||||
<p className="text-sm">{option}</p>
|
||||
{option === selectedOption && (
|
||||
<div className="w-5 h-5 text-[#49A1F5]">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select3;
|
||||
@@ -2,21 +2,21 @@ interface TabButtonProps {
|
||||
children: React.ReactNode;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
handleClick?: () => void;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function TabButton({
|
||||
children,
|
||||
active = false,
|
||||
className,
|
||||
handleClick,
|
||||
onClick,
|
||||
}: TabButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`px-6 py-4 text-sm font-semibold leading-none transition-colors ${
|
||||
className={`px-6 py-3.5 text-sm font-semibold transition-colors ${
|
||||
active ? "bg-white" : "hover:bg-neutral-200"
|
||||
} ${className}`}
|
||||
onClick={handleClick}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -119,7 +119,7 @@ function Timeline({
|
||||
setStartAt(undefined);
|
||||
setDuration(0);
|
||||
ref.current.style.height = "0px";
|
||||
console.log(ref.current.clientHeight);
|
||||
// console.log(ref.current.clientHeight);
|
||||
}, [draftMode]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
function ChevronDownIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 11L12 16L17 11"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
const SVGComponent = () => (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13 7L8 12L13 17"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SVGComponent;
|
||||
function ChevronLeftIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13 7L8 12L13 17"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChevronLeftIcon;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
function ChevronRightIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M11 7L16 12L11 17"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
function ChevronUpIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 13L12 8L17 13"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
function CloseIcon() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CloseIcon({ className = "" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M12 11.9999L17.6569 6.34331M12 11.9999L6.34312 6.34302M12 11.9999L17.6568 17.6567M12 11.9999L6.34302 17.6568"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CopyIcon({ className = "" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M6 17.3333V6C6 4.89543 6.89543 4 8 4H15.4286M9.42857 7.55556H17C17.5523 7.55556 18 8.00327 18 8.55556V19C18 19.5523 17.5523 20 17 20H10.4286C9.87629 20 9.42857 19.5523 9.42857 19V7.55556Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyIcon;
|
||||
@@ -1,15 +1,20 @@
|
||||
function MoreIcon() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function MoreIcon({ className = "" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<circle cx="5" cy="12" r="1.5" fill="currentColor" />
|
||||
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
|
||||
<circle cx="19" cy="12" r="1.5" fill="currentColor" />
|
||||
<circle cx={5} cy={12} r={1.5} fill="currentColor" />
|
||||
<circle cx={12} cy={12} r={1.5} fill="currentColor" />
|
||||
<circle cx={19} cy={12} r={1.5} fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function RotateIcon({ className = "" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clipPath="url(#clip0_1287_60293)">
|
||||
<g clipPath="url(#clip1_1287_60293)">
|
||||
<path
|
||||
d="M20.3294 8.58537C19.7506 7.16982 18.8029 5.88296 17.5061 4.8802C13.5739 1.83969 7.92145 2.5625 4.88094 6.49465L4.26923 7.28573M3.67201 15.4146C4.25081 16.8301 5.19856 18.117 6.49538 19.1198C10.4275 22.1603 16.08 21.4375 19.1205 17.5053L19.7322 16.7142M4.26923 7.28573L3.91047 4.48015M4.26923 7.28573L7.07481 6.92697M19.7322 16.7142L16.9266 17.073M19.7322 16.7142L20.091 19.5198"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1287_60293">
|
||||
<rect width={24} height={24} fill="currentColor" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1287_60293">
|
||||
<rect
|
||||
width={24}
|
||||
height={24}
|
||||
fill="currentColor"
|
||||
transform="translate(9.84766 -4.8335) rotate(37.7128)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default RotateIcon;
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useState } from "react";
|
||||
import Button from "../Button";
|
||||
import CloseIcon from "../icons/CloseIcon";
|
||||
import useModalStore from "../../stores/useModalStore";
|
||||
import DataField from "../DataField";
|
||||
import useAuthStore from "../../stores/useAuthStore";
|
||||
import MoreIcon from "../icons/MoreIcon";
|
||||
import useStore from "../../stores/useStore";
|
||||
import ModalTabButton from "../ModalTabButton";
|
||||
|
||||
function CompanyModal() {
|
||||
const [selectedTab, setSelectedTab] = useState<"company" | "employees">(
|
||||
"company"
|
||||
);
|
||||
const { user } = useAuthStore();
|
||||
const { setModal } = useModalStore();
|
||||
const { company, managers } = useStore();
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg w-[624px] h-[408px] flex flex-col">
|
||||
<div className="flex items-center justify-between pl-6 border-b border-[#DAE0E5]">
|
||||
<div className="flex gap-6">
|
||||
<ModalTabButton
|
||||
active={selectedTab === "company"}
|
||||
onClick={() => setSelectedTab("company")}
|
||||
>
|
||||
Компания
|
||||
</ModalTabButton>
|
||||
<ModalTabButton
|
||||
active={selectedTab === "employees"}
|
||||
onClick={() => setSelectedTab("employees")}
|
||||
>
|
||||
Сотрудники
|
||||
</ModalTabButton>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<CloseIcon />}
|
||||
onlyIcon
|
||||
onClick={() => setModal(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{selectedTab === "company" && (
|
||||
<div>
|
||||
<div className="flex gap-6 p-6 border-b border-[#DAE0E5]">
|
||||
<div className="min-w-[88px] min-h-[88px] w-[88px] h-[88px] bg-[#E6ECF2] rounded-full"></div>
|
||||
<div className="w-full space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<DataField label="Сайт" value={company?.site} copyIcon />
|
||||
<DataField label="Телефон" value={company?.phone} copyIcon />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<DataField label="Email" value={company?.email} copyIcon />
|
||||
<DataField label="Адрес" value={company?.address} copyIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" className="text-[#49A1F5] text-xs">
|
||||
Сообщить о проблеме
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="p-6 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs font-semibold">Проекты</p>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[#E6ECF2] rounded-full"></div>
|
||||
<p className="text-xs font-semibold"></p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className=""></div>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
{selectedTab === "employees" && (
|
||||
<>
|
||||
<div className="grid grid-cols-8 pt-6 pb-2 mx-6 border-b">
|
||||
<div className="col-span-3">
|
||||
<p className="text-xs font-semibold">Имя</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs font-semibold">Должность</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs font-semibold">Телефон</p>
|
||||
</div>
|
||||
<div className=""></div>
|
||||
</div>
|
||||
<div className="flex-1 mx-6 py-4 space-y-3 overflow-y-scroll border-b border-[#DAE0E5]">
|
||||
{managers.map((manager, index) => (
|
||||
<div key={index} className="grid items-center grid-cols-8">
|
||||
<div className="col-span-3">
|
||||
<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]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="">
|
||||
<p className="text-sm">{manager.name}</p>
|
||||
<p className="text-xs text-[#77828C]">
|
||||
{manager.username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs">{manager.position}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs">{manager.phone}</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
{user?.role === "admin" && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<MoreIcon className="w-5 h-5" />}
|
||||
onlyIcon
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<Button disabled variant="secondary">
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CompanyModal;
|
||||
@@ -47,7 +47,7 @@ function CreateBuildModal({ companyId }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 space-y-4 rounded-xl">
|
||||
<div className="p-8 space-y-4 bg-white rounded-xl">
|
||||
<p className="text-xl font-semibold">Создание ЖК</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
@@ -70,8 +70,8 @@ function CreateBuildModal({ companyId }: Props) {
|
||||
handleChange={(value) => setSessionLimit(+value)}
|
||||
required
|
||||
/>
|
||||
<div className="self-end flex gap-2">
|
||||
<Button color="secondary" handleClick={() => setModal(null)}>
|
||||
<div className="flex self-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setModal(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit">Сохранить</Button>
|
||||
|
||||
@@ -38,7 +38,7 @@ function CreateCompanyModal() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 space-y-4 rounded-xl">
|
||||
<div className="p-8 space-y-4 bg-white rounded-xl">
|
||||
<p className="text-xl font-semibold">Создание компании</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
@@ -48,8 +48,8 @@ function CreateCompanyModal() {
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<div className="self-end flex gap-2">
|
||||
<Button color="secondary" handleClick={() => setModal(null)}>
|
||||
<div className="flex self-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setModal(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit">Сохранить</Button>
|
||||
|
||||
@@ -114,11 +114,10 @@ function CreateScheduleModal() {
|
||||
<div className="border-b border-[#DAE0E5] pl-2 pr-3 h-12 flex justify-between items-center">
|
||||
<p className="p-4 font-semibold leading-[115%]">Создание расписания</p>
|
||||
<Button
|
||||
color="secondary"
|
||||
onlyIcon
|
||||
variant="secondary"
|
||||
icon={<CloseIcon />}
|
||||
className="bg-transparent"
|
||||
handleClick={() => setModal(null)}
|
||||
onlyIcon
|
||||
onClick={() => setModal(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -265,7 +264,7 @@ function CreateScheduleModal() {
|
||||
{/* <div className="p-4 border-b border-[#DAE0E5] ">
|
||||
<p className="text-sm font-semibold">Статистика</p>
|
||||
</div> */}
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<p className="text-sm font-semibold">Предварительный просмотр</p>
|
||||
<div className="flex flex-col gap-3 text-xs">
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -291,7 +290,7 @@ function CreateScheduleModal() {
|
||||
</div>
|
||||
{/* <div className="grid grid-cols-3">
|
||||
<p className="col-span-2 text-[#77828C]">Выходные дни</p>
|
||||
<p className="flex gap-1 flex-wrap">
|
||||
<p className="flex flex-wrap gap-1">
|
||||
{weekends.map((value) => (
|
||||
<span>{value}</span>
|
||||
))}
|
||||
@@ -303,9 +302,9 @@ function CreateScheduleModal() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-3 flex gap-2">
|
||||
<Button handleClick={handleClickCreateSchedule}>Создать</Button>
|
||||
<Button color="secondary" handleClick={() => setModal(null)}>
|
||||
<div className="flex gap-2 px-6 py-3">
|
||||
<Button onClick={handleClickCreateSchedule}>Создать</Button>
|
||||
<Button variant="secondary" onClick={() => setModal(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -58,10 +58,10 @@ function CreateScheduledSessionModal({ buildId, startAt, duration }: Props) {
|
||||
<p className="text-sm font-semibold">Запланировать демонстрацию</p>
|
||||
<span className="text-[#77828C]">
|
||||
<Button
|
||||
color="tertiary"
|
||||
variant="tertiary"
|
||||
icon={<CloseIcon />}
|
||||
onlyIcon
|
||||
handleClick={() => setModal(null)}
|
||||
onClick={() => setModal(null)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ function CreateScheduledSessionModal({ buildId, startAt, duration }: Props) {
|
||||
<Button disabled={isLoading} type="submit">
|
||||
Запланировать
|
||||
</Button>
|
||||
<Button color="secondary" handleClick={() => setModal(null)}>
|
||||
<Button variant="secondary" onClick={() => setModal(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ function CreateUserModal({ companyId }: Props) {
|
||||
}, [builds]);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 space-y-4 rounded-xl">
|
||||
<div className="p-8 space-y-4 bg-white rounded-xl">
|
||||
<p className="text-xl font-semibold">Создание пользователя</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
@@ -117,7 +117,7 @@ function CreateUserModal({ companyId }: Props) {
|
||||
required
|
||||
/>
|
||||
<select
|
||||
className="px-3 py-2 outline-none border border-gray-300 rounded-lg"
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg outline-none"
|
||||
defaultValue={role}
|
||||
onChange={(e) => setRole(e.target.selectedOptions[0].value)}
|
||||
>
|
||||
@@ -126,7 +126,7 @@ function CreateUserModal({ companyId }: Props) {
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="px-3 py-2 outline-none border border-gray-300 rounded-lg"
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg outline-none"
|
||||
onChange={handleChangeBuilds}
|
||||
multiple
|
||||
>
|
||||
@@ -135,8 +135,8 @@ function CreateUserModal({ companyId }: Props) {
|
||||
<option value={build.id}>{build.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="self-end flex gap-2">
|
||||
<Button color="secondary" handleClick={() => setModal(null)}>
|
||||
<div className="flex self-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setModal(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit">Сохранить</Button>
|
||||
|
||||
@@ -119,7 +119,7 @@ function EditUserModal({ companyId, userId }: Props) {
|
||||
}, [user, builds, buildIds]);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 space-y-4 rounded-xl">
|
||||
<div className="p-8 space-y-4 bg-white rounded-xl">
|
||||
<p className="text-xl font-semibold">Редактирование</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
@@ -137,7 +137,7 @@ function EditUserModal({ companyId, userId }: Props) {
|
||||
required
|
||||
/>
|
||||
<select
|
||||
className="px-3 py-2 outline-none border border-gray-300 rounded-lg"
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg outline-none"
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.selectedOptions[0].value)}
|
||||
>
|
||||
@@ -146,7 +146,7 @@ function EditUserModal({ companyId, userId }: Props) {
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="px-3 py-2 outline-none border border-gray-300 rounded-lg"
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg outline-none"
|
||||
onChange={handleChangeBuilds}
|
||||
multiple
|
||||
>
|
||||
@@ -160,8 +160,8 @@ function EditUserModal({ companyId, userId }: Props) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="self-end flex gap-2">
|
||||
<Button color="secondary" handleClick={() => setModal(null)}>
|
||||
<div className="flex self-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setModal(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit">Сохранить</Button>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FormEvent, useState } from "react";
|
||||
import useAuthStore from "../../stores/useAuthStore";
|
||||
import useModalStore from "../../stores/useModalStore";
|
||||
import Button from "../Button";
|
||||
import CloseIcon from "../icons/CloseIcon";
|
||||
import Input from "../Input";
|
||||
import Label from "../Label";
|
||||
import api from "../../utils/api";
|
||||
import toast from "react-hot-toast";
|
||||
import DataField from "../DataField";
|
||||
|
||||
function SettingsModal() {
|
||||
const { setModal } = useModalStore();
|
||||
const { user } = useAuthStore();
|
||||
const [oldPassword, setOldPassword] = useState<string>("");
|
||||
const [newPassword, setNewPassword] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmitChangePassword(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
await changePassword();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
try {
|
||||
const result: any = await api
|
||||
.post("changePassword", {
|
||||
json: {
|
||||
oldPassword,
|
||||
newPassword,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
if ("error" in result) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Пароль успешно изменен");
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg w-[440px]">
|
||||
<div className="flex items-center justify-between px-2 border-b border-[#DAE0E5]">
|
||||
<div className="flex">
|
||||
<p className="p-3.5 text-sm font-semibold">Профиль</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={<CloseIcon className="w-5 h-5" />}
|
||||
onlyIcon
|
||||
onClick={() => setModal(null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start gap-6 p-6 border-b border-[#DAE0E5]">
|
||||
<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">
|
||||
<DataField label="Имя" value={user?.name} copyIcon />
|
||||
<DataField label="Email" value={user?.username} copyIcon />
|
||||
{/* <DataField label="Телефон" copyIcon /> */}
|
||||
<DataField label="Должность" value={user?.position} />
|
||||
<div className="flex gap-2">
|
||||
<Button disabled variant="secondary">
|
||||
Статистика
|
||||
</Button>
|
||||
{/* <Button color="tertiary">Выйти из аккаунта</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className=""></div> */}
|
||||
<div className="flex gap-6 p-6">
|
||||
<div className="min-w-[88px] w-[88px]">
|
||||
<p className="text-xs font-semibold">Безопасность</p>
|
||||
</div>
|
||||
<form
|
||||
className="w-full space-y-3"
|
||||
onSubmit={handleSubmitChangePassword}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label value="Старый пароль" />
|
||||
<Input
|
||||
type="password"
|
||||
required
|
||||
className="w-full"
|
||||
value={oldPassword}
|
||||
handleChange={(value) => setOldPassword(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label value="Новый пароль" />
|
||||
<Input
|
||||
type="password"
|
||||
required
|
||||
className="w-full"
|
||||
value={newPassword}
|
||||
handleChange={(value) => setNewPassword(value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!oldPassword || !newPassword || loading}
|
||||
type="submit"
|
||||
>
|
||||
Изменить
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsModal;
|
||||
@@ -75,13 +75,13 @@ function AdminCompanyPage() {
|
||||
}, [users, builds]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col gap-8 bg-gray-100 p-8">
|
||||
<div className="flex flex-col min-h-screen gap-8 p-8 bg-gray-100">
|
||||
<div className="flex gap-8">
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="border border-gray-300"
|
||||
handleClick={() => navigate("..")}
|
||||
onClick={() => navigate("..")}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
@@ -90,23 +90,23 @@ function AdminCompanyPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-xl font-semibold">Жилые комплексы</p>
|
||||
<Button
|
||||
handleClick={() =>
|
||||
onClick={() =>
|
||||
companyId && setModal(<CreateBuildModal companyId={companyId} />)
|
||||
}
|
||||
>
|
||||
Добавить ЖК
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 border-b border-gray-300 pb-8">
|
||||
<div className="grid grid-cols-4 gap-4 pb-8 border-b border-gray-300">
|
||||
{builds &&
|
||||
builds.map((build) => (
|
||||
<div className="relative bg-white rounded-xl overflow-hidden">
|
||||
<div className="relative overflow-hidden bg-white rounded-xl">
|
||||
<img
|
||||
src=""
|
||||
alt=""
|
||||
className="aspect-video bg-gray-400 object-cover"
|
||||
className="object-cover bg-gray-400 aspect-video"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-6 text-white text-left">
|
||||
<div className="absolute text-left text-white bottom-4 left-6">
|
||||
<p className="text-xl font-semibold">{build.name}</p>
|
||||
<p className="">
|
||||
Сборка приложения:{" "}
|
||||
@@ -121,11 +121,11 @@ function AdminCompanyPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 border-b border-gray-300 pb-8">
|
||||
<div className="pb-8 space-y-4 border-b border-gray-300">
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-xl font-semibold">Пользователи</p>
|
||||
<Button
|
||||
handleClick={() =>
|
||||
onClick={() =>
|
||||
companyId && setModal(<CreateUserModal companyId={companyId} />)
|
||||
}
|
||||
>
|
||||
@@ -143,7 +143,7 @@ function AdminCompanyPage() {
|
||||
<img
|
||||
src={user.avatar || "/images/no-avatar.png"}
|
||||
alt=""
|
||||
className="w-8 h-8 rounded-full bg-gray-500"
|
||||
className="w-8 h-8 bg-gray-500 rounded-full"
|
||||
/>
|
||||
|
||||
<div className="text-sm">
|
||||
@@ -161,10 +161,10 @@ function AdminCompanyPage() {
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
icon={<MoreIcon />}
|
||||
onlyIcon
|
||||
handleClick={() =>
|
||||
onClick={() =>
|
||||
companyId &&
|
||||
setModal(
|
||||
<EditUserModal companyId={companyId} userId={user.id} />
|
||||
|
||||
@@ -46,10 +46,10 @@ function AdminPage() {
|
||||
}, [companies]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col gap-8 bg-gray-100 p-8">
|
||||
<div className="flex flex-col min-h-screen gap-8 p-8 bg-gray-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-xl font-semibold">Компании</p>
|
||||
<Button handleClick={() => setModal(<CreateCompanyModal />)}>
|
||||
<Button onClick={() => setModal(<CreateCompanyModal />)}>
|
||||
Добавить компанию
|
||||
</Button>
|
||||
</div>
|
||||
@@ -57,15 +57,15 @@ function AdminPage() {
|
||||
{companies &&
|
||||
companies.map((company) => (
|
||||
<button
|
||||
className="relative bg-white rounded-xl overflow-hidden"
|
||||
className="relative overflow-hidden bg-white rounded-xl"
|
||||
onClick={() => navigate(`${company.id}`)}
|
||||
>
|
||||
<img
|
||||
src=""
|
||||
alt=""
|
||||
className="aspect-video bg-gray-400 object-cover"
|
||||
className="object-cover bg-gray-400 aspect-video"
|
||||
/>
|
||||
<p className="text-xl font-semibold text-white absolute bottom-4 left-6">
|
||||
<p className="absolute text-xl font-semibold text-white bottom-4 left-6">
|
||||
{company.name}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -17,13 +17,13 @@ import IUser from "../types/IUser";
|
||||
import IError from "../types/IError";
|
||||
import Schedule from "../components/Schedule";
|
||||
import IScheduledSession from "../types/IScheduledSession";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
|
||||
function DashboardPage() {
|
||||
const { user } = useAuthStore();
|
||||
const {
|
||||
company,
|
||||
setCompany,
|
||||
selectedBuild,
|
||||
builds,
|
||||
setBuilds,
|
||||
managers,
|
||||
@@ -55,7 +55,7 @@ function DashboardPage() {
|
||||
|
||||
async function getCompany() {
|
||||
if (!user) {
|
||||
console.log("No User", user);
|
||||
// console.log("No User", user);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,15 +64,13 @@ function DashboardPage() {
|
||||
|
||||
setCompany(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuilds() {
|
||||
if (!user) {
|
||||
console.log("No User", user);
|
||||
// console.log("No User", user);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,20 +81,18 @@ function DashboardPage() {
|
||||
|
||||
setBuilds(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getManagers() {
|
||||
if (!company || !selectedBuild) {
|
||||
if (!company) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: IUser[] | IError = await api
|
||||
.get(`users?companyId=${company.id}&buildIds=${selectedBuild.id}`)
|
||||
.get(`companies/${company.id}/users`)
|
||||
.json();
|
||||
|
||||
if ("error" in result) {
|
||||
@@ -104,8 +100,6 @@ function DashboardPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("result", result);
|
||||
|
||||
setManagers(result);
|
||||
} catch (error) {
|
||||
alert((error as Error).message);
|
||||
@@ -114,7 +108,7 @@ function DashboardPage() {
|
||||
|
||||
async function getScheduledSessions(useLoader?: boolean) {
|
||||
if (!company) {
|
||||
console.log("No ScheduledSessions");
|
||||
// console.log("No ScheduledSessions");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,13 +123,9 @@ function DashboardPage() {
|
||||
)
|
||||
.json();
|
||||
|
||||
console.log(result);
|
||||
|
||||
setScheduledSessions(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log("Error: ", error.message);
|
||||
}
|
||||
toast.error((error as Error).message);
|
||||
}
|
||||
|
||||
if (useLoader) setIsLoadingScheduledSessions(false);
|
||||
@@ -200,110 +190,23 @@ function DashboardPage() {
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
handleClick={selectPrevDay}
|
||||
icon={<ChevronLeftIcon />}
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
onlyIcon
|
||||
onClick={selectPrevDay}
|
||||
/>
|
||||
<Button
|
||||
handleClick={selectNextDay}
|
||||
variant="secondary"
|
||||
icon={<ChevronRightIcon />}
|
||||
color="secondary"
|
||||
onlyIcon
|
||||
onClick={selectNextDay}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button handleClick={selectCurrentDate}>Сегодня</Button>
|
||||
<Button onClick={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
|
||||
ref={scheduledSessionsRef}
|
||||
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>
|
||||
|
||||
{generatedScheduledSessions?.map(
|
||||
(generatedScheduledSession, index) => (
|
||||
<div key={index} className="flex">
|
||||
<div className="w-[84px] h-[164px] flex justify-center items-center text-sm font-semibold bg-white border-r border-b border-[#DAE0E5]">
|
||||
<p>{format(generatedScheduledSession.time, "HH:mm")}</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{company &&
|
||||
managers &&
|
||||
selectedBuild &&
|
||||
generatedScheduledSession.sessions.map(
|
||||
(session: any, index2: number) => {
|
||||
const selectedManager = managers.find(
|
||||
(manager) => manager.id == session.userId
|
||||
);
|
||||
|
||||
if (!_.isEmpty(session)) {
|
||||
return (
|
||||
<Card
|
||||
key={index2}
|
||||
companyId={company.id}
|
||||
buildId={selectedBuild.id}
|
||||
scheduledSessionId={session.id}
|
||||
scheduleSessionStartAt={session.startAt}
|
||||
client={session.client}
|
||||
manager={selectedManager}
|
||||
managers={managers}
|
||||
handleSelect={(scheduledSessionId, managerId) =>
|
||||
updateScheduledSessionManager(
|
||||
scheduledSessionId,
|
||||
managerId
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EmptyCard
|
||||
key={index2}
|
||||
buildId={selectedBuild?.id}
|
||||
startAt={generatedScheduledSession.time}
|
||||
duration={duration!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
{company && scheduledSessions && (
|
||||
<Schedule
|
||||
selectedDay={selectedDay}
|
||||
@@ -332,6 +235,7 @@ function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<ModalContainer />
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Input from "../components/Input";
|
||||
import Button from "../components/Button";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import toast from "react-hot-toast";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import IError from "../types/IError";
|
||||
import IUser from "../types/IUser";
|
||||
import api from "../utils/api";
|
||||
@@ -32,19 +32,11 @@ function LoginPage() {
|
||||
|
||||
if ("error" in result) {
|
||||
setLoading(false);
|
||||
toast.error(`Error: ${result.error}`);
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setUser({
|
||||
id: result.id,
|
||||
username: "username",
|
||||
accessToken: result.accessToken,
|
||||
companyId: result.companyId,
|
||||
name: result.name,
|
||||
role: result.role,
|
||||
buildIds: result.buildIds,
|
||||
});
|
||||
setUser(result);
|
||||
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
@@ -88,6 +80,7 @@ function LoginPage() {
|
||||
size="medium"
|
||||
className="mt-10"
|
||||
loading={loading}
|
||||
widthFull
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
@@ -102,6 +95,8 @@ function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ 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 items-center justify-center flex-1 min-h-screen p-8">
|
||||
<div className="overflow-hidden bg-white rounded-lg shadow-md ">
|
||||
<div className="flex flex-col gap-6 p-12 w-[528px]">
|
||||
<p className="text-2xl font-semibold">Получение данных для входа</p>
|
||||
|
||||
@@ -22,7 +22,7 @@ function RegistrationPage() {
|
||||
</div>
|
||||
|
||||
<Link to="/login">
|
||||
<Button size="medium" color="secondary">
|
||||
<Button variant="secondary" size="medium">
|
||||
Назад
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,11 @@ interface ICompany {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionLimit: number;
|
||||
site?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export default ICompany;
|
||||
|
||||
@@ -7,6 +7,8 @@ interface IUser {
|
||||
name: string;
|
||||
role: string;
|
||||
buildIds?: string[];
|
||||
position?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export default IUser;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import ky from "ky";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import IError from "../types/IError";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const api = ky.extend({
|
||||
prefixUrl: import.meta.env.VITE_API_URL,
|
||||
@@ -38,8 +37,6 @@ const api = ky.extend({
|
||||
} catch (error) {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
} else {
|
||||
toast.error(response.statusText);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user