feat: add ClientModal and CreateSessionModal components, enhance Input with children prop, and include mock manager photos

This commit is contained in:
2025-06-19 17:27:23 +05:00
parent 8bdf63eb57
commit e635b7ef0a
10 changed files with 254 additions and 12 deletions
+200
View File
@@ -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;
+5 -4
View File
@@ -16,13 +16,14 @@ import { AnimatePresence, motion } from "motion/react";
interface Props {
targetServerId: string | null;
client?: Client | null;
}
export default function CreateSessionModal({ targetServerId }: Props) {
export default function CreateSessionModal({ targetServerId, client }: Props) {
const { setModal } = useModalStore();
const [name, setName] = useState<string | null>(null);
const [phone, setPhone] = useState<string | null>(null);
const [email, setEmail] = useState<string | null>(null);
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 queryClient = useQueryClient();
+1 -1
View File
@@ -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="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="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">
<p className="title-s font-medium">{session.manager.fullname}</p>
<p className="caption-s text-[#BDBDBD] font-medium">