Merge branch 'main' of http://192.168.1.163:3000/inmake/graff-mate-client
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"name": "graff-mate-client",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@mona-health/react-input-mask": "^3.0.3",
|
||||
"@react-router/dev": "^7.3.0",
|
||||
"@tailwindcss/vite": "^4.0.13",
|
||||
"@tanstack/react-query": "^5.67.3",
|
||||
@@ -186,6 +187,8 @@
|
||||
|
||||
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
||||
|
||||
"@mona-health/react-input-mask": ["@mona-health/react-input-mask@3.0.3", "", { "dependencies": { "invariant": "^2.2.4", "prop-types": "^15.7.2", "warning": "^4.0.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-E8t4ZAaHjmPT1NS/9nlIGR7OsyshP7hs5OwqEYnVslm/eaWrQg5ZziB6r5IOxGmFUyB+G1muFPobVPA08L2sJg=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
@@ -488,6 +491,8 @@
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@@ -556,6 +561,8 @@
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
@@ -590,6 +597,8 @@
|
||||
|
||||
"npm-pick-manifest": ["npm-pick-manifest@8.0.2", "", { "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "npm-package-arg": "^10.0.0", "semver": "^7.3.5" } }, "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
@@ -624,6 +633,8 @@
|
||||
|
||||
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
@@ -634,6 +645,8 @@
|
||||
|
||||
"react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"react-router": ["react-router@7.3.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw=="],
|
||||
@@ -726,6 +739,8 @@
|
||||
|
||||
"vite-node": ["vite-node@3.0.0-beta.2", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg=="],
|
||||
|
||||
"warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
Generated
+3491
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@mona-health/react-input-mask": "^3.0.3",
|
||||
"@react-router/dev": "^7.3.0",
|
||||
"@tailwindcss/vite": "^4.0.13",
|
||||
"@tanstack/react-query": "^5.67.3",
|
||||
|
||||
+15
-10
@@ -1,5 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import SpinIcon from "./icons/SpinIcon";
|
||||
import InputMask from "@mona-health/react-input-mask";
|
||||
|
||||
interface NewInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
placeholder?: string;
|
||||
@@ -7,6 +8,7 @@ interface NewInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
errorMessage?: string;
|
||||
isLoading?: boolean;
|
||||
children?: React.ReactNode;
|
||||
mask?: string;
|
||||
}
|
||||
|
||||
function Input({
|
||||
@@ -15,20 +17,23 @@ function Input({
|
||||
errorMessage,
|
||||
isLoading,
|
||||
children,
|
||||
mask,
|
||||
...props
|
||||
}: NewInputProps) {
|
||||
return (
|
||||
<div className={clsx("relative", props.disabled && "opacity-40")}>
|
||||
<input
|
||||
{...props}
|
||||
placeholder=""
|
||||
className={clsx(
|
||||
isError
|
||||
? "hover:ring-[#FF4517] focus:ring-[#FF4517]"
|
||||
: "hover:ring-[#7B60F3] focus:ring-[#7B60F3]",
|
||||
"peer bg-[#F6F6F6] rounded-[0.833vw] px-[1.111vw] pt-[19px] pb-[11px] outline-none ring-1 ring-transparent transition-all inline-block w-full h-[3.889vw] text-m"
|
||||
)}
|
||||
/>
|
||||
<InputMask mask={mask || ""} {...props}>
|
||||
<input
|
||||
placeholder=""
|
||||
className={clsx(
|
||||
isError
|
||||
? "hover:ring-[#FF4517] focus:ring-[#FF4517]"
|
||||
: "hover:ring-[#7B60F3] focus:ring-[#7B60F3]",
|
||||
"peer bg-[#F6F6F6] rounded-[0.833vw] px-[1.111vw] pt-[19px] pb-[11px] outline-none ring-1 ring-transparent transition-all inline-block w-full h-[3.889vw] text-m"
|
||||
)}
|
||||
/>
|
||||
</InputMask>
|
||||
|
||||
{children}
|
||||
{placeholder && (
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Manager } from "../types/Manager";
|
||||
import SortIcon from "./icons/SortIcon";
|
||||
import clsx from "clsx";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { getPositionAbove } from "../utils/getPositionAbove";
|
||||
|
||||
function ManagerSelect({
|
||||
placeholder,
|
||||
data,
|
||||
}: {
|
||||
placeholder: string;
|
||||
data: Manager[];
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedManager, setSelectedManager] = useState<Manager | null>(
|
||||
data[0]
|
||||
);
|
||||
const [position, setPosition] = useState<"top" | "bottom">("bottom");
|
||||
const selectRef = useClickAway<HTMLDivElement>(() => setIsOpen(false));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !selectRef.current) return;
|
||||
|
||||
setPosition(getPositionAbove(selectRef) ? "top" : "bottom");
|
||||
}, [isOpen, selectRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (isOpen) {
|
||||
setPosition(getPositionAbove(selectRef) ? "top" : "bottom");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
return () => window.removeEventListener("scroll", handleScroll, true);
|
||||
}, [isOpen, selectRef]);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={selectRef}
|
||||
className={clsx(
|
||||
"relative w-full rounded-[0.833vw] p-[1.111vw] bg-[#F6F6F6] cursor-pointer flex items-center justify-between select-none",
|
||||
isOpen && "outline outline-[#7B60F3]"
|
||||
)}
|
||||
style={{ boxShadow: "0px 2px 2px 0px #0000000D" }}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className="flex flex-col gap-[0.278vw]">
|
||||
<div className="caption-s font-medium text-[#7D7D7D]">
|
||||
{placeholder}
|
||||
</div>
|
||||
<div className="flex items-center gap-[0.556vw]">
|
||||
<div className="size-[1.111vw] rounded-full bg-[#7D7D7D]" />
|
||||
<div className="text-s">{selectedManager?.fullname}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="size-[1.389vw] text-[#7D7D7D]">
|
||||
<SortIcon />
|
||||
</span>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={clsx(
|
||||
"absolute left-0 w-full z-10",
|
||||
position === "top"
|
||||
? "bottom-[calc(100%+4px)]"
|
||||
: "top-[calc(100%+4px)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="w-full rounded-[0.833vw] p-[0.833vw] max-h-[11.389vw] bg-white overflow-y-auto [scrollbar-width:thin]"
|
||||
style={{ boxShadow: "0px 4px 40px 0px #0000000D" }}
|
||||
>
|
||||
{data.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-[0.833vw] flex items-center gap-[0.556vw] hover:bg-[#F6F6F6] rounded-[0.278vw]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedManager(item);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="size-[1.111vw] rounded-full text-[#7B60F3] flex items-center justify-center">
|
||||
{selectedManager?.id === item.id && <CheckIcon />}
|
||||
</div>
|
||||
<div className="size-[1.111vw] rounded-full bg-[#7D7D7D]" />
|
||||
<div className="text-s">{item.fullname}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManagerSelect;
|
||||
@@ -96,7 +96,10 @@ function MultySelect<T extends { name: string; id: string }>({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="!justify-start w-full text-s font-medium p-[0.833vw] text-[#7D7D7D] flex items-center gap-[0.278vw] cursor-pointer rounded-[0.278vw] hover:bg-[#F6F6F6] bg-white"
|
||||
onClick={() => setSelectedValues([])}
|
||||
onClick={() => {
|
||||
setSelectedValues([]);
|
||||
setIsSelectVisible(false);
|
||||
}}
|
||||
>
|
||||
<span className="size-[1.111vw] flex items-center justify-center">
|
||||
<CloseIcon />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { App } from "../types/App";
|
||||
import ChevronLeftIcon from "./icons/ChevronLeftIcon";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import LightningIcon from "./icons/LightningIcon";
|
||||
import Button from "./Button";
|
||||
import CheckIcon from "./icons/CheckIcon";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import clsx from "clsx";
|
||||
import FlashIcon from "./icons/FlashIcon";
|
||||
import SortIcon from "./icons/SortIcon";
|
||||
import { getPositionAbove } from "../utils/getPositionAbove";
|
||||
|
||||
interface Props {
|
||||
projects: App[];
|
||||
@@ -21,124 +22,105 @@ function ProjectSelector({
|
||||
activeProject,
|
||||
}: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [pointedProject, setPointedProject] = useState<App | null>(null);
|
||||
const [position, setPosition] = useState<"top" | "bottom">("bottom");
|
||||
const selectRef = useClickAway<HTMLDivElement>(() => setIsOpen(false));
|
||||
|
||||
useEffect(() => {
|
||||
setPointedProject(selectedProject);
|
||||
}, [selectedProject]);
|
||||
setPosition(getPositionAbove(selectRef) ? "top" : "bottom");
|
||||
}, [isOpen, selectRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (isOpen) {
|
||||
setPosition(getPositionAbove(selectRef) ? "top" : "bottom");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
return () => window.removeEventListener("scroll", handleScroll, true);
|
||||
}, [isOpen, selectRef]);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
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 font-medium w-fit">{selectedProject?.name}</p>
|
||||
</div>
|
||||
<div
|
||||
ref={selectRef}
|
||||
className={clsx(
|
||||
"relative w-full rounded-[0.833vw] p-[1.111vw] bg-[#F6F6F6] cursor-pointer flex items-center justify-between select-none",
|
||||
isOpen && "outline outline-[#7B60F3]"
|
||||
)}
|
||||
style={{ boxShadow: "0px 2px 2px 0px #0000000D" }}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className="flex flex-col gap-[0.278vw]">
|
||||
<div className="caption-s font-medium text-[#7D7D7D]">Проект</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 className="text-s">{selectedProject?.name}</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">
|
||||
<Button variant="secondary" size="small">
|
||||
<div className="text-[#7D7D7D] size-[0.972vw]">
|
||||
<ChevronLeftIcon />
|
||||
</div>
|
||||
</Button>
|
||||
<p className="title-s font-medium">Смена проекта</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
</div>
|
||||
<div className="flex items-center gap-[0.556vw]">
|
||||
<img
|
||||
src="/images/app_image.png"
|
||||
className="size-[2.222vw]"
|
||||
alt="app_image"
|
||||
/>
|
||||
<span className="size-[1.389vw] text-[#7D7D7D]">
|
||||
<SortIcon />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={clsx(
|
||||
"absolute left-0 w-full z-10",
|
||||
position === "top"
|
||||
? "bottom-[calc(100%+4px)]"
|
||||
: "top-[calc(100%+4px)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="w-full rounded-[0.833vw] p-[0.833vw] max-h-[11.389vw] bg-white overflow-y-auto [scrollbar-width:thin]"
|
||||
style={{ boxShadow: "0px 4px 40px 0px #0000000D" }}
|
||||
>
|
||||
<span className="text-[#7D7D7D] size-[0.972vw]">
|
||||
<CloseIcon />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1.667vw]">
|
||||
<div className="flex flex-col">
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
<div
|
||||
key={project.id}
|
||||
className="p-[0.833vw] flex items-center gap-[0.556vw] hover:bg-[#F6F6F6] rounded-[0.278vw]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPointedProject(project);
|
||||
e.stopPropagation();
|
||||
setSelectedProject(project);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
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 font-medium">{project.name}</p>
|
||||
{activeProject &&
|
||||
project.name === activeProject.name && (
|
||||
<span className="size-[0.972vw] text-[#7B60F3]">
|
||||
<LightningIcon />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="caption-s text-[#7D7D7D] font-medium">
|
||||
Доступно 128 квартир
|
||||
</p>
|
||||
</div>
|
||||
<div className="size-[1.111vw] rounded-full text-[#7B60F3] flex items-center justify-center">
|
||||
{selectedProject?.id === project.id && <CheckIcon />}
|
||||
</div>
|
||||
{pointedProject?.name === project.name ? (
|
||||
<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>
|
||||
<img
|
||||
src="/images/app_image.png"
|
||||
className="size-[1.389vw]"
|
||||
alt=""
|
||||
/>
|
||||
<div className="flex items-center gap-[0.278vw]">
|
||||
<div className="text-s">{project.name}</div>
|
||||
{activeProject && project.name === activeProject.name && (
|
||||
<span className="size-[0.972vw] text-[#7B60F3]">
|
||||
<FlashIcon />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-[0.556vw]">
|
||||
<Button
|
||||
variant="cta"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setSelectedProject(pointedProject);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Переключиться
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Отменить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import { Server } from "../types/Server";
|
||||
import LightningIcon from "./icons/LightningIcon";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface TableSelectorProps {
|
||||
tables: Server[];
|
||||
selectedTable: Server | null;
|
||||
onSelect: (table: Server) => void;
|
||||
onSelect: (table: Server | null) => void;
|
||||
}
|
||||
|
||||
function TableSelector({
|
||||
@@ -13,6 +14,14 @@ function TableSelector({
|
||||
selectedTable,
|
||||
onSelect,
|
||||
}: TableSelectorProps) {
|
||||
useEffect(() => {
|
||||
if (selectedTable !== null) {
|
||||
onSelect(selectedTable);
|
||||
} else {
|
||||
onSelect(tables.find((table) => table.status === "online") || null);
|
||||
}
|
||||
}, [onSelect, selectedTable, tables]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-[0.556vw] overflow-x-auto -mx-[1.111vw] pl-[1.111vw] [scrollbar-width:none]">
|
||||
{tables.map((table) => (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
function SortIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.8571 11.5257L9.99943 14.3905L7.14179 11.5257C7.01635 11.4002 6.84623 11.3297 6.66884 11.3297C6.49146 11.3297 6.32133 11.4002 6.1959 11.5257C6.07047 11.6511 6 11.8213 6 11.9987C6 12.1761 6.07047 12.3463 6.1959 12.4717L9.52649 15.8029C9.58841 15.8654 9.66209 15.9149 9.74326 15.9488C9.82443 15.9826 9.9115 16 9.99943 16C10.0874 16 10.1744 15.9826 10.2556 15.9488C10.3368 15.9149 10.4105 15.8654 10.4724 15.8029L13.803 12.4717C13.8651 12.4096 13.9143 12.3359 13.948 12.2547C13.9816 12.1736 13.9989 12.0866 13.9989 11.9987C13.9989 11.9109 13.9816 11.8239 13.948 11.7427C13.9143 11.6615 13.8651 11.5878 13.803 11.5257C13.7409 11.4636 13.6671 11.4143 13.586 11.3807C13.5048 11.3471 13.4179 11.3297 13.33 11.3297C13.2422 11.3297 13.1552 11.3471 13.0741 11.3807C12.9929 11.4143 12.9192 11.4636 12.8571 11.5257ZM7.14179 8.47432L9.99943 5.6095L12.8571 8.47432C12.919 8.53676 12.9927 8.58633 13.0738 8.62015C13.155 8.65397 13.2421 8.67139 13.33 8.67139C13.418 8.67139 13.505 8.65397 13.5862 8.62015C13.6674 8.58633 13.741 8.53676 13.803 8.47432C13.8654 8.41238 13.915 8.33869 13.9488 8.25751C13.9826 8.17632 14 8.08924 14 8.00129C14 7.91334 13.9826 7.82626 13.9488 7.74507C13.915 7.66388 13.8654 7.59019 13.803 7.52826L10.4724 4.19707C10.4105 4.13463 10.3368 4.08506 10.2556 4.05124C10.1744 4.01741 10.0874 4 9.99943 4C9.9115 4 9.82443 4.01741 9.74326 4.05124C9.66209 4.08506 9.58841 4.13463 9.52649 4.19707L6.1959 7.52826C6.13379 7.59038 6.08453 7.66412 6.05091 7.74529C6.0173 7.82645 6 7.91344 6 8.00129C6 8.17871 6.07047 8.34886 6.1959 8.47432C6.32133 8.59977 6.49146 8.67025 6.66884 8.67025C6.84623 8.67025 7.01635 8.59977 7.14179 8.47432Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SortIcon;
|
||||
@@ -4,7 +4,7 @@ function StartSessionIcon() {
|
||||
<rect width={20} height={20} rx={10} fill="#fff" fillOpacity={0.1} />
|
||||
<path
|
||||
d="M13.438 9.595a.5.5 0 0 1 0 .81l-4.645 3.353A.5.5 0 0 1 8 13.353V6.647a.5.5 0 0 1 .793-.405z"
|
||||
fill="currentColor"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,14 @@ function ClientModal({ client }: { client: Client }) {
|
||||
|
||||
const { mutate: updateClientData, isPending } = useMutation({
|
||||
mutationKey: ["clients", client.id],
|
||||
mutationFn: () => api.put(`clients/${client.id}`, { json: clientData }),
|
||||
mutationFn: () =>
|
||||
api.put(`clients/${client.id}`, {
|
||||
json: {
|
||||
name: clientData.name,
|
||||
phone: clientData.phone.replace(/\D/g, ""),
|
||||
email: clientData.email,
|
||||
},
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["clients"] });
|
||||
},
|
||||
@@ -59,7 +66,6 @@ function ClientModal({ client }: { client: Client }) {
|
||||
onChange={(e) => {
|
||||
setClientData({ ...clientData, name: e.target.value });
|
||||
}}
|
||||
className="relative"
|
||||
required
|
||||
>
|
||||
<span
|
||||
@@ -77,8 +83,8 @@ function ClientModal({ client }: { client: Client }) {
|
||||
onChange={(e) => {
|
||||
setClientData({ ...clientData, phone: e.target.value });
|
||||
}}
|
||||
className="relative"
|
||||
required
|
||||
mask="+7 (999) 999-99-99"
|
||||
>
|
||||
<span
|
||||
className="absolute top-[1.25vw] left-[17.917vw] size-[1.389vw] text-[#7D7D7D] cursor-pointer"
|
||||
@@ -103,7 +109,15 @@ function ClientModal({ client }: { client: Client }) {
|
||||
size="large"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
disabled={!clientData.name || !clientData.phone || isPending}
|
||||
disabled={
|
||||
!clientData.name ||
|
||||
!clientData.phone ||
|
||||
isPending ||
|
||||
(clientData.name === client.name &&
|
||||
clientData.phone.replace(/\D/g, "") ===
|
||||
client.phone.replace(/\D/g, "") &&
|
||||
clientData.email === client.email)
|
||||
}
|
||||
>
|
||||
{isPending ? (
|
||||
<span className="size-[1.111vw] animate-spin text-[#7B60F3] flex items-center justify-center">
|
||||
|
||||
@@ -11,8 +11,10 @@ import StartSessionIcon from "../icons/StartSessionIcon.tsx";
|
||||
import Button from "../Button.tsx";
|
||||
import ProjectSelector from "../ProjectSelector.tsx";
|
||||
import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useDebounce } from "@uidotdev/usehooks";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import useClientSearch from "../../hooks/useClientSearch.tsx";
|
||||
import ManagerSelect from "../ManagerSelect.tsx";
|
||||
import { Manager } from "../../types/Manager.ts";
|
||||
|
||||
interface Props {
|
||||
targetServerId: string | null;
|
||||
@@ -24,7 +26,8 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
const [name, setName] = useState<string | null>(client?.name || null);
|
||||
const [phone, setPhone] = useState<string | null>(client?.phone || null);
|
||||
const [email, setEmail] = useState<string | null>(client?.email || null);
|
||||
// const [isSessionExists, setIsSessionExists] = useState(false);
|
||||
const [isFullPhone, setIsFullPhone] = useState(false);
|
||||
const [isSessionExists, setIsSessionExists] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -34,6 +37,11 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const { data: managers } = useQuery({
|
||||
queryKey: ["managers"],
|
||||
queryFn: () => api.get("users").json<Manager[]>(),
|
||||
});
|
||||
|
||||
const targetServer = targetServerId
|
||||
? servers?.find((server) => server.id === targetServerId) || null
|
||||
: null;
|
||||
@@ -43,34 +51,20 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
);
|
||||
const [selectedApp, setSelectedApp] = useState<App | null>(null);
|
||||
|
||||
const { data, isLoading, error } = useClientSearch(phone);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedApp(
|
||||
selectedServer?.sessions?.[0]?.app ||
|
||||
selectedServer?.appsToServers?.[0].app ||
|
||||
selectedServer?.appsToServers?.[0]?.app ||
|
||||
null
|
||||
);
|
||||
}, [selectedServer]);
|
||||
|
||||
const debouncedPhone = useDebounce(phone, 500);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["get-user-by-phone", debouncedPhone],
|
||||
queryFn: () =>
|
||||
api
|
||||
.get("clients/by-phone", {
|
||||
searchParams: debouncedPhone ? { phone: debouncedPhone } : {},
|
||||
})
|
||||
.json<Client>(),
|
||||
enabled: !!debouncedPhone,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!error && data) {
|
||||
setName(data.name);
|
||||
setEmail(data.email);
|
||||
} else {
|
||||
setName(null);
|
||||
setEmail(null);
|
||||
}
|
||||
}, [data, error]);
|
||||
|
||||
@@ -80,7 +74,7 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
.post("clients", {
|
||||
json: {
|
||||
name,
|
||||
phone,
|
||||
phone: phone?.replace(/\D/g, ""),
|
||||
email,
|
||||
},
|
||||
})
|
||||
@@ -114,70 +108,74 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
},
|
||||
});
|
||||
|
||||
// const { mutate: endSession } = useMutation({
|
||||
// mutationKey: ["end-session", selectedServer?.sessions?.[0]?.id],
|
||||
// mutationFn: () =>
|
||||
// api.put(`sessions/${selectedServer?.sessions?.[0]?.id}`, {
|
||||
// json: { status: "ending" },
|
||||
// }),
|
||||
// onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
|
||||
// });
|
||||
const { mutate: endSession } = useMutation({
|
||||
mutationKey: ["end-session", selectedServer?.sessions?.[0]?.id],
|
||||
mutationFn: () =>
|
||||
api.put(`sessions/${selectedServer?.sessions?.[0]?.id}`, {
|
||||
json: { status: "ending" },
|
||||
}),
|
||||
onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
|
||||
});
|
||||
|
||||
async function handleClickCreateSession(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name || !phone || !selectedServer || !selectedApp) return;
|
||||
|
||||
// if (selectedServer?.sessions?.[0]?.status !== "started") {
|
||||
createClient(undefined, {
|
||||
onSuccess: (client) => {
|
||||
createSession({
|
||||
clientId: client.id,
|
||||
serverId: selectedServer.id,
|
||||
appId: selectedApp.id,
|
||||
});
|
||||
if (selectedServer?.sessions?.[0]?.status !== "started") {
|
||||
createClient(undefined, {
|
||||
onSuccess: (client) => {
|
||||
createSession({
|
||||
clientId: client.id,
|
||||
serverId: selectedServer.id,
|
||||
appId: selectedApp.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSessionExists) {
|
||||
setIsSessionExists(true);
|
||||
return;
|
||||
}
|
||||
|
||||
endSession(undefined, {
|
||||
onError: (error) => {
|
||||
console.log("Ошибка при завершении сессии:", error);
|
||||
},
|
||||
});
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!isSessionExists) {
|
||||
// setIsSessionExists(true);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// endSession(undefined, {
|
||||
// onError: (error) => {
|
||||
// console.log("Ошибка при завершении сессии:", error);
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// if (
|
||||
// selectedServer &&
|
||||
// servers?.find((server) => server.id === selectedServer?.id)?.sessions?.[0]
|
||||
// ?.status === "ended" &&
|
||||
// selectedApp
|
||||
// // && isSessionExists
|
||||
// )
|
||||
// createClient(undefined, {
|
||||
// onSuccess: (client) => {
|
||||
// createSession({
|
||||
// clientId: client.id,
|
||||
// serverId: selectedServer?.id,
|
||||
// appId: selectedApp.id,
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// }, [
|
||||
// selectedApp,
|
||||
// servers,
|
||||
// createClient,
|
||||
// createSession,
|
||||
// // isSessionExists,
|
||||
// selectedServer,
|
||||
// ]);
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedServer &&
|
||||
servers?.find((server) => server.id === selectedServer?.id)?.sessions?.[0]
|
||||
?.status === "ended" &&
|
||||
selectedApp &&
|
||||
isSessionExists
|
||||
)
|
||||
createClient(undefined, {
|
||||
onSuccess: (client) => {
|
||||
createSession({
|
||||
clientId: client.id,
|
||||
serverId: selectedServer?.id,
|
||||
appId: selectedApp.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [
|
||||
selectedApp,
|
||||
servers,
|
||||
createClient,
|
||||
createSession,
|
||||
isSessionExists,
|
||||
selectedServer,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(managers);
|
||||
}, [managers]);
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
@@ -202,13 +200,19 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
<div className="flex flex-col gap-y-[0.556vw]">
|
||||
<Input
|
||||
value={phone || ""}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setPhone(e.target.value);
|
||||
if (e.target.value.replace(/\D/g, "").length === 11) {
|
||||
setIsFullPhone(true);
|
||||
}
|
||||
}}
|
||||
placeholder="Номер телефона"
|
||||
mask="+7 (999) 999-99-99"
|
||||
required
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{phone && (
|
||||
{isFullPhone && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -243,23 +247,31 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-[0.833vw]">
|
||||
<p className="title-s font-medium">Выберите параметры сеанса</p>
|
||||
|
||||
{selectedServer &&
|
||||
selectedServer?.appsToServers &&
|
||||
selectedServer?.appsToServers?.length > 0 && (
|
||||
<ProjectSelector
|
||||
activeProject={
|
||||
selectedServer?.sessions?.[0]?.status === "started"
|
||||
? selectedApp
|
||||
: null
|
||||
}
|
||||
projects={selectedServer?.appsToServers.map(({ app }) => app)}
|
||||
selectedProject={selectedApp}
|
||||
setSelectedProject={setSelectedApp}
|
||||
/>
|
||||
selectedServer.appsToServers?.length > 0 && (
|
||||
<>
|
||||
<ManagerSelect
|
||||
placeholder="Менеджер сеанса"
|
||||
data={managers || []}
|
||||
/>
|
||||
<ProjectSelector
|
||||
activeProject={
|
||||
selectedServer?.sessions?.[0]?.status === "started"
|
||||
? selectedApp
|
||||
: null
|
||||
}
|
||||
projects={selectedServer?.appsToServers.map(({ app }) => app)}
|
||||
selectedProject={selectedApp}
|
||||
setSelectedProject={setSelectedApp}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* {isSessionExists && ( */}
|
||||
{/* <div className="absolute inset-0 top-[11.806vw] bg-[#FFFFFF] flex flex-col gap-[1.111vw] items-center justify-center h-[31.458vw]">
|
||||
|
||||
{isSessionExists && (
|
||||
<div className="absolute inset-0 top-[11.806vw] bg-[#FFFFFF] flex flex-col gap-[1.111vw] items-center justify-center h-[31.458vw]">
|
||||
<img
|
||||
src="/images/ghost.png"
|
||||
alt="ghost error"
|
||||
@@ -272,8 +284,8 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
При запуске нового текущий будет завершен.`}
|
||||
</p>
|
||||
</div>
|
||||
</div> */}
|
||||
{/* )} */}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex flex-col justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -288,7 +300,7 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||
variant="cta"
|
||||
size="large"
|
||||
>
|
||||
<div className="size-[1.111vw]">
|
||||
<div className="size-[1.111vw] text-[#9184F6]">
|
||||
<StartSessionIcon />
|
||||
</div>
|
||||
<span>Запустить сеанс</span>
|
||||
|
||||
@@ -6,14 +6,17 @@ import CurrentSessionModal from "./CurrentSessionModal";
|
||||
import api from "../../utils/api";
|
||||
import SpinIcon from "../icons/SpinIcon";
|
||||
import SessionModal from "./SessionModal";
|
||||
import { Server } from "../../types/Server";
|
||||
|
||||
function EndSessionModal({ session }: { session: Session }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { setModal, setPosition } = useModalStore();
|
||||
|
||||
const { mutate: endSession, isPending } = useMutation({
|
||||
const {
|
||||
mutate: endSession,
|
||||
isPending,
|
||||
isSuccess,
|
||||
} = useMutation({
|
||||
mutationKey: ["sessions", session.id],
|
||||
mutationFn: () =>
|
||||
api
|
||||
@@ -28,15 +31,6 @@ function EndSessionModal({ session }: { session: Session }) {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["servers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["last-sessions"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||
const servers = queryClient.getQueryData<Server[]>(["servers"]);
|
||||
const updatedSession = servers
|
||||
?.find((s) => s.id === session.serverId)
|
||||
?.sessions?.find((s) => s.id === session.id);
|
||||
if (updatedSession) {
|
||||
setPosition("right");
|
||||
setModal(<SessionModal session={updatedSession} />);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -62,31 +56,45 @@ function EndSessionModal({ session }: { session: Session }) {
|
||||
<div className="p-[1.389vw] flex flex-col gap-[1.667vw]">
|
||||
<div className="flex flex-col gap-[0.556vw]">
|
||||
<p className="title-m font-medium">
|
||||
Точно хотите завершить сеанс?
|
||||
{isSuccess
|
||||
? "Доступен отчет по встрече"
|
||||
: "Точно хотите завершить сеанс?"}
|
||||
</p>
|
||||
<p className="caption-m font-medium text-[#BDBDBD]">
|
||||
Текущий сеанс будет завершен немедленно
|
||||
{isSuccess
|
||||
? "Вы можете просмотреть отчет или создать новый сеанс, перейдя на главную страницу"
|
||||
: "Текущий сеанс будет завершен немедленно"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[0.556vw]">
|
||||
<Button
|
||||
size="large"
|
||||
variant="critical"
|
||||
variant={isSuccess ? "cta" : "critical"}
|
||||
className="w-full"
|
||||
onClick={() => endSession()}
|
||||
onClick={() => {
|
||||
if (isSuccess) {
|
||||
setModal(<SessionModal session={session} />);
|
||||
} else {
|
||||
endSession();
|
||||
}
|
||||
}}
|
||||
disabled={isPending}
|
||||
>
|
||||
Завершить сеанс
|
||||
{isSuccess ? "Перейти к отчету" : "Завершить сеанс"}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() =>
|
||||
setModal(<CurrentSessionModal session={session} />)
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSuccess) {
|
||||
setModal(null);
|
||||
} else {
|
||||
setModal(<CurrentSessionModal session={session} />);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Отменить
|
||||
{isSuccess ? "На главную" : "Отменить"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useDebounce } from "@uidotdev/usehooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Client } from "../types/Client";
|
||||
import api from "../utils/api";
|
||||
|
||||
function useClientSearch(phone: string | null) {
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const isPhoneComplete = Boolean(
|
||||
phone && phone.replace(/\D/g, "").length === 11
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSearching(isPhoneComplete);
|
||||
}, [isPhoneComplete]);
|
||||
|
||||
const debouncedPhone = useDebounce(phone, 500);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["get-user-by-phone", debouncedPhone],
|
||||
queryFn: () =>
|
||||
api
|
||||
.get("clients/by-phone", {
|
||||
searchParams:
|
||||
debouncedPhone && debouncedPhone.replace(/\D/g, "").length === 11
|
||||
? { phone: debouncedPhone.replace(/\D/g, "") }
|
||||
: {},
|
||||
})
|
||||
.json<Client>(),
|
||||
enabled: Boolean(debouncedPhone && isPhoneComplete),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && isSearching) {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, [isLoading, isSearching]);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: isSearching || isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export default useClientSearch;
|
||||
@@ -60,6 +60,7 @@ function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-[1.667vw]">
|
||||
<h1 className="title-l font-medium">Последние сеансы</h1>
|
||||
|
||||
@@ -71,7 +71,7 @@ function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button type="button" variant="secondary">
|
||||
<Button type="submit" variant="secondary">
|
||||
Забыли пароль?
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -122,14 +122,19 @@ function SessionsPage() {
|
||||
<p className="caption-m font-medium opacity-40">
|
||||
Найдено {count ? pluralize(count, "сеанс") : "0 сеансов"}
|
||||
</p>
|
||||
<button className="flex gap-[0.278vw] items-center" onClick={reset}>
|
||||
<div className="size-[1.111vw] text-[#7D7D7D]">
|
||||
<CloseIcon />
|
||||
</div>
|
||||
<p className="text-[#7D7D7D] text-[0.972vw] font-medium">
|
||||
Сбросить все
|
||||
</p>
|
||||
</button>
|
||||
{managerIds.length === 0 && appIds.length === 0 ? null : (
|
||||
<button
|
||||
className="flex gap-[0.278vw] items-center"
|
||||
onClick={reset}
|
||||
>
|
||||
<div className="size-[1.111vw] text-[#7D7D7D]">
|
||||
<CloseIcon />
|
||||
</div>
|
||||
<p className="text-[#7D7D7D] text-[0.972vw] font-medium">
|
||||
Сбросить все
|
||||
</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +146,7 @@ function SessionsPage() {
|
||||
) : grouppedSessions?.length ? (
|
||||
grouppedSessions?.map(([timestamp, sessions]) => (
|
||||
<div key={timestamp} className="space-y-[0.833vw]">
|
||||
<p className="caption-m font-medium opacity-40">
|
||||
<p className="caption-s font-medium opacity-40">
|
||||
{isToday(new Date(timestamp))
|
||||
? "Сегодня"
|
||||
: format(new Date(timestamp), "d MMMM", { locale: ru })}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function getPositionAbove(selectRef: React.RefObject<HTMLDivElement>) {
|
||||
const rect = selectRef.current.getBoundingClientRect();
|
||||
const dropdownHeight = 200;
|
||||
const margin = 8;
|
||||
|
||||
const spaceBelow = window.innerHeight - rect.bottom - margin;
|
||||
|
||||
const spaceAbove = rect.top - margin;
|
||||
|
||||
return spaceBelow < dropdownHeight && spaceAbove >= dropdownHeight;
|
||||
}
|
||||
Reference in New Issue
Block a user