feat: add ClientModal and CreateSessionModal components, enhance Input with children prop, and include mock manager photos
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 794 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
@@ -6,6 +6,7 @@ interface NewInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Input({
|
function Input({
|
||||||
@@ -13,6 +14,7 @@ function Input({
|
|||||||
isError,
|
isError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: NewInputProps) {
|
}: NewInputProps) {
|
||||||
return (
|
return (
|
||||||
@@ -27,6 +29,7 @@ function Input({
|
|||||||
"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"
|
"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"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
{placeholder && (
|
{placeholder && (
|
||||||
<span
|
<span
|
||||||
className="absolute caption-m font-medium text-[#7D7D7D] left-[1.111vw] top-1/2 -translate-y-1/2 pointer-events-none transition-all duration-300
|
className="absolute caption-m font-medium text-[#7D7D7D] left-[1.111vw] top-1/2 -translate-y-1/2 pointer-events-none transition-all duration-300
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function SessionCard({ session }: { session: Session }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="rounded-[1.111vw] w-full h-full flex items-center gap-[0.556vw] group-hover:bg-[#F6F6F6] transition-colors duration-200 px-[1.111vw] py-[0.972vw]">
|
<div className="rounded-[1.111vw] w-full h-full flex items-center gap-[0.556vw] group-hover:bg-[#F6F6F6] transition-colors duration-200 px-[1.111vw] py-[0.972vw]">
|
||||||
<div className="size-[2.5vw] bg-[#F6F6F6] rounded-full"></div>
|
<div className="size-[2.5vw] bg-[#F6F6F6] rounded-full bg-[url(/images/mock_manager_photo_1_c.png)] bg-cover bg-no-repeat bg-center"></div>
|
||||||
<div className="flex flex-col w-full gap-[0.278vw]">
|
<div className="flex flex-col w-full gap-[0.278vw]">
|
||||||
<p className="button-m font-medium">{session.manager.fullname}</p>
|
<p className="button-m font-medium">{session.manager.fullname}</p>
|
||||||
<p className="caption-s font-medium text-[#7D7D7D]">
|
<p className="caption-s font-medium text-[#7D7D7D]">
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
function CopyIcon() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M6 5.725V4.03C6 3.461 6.462 3 7.031 3h8.938C16.539 3 17 3.462 17 4.031v8.938A1.03 1.03 0 0 1 15.969 14h-1.713"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.969 6H4.03A1.03 1.03 0 0 0 3 7.031v8.938C3 16.539 3.462 17 4.031 17h8.938A1.03 1.03 0 0 0 14 15.969V7.03A1.03 1.03 0 0 0 12.969 6Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.2}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyIcon;
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { format, isToday } from "date-fns";
|
||||||
|
import { Client } from "../../types/Client";
|
||||||
|
import Badge from "../Badge";
|
||||||
|
import Button from "../Button";
|
||||||
|
import PeopleIcon from "../icons/PeopleIcon";
|
||||||
|
import PlusIcon from "../icons/PlusIcon";
|
||||||
|
import Input from "../Input";
|
||||||
|
import { ru } from "date-fns/locale";
|
||||||
|
import ChevronRightIcon from "../icons/ChevronRightIcon";
|
||||||
|
import CopyIcon from "../icons/CopyIcon";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import api from "../../utils/api";
|
||||||
|
import SpinIcon from "../icons/SpinIcon";
|
||||||
|
import useModalStore from "../../stores/useModalStore";
|
||||||
|
import CreateSessionModal from "./CreateSessionModal";
|
||||||
|
|
||||||
|
function ClientModal({ client }: { client: Client }) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { setModal } = useModalStore();
|
||||||
|
const [clientData, setClientData] = useState({
|
||||||
|
name: client.name,
|
||||||
|
phone: client.phone,
|
||||||
|
email: client.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: updateClientData, isPending } = useMutation({
|
||||||
|
mutationKey: ["clients", client.id],
|
||||||
|
mutationFn: () => api.put(`clients/${client.id}`, { json: clientData }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["clients"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[49.722vw] bg-[#FFFFFF] rounded-[2.222vw] overflow-hidden">
|
||||||
|
<div className="flex justify-center items-center py-[1.806vw] border-b border-[#D6D6D6]">
|
||||||
|
<p className="title-s font-medium">{client.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex bg-[#F0F0F0] h-[calc(100vh-8.861vw)] rounded-b-[2.222vw]">
|
||||||
|
<div className="flex flex-col gap-[1.111vw] p-[1.111vw] flex-1 overflow-y-auto [scrollbar-width:thin]">
|
||||||
|
<div className="flex flex-col gap-[1.111vw] rounded-[1.667vw] bg-white p-[1.111vw]">
|
||||||
|
<div className="flex flex-col gap-[0.278vw]">
|
||||||
|
<p className="title-s font-medium">Данные клиента</p>
|
||||||
|
<p className="caption-s text-[#BDBDBD] font-medium">
|
||||||
|
Вы можете изменить данные клиента
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-[0.278vw]">
|
||||||
|
<Input
|
||||||
|
placeholder="Имя"
|
||||||
|
defaultValue={clientData.name || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setClientData({ ...clientData, name: e.target.value });
|
||||||
|
}}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="absolute z-10 top-[1.25vw] left-[17.917vw] size-[1.389vw] text-[#7D7D7D] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(clientData.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</span>
|
||||||
|
</Input>
|
||||||
|
<Input
|
||||||
|
placeholder="Номер телефона"
|
||||||
|
defaultValue={clientData.phone || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setClientData({ ...clientData, phone: e.target.value });
|
||||||
|
}}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="absolute top-[1.25vw] left-[17.917vw] size-[1.389vw] text-[#7D7D7D] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(clientData.phone);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</span>
|
||||||
|
</Input>
|
||||||
|
<Input
|
||||||
|
placeholder="Эл. почта"
|
||||||
|
defaultValue={clientData.email || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setClientData({ ...clientData, email: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="large"
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => updateClientData()}
|
||||||
|
disabled={
|
||||||
|
clientData.name === client.name &&
|
||||||
|
clientData.phone === client.phone &&
|
||||||
|
clientData.email === client.email &&
|
||||||
|
!isPending
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<span className="size-[1.111vw] animate-spin text-[#7B60F3] flex items-center justify-center">
|
||||||
|
<SpinIcon />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"Сохранить изменения"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-[1.111vw] rounded-[1.667vw] bg-white p-[1.111vw]">
|
||||||
|
<div className="flex flex-col gap-[0.278vw]">
|
||||||
|
<p className="title-s font-medium">Управление доступом</p>
|
||||||
|
<p className="caption-s text-[#BDBDBD] font-medium">
|
||||||
|
Выберите, кто может видеть и редактировать данные этого клиента
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[0.556vw]">
|
||||||
|
<div className="size-[2.222vw] rounded-full bg-[#F0F0F0] bg-[url(/images/mock_manager_photo_c.png)] bg-cover bg-no-repeat bg-center" />
|
||||||
|
<div className="size-[2.222vw] rounded-full bg-[#F0F0F0] bg-[url(/images/mock_manager_photo_1_c.png)] bg-cover bg-no-repeat bg-center" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="button-m text-[#7B60F3] font-medium flex items-center gap-[0.278vw]">
|
||||||
|
<span className="size-[0.972vw]">
|
||||||
|
<PeopleIcon />
|
||||||
|
</span>
|
||||||
|
Настроить доступ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-[1.111vw] rounded-[1.667vw] bg-white p-[1.111vw]">
|
||||||
|
<div className="flex items-center gap-[0.556vw]">
|
||||||
|
<p className="title-s font-medium">История сеансов</p>
|
||||||
|
{client.sessions.length > 0 && (
|
||||||
|
<Badge count={client.sessions.length} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-[0.556vw]">
|
||||||
|
{client.sessions.length === 0 && (
|
||||||
|
<p className="caption-s text-[#BDBDBD] font-medium text-center">
|
||||||
|
Пока не было сеансов
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{client.sessions.map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
className="p-[0.278vw] border-b border-[#F6F6F6] cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="p-[0.833vw] flex justify-between items-center">
|
||||||
|
<div className="flex gap-[0.556vw] items-center">
|
||||||
|
<div className="size-[2.5vw] rounded-full bg-[#F0F0F0] bg-[url(/images/mock_manager_photo_c.png)] bg-cover bg-no-repeat bg-center" />
|
||||||
|
<div className="flex flex-col gap-[0.278vw]">
|
||||||
|
<p className="button-m font-medium">
|
||||||
|
{session.manager.fullname}
|
||||||
|
</p>
|
||||||
|
<p className="caption-s text-[#BDBDBD] font-medium">
|
||||||
|
{isToday(new Date(session.updatedAt))
|
||||||
|
? "Сегодня"
|
||||||
|
: format(new Date(session.updatedAt), "d MMMM", {
|
||||||
|
locale: ru,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="size-[1.389vw] text-[#7D7D7D]">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="cta"
|
||||||
|
size="large"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() =>
|
||||||
|
setModal(
|
||||||
|
<CreateSessionModal targetServerId={null} client={client} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="size-[1.111vw]">
|
||||||
|
<PlusIcon />
|
||||||
|
</span>
|
||||||
|
Новый сеанс с клиентом
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClientModal;
|
||||||
@@ -16,13 +16,14 @@ import { AnimatePresence, motion } from "motion/react";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
targetServerId: string | null;
|
targetServerId: string | null;
|
||||||
|
client?: Client | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateSessionModal({ targetServerId }: Props) {
|
export default function CreateSessionModal({ targetServerId, client }: Props) {
|
||||||
const { setModal } = useModalStore();
|
const { setModal } = useModalStore();
|
||||||
const [name, setName] = useState<string | null>(null);
|
const [name, setName] = useState<string | null>(client?.name || null);
|
||||||
const [phone, setPhone] = useState<string | null>(null);
|
const [phone, setPhone] = useState<string | null>(client?.phone || null);
|
||||||
const [email, setEmail] = useState<string | null>(null);
|
const [email, setEmail] = useState<string | null>(client?.email || null);
|
||||||
// const [isSessionExists, setIsSessionExists] = useState(false);
|
// const [isSessionExists, setIsSessionExists] = useState(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function SessionModal({ session }: { session: Session }) {
|
|||||||
<div className="bg-[#F0F0F0] flex h-[calc(100vh-8.861vw)] overflow-hidden rounded-b-[2.222vw]">
|
<div className="bg-[#F0F0F0] flex h-[calc(100vh-8.861vw)] overflow-hidden rounded-b-[2.222vw]">
|
||||||
<div className="flex-1 flex flex-col gap-[0.833vw] px-[1.111vw] overflow-y-auto pr-[0.556vw] pb-[1.111vw] [scrollbar-width:thin]">
|
<div className="flex-1 flex flex-col gap-[0.833vw] px-[1.111vw] overflow-y-auto pr-[0.556vw] pb-[1.111vw] [scrollbar-width:thin]">
|
||||||
<div className="flex flex-col gap-[0.556vw] justify-center items-center pt-[1.111vw]">
|
<div className="flex flex-col gap-[0.556vw] justify-center items-center pt-[1.111vw]">
|
||||||
<div className="size-[3.333vw] rounded-full bg-white"></div>
|
<div className="size-[3.333vw] rounded-full bg-white bg-[url(/images/mock_manager_photo_1_c.png)] bg-cover bg-no-repeat bg-center"></div>
|
||||||
<div className="flex flex-col gap-[0.278vw] items-center">
|
<div className="flex flex-col gap-[0.278vw] items-center">
|
||||||
<p className="title-s font-medium">{session.manager.fullname}</p>
|
<p className="title-s font-medium">{session.manager.fullname}</p>
|
||||||
<p className="caption-s text-[#BDBDBD] font-medium">
|
<p className="caption-s text-[#BDBDBD] font-medium">
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ import { Client } from "../types/Client";
|
|||||||
import { useDebounce } from "@uidotdev/usehooks";
|
import { useDebounce } from "@uidotdev/usehooks";
|
||||||
import pluralize from "../utils/pluralize";
|
import pluralize from "../utils/pluralize";
|
||||||
import ChevronRightIcon from "../components/icons/ChevronRightIcon";
|
import ChevronRightIcon from "../components/icons/ChevronRightIcon";
|
||||||
|
import useModalStore from "../stores/useModalStore";
|
||||||
|
import ClientModal from "../components/modals/ClientModal";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
function ClientsPage() {
|
function ClientsPage() {
|
||||||
const [limit, setLimit] = useState(10);
|
const [limit, setLimit] = useState(10);
|
||||||
const [search, setSearch] = useState<string | null>(null);
|
const [search, setSearch] = useState<string | null>(null);
|
||||||
|
const { setModal, setPosition } = useModalStore();
|
||||||
|
|
||||||
const debouncedSearch = useDebounce(search, 500);
|
const debouncedSearch = useDebounce(search, 500);
|
||||||
|
|
||||||
@@ -102,17 +106,27 @@ function ClientsPage() {
|
|||||||
<SpinIcon />
|
<SpinIcon />
|
||||||
</div>
|
</div>
|
||||||
) : clients?.length ? (
|
) : clients?.length ? (
|
||||||
clients?.map(({ name, email, phone }) => (
|
clients?.map((client, index) => (
|
||||||
<div className="p-[0.278vw] border-b border-[#F6F6F6]">
|
<div
|
||||||
|
key={client.id}
|
||||||
|
className={clsx(
|
||||||
|
"p-[0.278vw]",
|
||||||
|
clients.length - 1 !== index && "border-b border-[#F6F6F6]"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setPosition("right");
|
||||||
|
setModal(<ClientModal client={client} />);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={client.id}
|
||||||
className="flex justify-between items-center cursor-pointer aspace-y-[0.833vw] p-[1.111vw] hover:bg-[#F6F6F6] rounded-[0.833vw]"
|
className="flex justify-between items-center cursor-pointer aspace-y-[0.833vw] p-[1.111vw] hover:bg-[#F6F6F6] rounded-[0.833vw]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-[0.556vw]">
|
<div className="flex flex-col gap-[0.556vw]">
|
||||||
<p className="button-m font-medium">{name}</p>
|
<p className="button-m font-medium">{client.name}</p>
|
||||||
<div className="flex gap-[0.278vw] caption-s font-medium text-[#7D7D7D]">
|
<div className="flex gap-[0.278vw] caption-s font-medium text-[#7D7D7D]">
|
||||||
<p>{phone}</p>
|
<p>{client.phone}</p>
|
||||||
<p>{email ? "• " + email : ""}</p>
|
<p>{client.email ? "• " + client.email : ""}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="size-[1.389vw] text-[#7D7D7D]">
|
<div className="size-[1.389vw] text-[#7D7D7D]">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Session } from "./Session";
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -6,4 +8,5 @@ export interface Client {
|
|||||||
companyId: string;
|
companyId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
sessions: Session[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user