upd create session modal
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -14,14 +14,20 @@ function NewButton({
|
||||
size = "medium",
|
||||
className,
|
||||
ref,
|
||||
type,
|
||||
onClick,
|
||||
...props
|
||||
}: NewButtonProps) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
if (type !== "submit") e.preventDefault();
|
||||
onClick?.(e);
|
||||
}}
|
||||
className={clsx(
|
||||
"transition-all 2xl:rounded-[0.556vw] rounded-lg flex 2xl:gap-[0.556vw] gap-2 items-center justify-between font-medium disabled:bg-[#F6F6F6] disabled:text-[#D6D6D6]",
|
||||
"transition-all 2xl:rounded-[0.556vw] rounded-lg flex 2xl:gap-[0.556vw] gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:text-[#D6D6D6]",
|
||||
variant === "critical" &&
|
||||
"text-[#FF4517] bg-[#FEF3F2] hover:bg-[#FEE4E2]",
|
||||
variant === "secondary" &&
|
||||
@@ -30,9 +36,9 @@ function NewButton({
|
||||
"bg-[#F8F7FE] text-[#7B60F3] hover:bg-[#E1DEFC] active:bg-[#F8F7FE]",
|
||||
variant === "cta" &&
|
||||
"bg-[#7B60F3] text-white hover:bg-[#9184F6] active:bg-[#B3AAF9]",
|
||||
size === "large" && "2xl:p-[1.111vw] p-4",
|
||||
size === "medium" && "2xl:p-[0.833vw] p-3",
|
||||
size === "small" && "2xl:p-[0.556vw] p-2",
|
||||
size === "large" && "2xl:p-[1.111vw] p-4 button-m",
|
||||
size === "medium" && "2xl:p-[0.833vw] p-3 button-s",
|
||||
size === "small" && "2xl:p-[0.556vw] p-2 text-[10px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,138 @@
|
||||
function ProjectSelector() {
|
||||
return <div></div>;
|
||||
import { useState } from "react";
|
||||
import { IApp } from "../types/IApp";
|
||||
import ChevronLeftIcon from "./icons/ChevronLeftIcon";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import LightningIcon from "./icons/LightningIcon";
|
||||
import NewButton from "./NewButton";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
|
||||
interface Props {
|
||||
projects: IApp[];
|
||||
selectedProject: IApp | null;
|
||||
setSelectedProject: (project: IApp | null) => void;
|
||||
}
|
||||
|
||||
function ProjectSelector({
|
||||
projects,
|
||||
selectedProject,
|
||||
setSelectedProject,
|
||||
}: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [pointedProject, setPointedProject] = useState<IApp | null>(
|
||||
selectedProject
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="p-[1.111vw] rounded-[0.833vw] bg-[#F6F6F6] flex justify-between items-center gap-[0.833vw]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className="space-y-[0.278vw]">
|
||||
<p className="caption-s text-[#7D7D7D] w-fit font-medium">Проект</p>
|
||||
<p className="text-s w-fit">{selectedProject?.name}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-[0.556vw]">
|
||||
<img src="/images/app_image.png" className="size-[2.222vw]" alt="" />
|
||||
<div className="size-[1.389vw] text-[#7D7D7D]">
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="fixed z-1 top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 rounded-[2.222vw] bg-white p-[1.389vw] flex flex-col gap-[2.778vw] w-[25vw]">
|
||||
<div className="flex justify-between items-center">
|
||||
<NewButton variant="secondary" size="small">
|
||||
<div className="text-[#7D7D7D] size-[0.972vw]">
|
||||
<ChevronLeftIcon />
|
||||
</div>
|
||||
</NewButton>
|
||||
<p className="title-s font-medium">Смена проекта</p>
|
||||
<NewButton
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="text-[#7D7D7D] size-[0.972vw]">
|
||||
<CloseIcon />
|
||||
</span>
|
||||
</NewButton>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1.667vw]">
|
||||
<div className="flex flex-col">
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPointedProject(project);
|
||||
}}
|
||||
className="flex justify-between items-center not-last:border-b py-[0.883vw] border-[#F6F6F6] cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-[0.556vw]">
|
||||
<img
|
||||
src="/images/app_image.png"
|
||||
className="size-[2.222vw] object-cover"
|
||||
alt=""
|
||||
/>
|
||||
<div className="space-y-[0.278vw]">
|
||||
<div className="flex items-center gap-[0.278vw]">
|
||||
<p className="text-s">{project.name}</p>
|
||||
<span className="size-[0.972vw] text-[#7B60F3]">
|
||||
<LightningIcon />
|
||||
</span>
|
||||
</div>
|
||||
<p className="caption-s text-[#7D7D7D] font-medium">
|
||||
Доступно 128 квартир
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{pointedProject?.id === project.id ? (
|
||||
<div className="size-[1.389vw] flex items-center justify-center rounded-full bg-[#7B60F3]">
|
||||
<div className="size-[0.833vw] text-white">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-full bg-[#F6F6F6] ring ring-[#F0F0F0] size-[1.389vw]" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-[0.556vw]">
|
||||
<NewButton
|
||||
variant="cta"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setSelectedProject(pointedProject);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Переключиться
|
||||
</NewButton>
|
||||
<NewButton
|
||||
variant="primary"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Отменить
|
||||
</NewButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectSelector;
|
||||
|
||||
+11
-10
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useState } from "react";
|
||||
import Button from "./Button";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import ArrowDownIcon from "./icons/ArrowDownIcon";
|
||||
import NewButton from "./NewButton";
|
||||
|
||||
interface Props {
|
||||
options: string[]; // ["StroyProject"]
|
||||
@@ -31,11 +31,13 @@ export default function Select({ options, onChange }: Props) {
|
||||
ref={ref}
|
||||
className="relative outline-black/10 outline w-[13.889vw] rounded-[0.556vw]"
|
||||
>
|
||||
<Button
|
||||
onlyIcon
|
||||
variant="tertiary"
|
||||
<NewButton
|
||||
variant="secondary"
|
||||
className="px-[0.833vw] py-[0.417vw] !justify-between w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
>
|
||||
<p className="text-[0.972vw] leading-[115%]">
|
||||
{selectedOption || "Выберите из списка"}
|
||||
@@ -47,21 +49,20 @@ export default function Select({ options, onChange }: Props) {
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</Button>
|
||||
</NewButton>
|
||||
{isOpen && (
|
||||
<div className="absolute top-full w-full bg-white rounded-[0.556vw] outline outline-black/10">
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
<NewButton
|
||||
key={option}
|
||||
onlyIcon
|
||||
variant="tertiary"
|
||||
variant="secondary"
|
||||
className="px-[0.833vw] py-[0.417vw] !justify-start w-full !first:rounded-t-[0.556vw] !last:rounded-b-[0.556vw]"
|
||||
onClick={() => handleClickOption(option)}
|
||||
>
|
||||
<div className="flex gap-[0.278vw] items-center">
|
||||
<p className="text-[0.972vw] leading-[115%]">{option}</p>
|
||||
</div>
|
||||
</Button>
|
||||
</NewButton>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -30,9 +30,9 @@ function TableSelector({
|
||||
>
|
||||
<p className="button-m font-medium">{table.name}</p>
|
||||
{table.status === "offline" ? (
|
||||
<p className="text-[#D6D6D6]">Недоступен</p>
|
||||
<p className="text-[#D6D6D6] font-medium caption-s">Недоступен</p>
|
||||
) : table.sessions?.[0].status === "ended" ? (
|
||||
<p className="text-[#29AF61] caption-s">Свободен</p>
|
||||
<p className="text-[#29AF61] caption-s font-medium">Свободен</p>
|
||||
) : (
|
||||
<div className="flex gap-[0.139vw] items-center">
|
||||
<span className="size-[0.883vw] text-[#7B60F3]">
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.874 3.668a.5.5 0 0 1-.042.706l-4.5 4a.5.5 0 0 1-.686-.02l-2.5-2.5a.5.5 0 1 1 .708-.708L5.02 7.313l4.148-3.687a.5.5 0 0 1 .706.042"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckIcon;
|
||||
@@ -1,12 +1,6 @@
|
||||
function ChevronDownIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.833 8.333 10 12.5l4.167-4.167"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
function ChevronLeftIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={7}
|
||||
height={12}
|
||||
viewBox='0 0 7 12'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d='M0.75 1L5.75 6L0.75 11'
|
||||
stroke='currentColor'
|
||||
d="M7.875 3.5 4.375 7l3.5 3.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
function StartSessionIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 7 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.938 3.595a.5.5 0 0 1 0 .81L1.293 7.758A.5.5 0 0 1 .5 7.353V.647a.5.5 0 0 1 .793-.405z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default StartSessionIcon;
|
||||
@@ -1,16 +1,15 @@
|
||||
import { IServer } from "../../types/IServer.ts";
|
||||
import Button from "../Button.tsx";
|
||||
import DesktopSelect from "../DesktopSelect.tsx";
|
||||
import Input from "../Input.tsx";
|
||||
import DisplayIcon from "../icons/DisplayIcon.tsx";
|
||||
import Select from "../Select.tsx";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { IApp } from "../../types/IApp.ts";
|
||||
import api from "../../utils/api.ts";
|
||||
import { ISession } from "../../types/ISession.ts";
|
||||
import { IClient } from "../../types/IClient.ts";
|
||||
import useModalStore from "../../stores/useModalStore.ts";
|
||||
import TableSelector from "../TableSelector.tsx";
|
||||
import NewInput from "../NewInput.tsx";
|
||||
import StartSessionIcon from "../icons/StartSessionIcon.tsx";
|
||||
import NewButton from "../NewButton.tsx";
|
||||
import ProjectSelector from "../ProjectSelector.tsx";
|
||||
|
||||
interface Props {
|
||||
servers: IServer[] | undefined;
|
||||
@@ -31,8 +30,6 @@ export default function CreateSessionModal({
|
||||
const [selectedApp, setSelectedApp] = useState<IApp | null>(null);
|
||||
|
||||
async function createClient() {
|
||||
console.log(name, phone, email);
|
||||
|
||||
return await api
|
||||
.post("clients", {
|
||||
json: {
|
||||
@@ -71,10 +68,13 @@ export default function CreateSessionModal({
|
||||
}
|
||||
}
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="rounded-[2.222vw] w-[25vw] min-h-[calc(100dvh-2.222vw)] bg-[#F0F0F0] flex flex-col overflow-hidden"
|
||||
onSubmit={handleClickCreateSession}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="w-full h-[4.861vw] flex items-center justify-center">
|
||||
<p className="title-s font-medium">Новый сеанс</p>
|
||||
@@ -89,117 +89,53 @@ export default function CreateSessionModal({
|
||||
<div className="flex flex-col gap-y-[0.833vw]">
|
||||
<p className="title-s font-medium">Укажите данные клиента</p>
|
||||
<div className="flex flex-col gap-y-[0.556vw]">
|
||||
<Input placeholder="Номер телефона" required />
|
||||
<Input placeholder="Имя" required />
|
||||
<Input placeholder="Электронная почта" />
|
||||
<NewInput
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="Номер телефона"
|
||||
required
|
||||
/>
|
||||
<NewInput
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Имя"
|
||||
required
|
||||
/>
|
||||
<NewInput
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Электронная почта"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-[0.833vw]">
|
||||
<p className="title-s font-medium">Выберите параметры сеанса</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="gap-y-[1.111vw] flex flex-col justify-between">
|
||||
<div className="space-y-[0.556vw]">
|
||||
<div className="p-[0.833vw] ring-[0.069vw] ring-[#E6E6E6] w-fit rounded-[0.556vw]">
|
||||
<div className="w-[1.389vw] h-[1.389vw]">
|
||||
<DisplayIcon />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[1.389vw]">Создание сеанса</p>
|
||||
<p className="text-[0.833vw] text-black/20">
|
||||
Укажите данные клиента, выберите менеджера и стол
|
||||
</p>
|
||||
</div>
|
||||
<hr className="border-black/10" />
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">
|
||||
Имя <span className="text-[#C6C6C699]">*</span>
|
||||
</p>
|
||||
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
|
||||
<Input
|
||||
placeholder="Константин"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
{selectedServer?.apps?.length && selectedServer?.apps?.length > 0 && (
|
||||
<ProjectSelector
|
||||
projects={selectedServer?.apps || []}
|
||||
selectedProject={selectedApp ?? selectedServer?.apps?.[0] ?? null}
|
||||
setSelectedProject={setSelectedApp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">
|
||||
Номер <span className="text-[#C6C6C699]">*</span>
|
||||
</p>
|
||||
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
|
||||
<Input
|
||||
placeholder="79221234567"
|
||||
required
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">Электронная почта</p>
|
||||
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
|
||||
<Input
|
||||
placeholder="sample@mail.ru"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-[0.556vw]">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">Стол</p>
|
||||
<DesktopSelect
|
||||
servers={servers?.filter(
|
||||
({ sessions }) =>
|
||||
!sessions ||
|
||||
!sessions.length ||
|
||||
sessions[0]?.status === "ended"
|
||||
)}
|
||||
value={selectedServer || undefined}
|
||||
onChange={setSelectedServer}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[0.694vw] text-black/30 w-[13.889vw] leading-[115%] self-end">
|
||||
При запуске нового сеанса текущий будет завершен принудительно.
|
||||
</p>
|
||||
</div>
|
||||
{selectedServer && (
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">Проекты</p>
|
||||
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
|
||||
<Select
|
||||
options={selectedServer.apps?.map((app) => app.name) || []}
|
||||
onChange={(option) =>
|
||||
setSelectedApp(
|
||||
selectedServer.apps?.find((app) => app.name === option)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<hr className="border-black/10" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-[#F9F9F9] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]"
|
||||
onClick={() => setModal(null)}
|
||||
>
|
||||
<p className="text-black font-medium text-[0.972vw]">Отменить</p>
|
||||
</Button>
|
||||
<Button
|
||||
<NewButton
|
||||
type="submit"
|
||||
className="bg-[#EF3C26] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]"
|
||||
disabled={
|
||||
!ref.current?.checkValidity() || !selectedServer || !selectedApp
|
||||
}
|
||||
variant="cta"
|
||||
size="large"
|
||||
className="sticky bottom-[1.111vw]"
|
||||
>
|
||||
<p className="text-[0.972vw] font-medium">Запустить сеанс</p>
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="rounded-full bg-[#9184F6] in-disabled:bg-transparent px-[0.278vw] py-[0.208vw] size-[1.111vw]">
|
||||
<div className="w-[0.694vw] h-[0.556vw]">
|
||||
<StartSessionIcon />
|
||||
</div>
|
||||
</div>
|
||||
<p>Запустить сеанс</p>
|
||||
</NewButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user