upd create session modal

This commit is contained in:
2025-06-03 18:44:03 +05:00
parent 3cabd710a5
commit 9870866f1e
10 changed files with 237 additions and 147 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+10 -4
View File
@@ -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
)}
>
+135 -2
View File
@@ -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
View File
@@ -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>
)}
+2 -2
View File
@@ -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]">
+14
View File
@@ -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 -7
View File
@@ -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"
+5 -11
View File
@@ -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>
);
+12
View File
@@ -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;
+47 -111
View File
@@ -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>
);
}