feat: add ClientCard and SessionModal components, enhance session handling with comments and duration utilities
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,28 @@
|
||||
import { IUser } from "../types/IUser";
|
||||
import ChevronRightIcon from "./icons/ChevronRightIcon";
|
||||
import NewButton from "./NewButton";
|
||||
|
||||
function ClientCard({ client }: { client: IUser }) {
|
||||
return (
|
||||
<>
|
||||
<NewButton variant="secondary" className="w-full">
|
||||
<div className="flex flex-col gap-[0.278vw] w-full text-left h-[2.222vw]">
|
||||
<p className="caption-s font-medium text-[#BDBDBD]">Клиент</p>
|
||||
<p className="text-s font-medium">{client.name}</p>
|
||||
</div>
|
||||
<div className="flex gap-[0.556vw] items-center">
|
||||
{!client.email && (
|
||||
<p className="caption-s font-medium text-[#7B60F3] whitespace-nowrap">
|
||||
Добавьте email
|
||||
</p>
|
||||
)}
|
||||
<span className="w-[1.389vw] h-[1.389vw] flex items-center justify-center text-[#7B60F3]">
|
||||
<ChevronRightIcon />
|
||||
</span>
|
||||
</div>
|
||||
</NewButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClientCard;
|
||||
@@ -28,6 +28,9 @@ function CurrentSessionCard({
|
||||
queryClient.invalidateQueries({ queryKey: ["last-started"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["servers"] });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["last-sessions"] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function DesktopCard({ server }: IDesktopCardProps) {
|
||||
</NewButton>
|
||||
</div>
|
||||
) : server.status === "offline" ? (
|
||||
<NewButton variant="critical">
|
||||
<NewButton variant="critical" className="hover:bg-[#FEF3F2]">
|
||||
<span className="text-[#FF4517] size-[0.972vw]">
|
||||
<UnlinkIcon />
|
||||
</span>
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import useModalStore from "../stores/useModalStore";
|
||||
import { ISession } from "../types/ISession";
|
||||
import SessionModal from "./modals/SessionModal";
|
||||
|
||||
function SessionCard({ session }: { session: ISession }) {
|
||||
const { setModal, setPosition } = useModalStore();
|
||||
return (
|
||||
<div className="w-full h-[4.444vw] border-1 border-l-0 border-r-0 border-t-0 border-b-[#F6F6F6] flex py-[0.278vw] items-center gap-[0.556vw] cursor-pointer group">
|
||||
<div
|
||||
className="w-full h-[4.444vw] border-1 border-l-0 border-r-0 border-t-0 border-b-[#F6F6F6] flex py-[0.278vw] items-center gap-[0.556vw] cursor-pointer group"
|
||||
onClick={() => {
|
||||
setModal(<SessionModal session={session} />);
|
||||
setPosition("right");
|
||||
}}
|
||||
>
|
||||
<div className="rounded-xl w-full h-full flex items-center gap-[0.556vw] group-hover:bg-[#F6F6F6] transition-colors duration-200">
|
||||
<div className="size-[2.5vw] bg-[#F6F6F6] rounded-full"></div>
|
||||
<div className="flex flex-col w-full gap-[0.278vw]">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
function DownloadIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 17.5h15M13.75 10 10 13.75 6.25 10m3.746-7.5v10.833"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadIcon;
|
||||
@@ -0,0 +1,12 @@
|
||||
function MagicIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M12.434 8.242a.83.83 0 0 1-.158.508.83.83 0 0 1-.434.317l-1.425.475a3 3 0 0 0-1.166.716 2.9 2.9 0 0 0-.717 1.167l-.5 1.417a.83.83 0 0 1-.3.425.9.9 0 0 1-.517.158.834.834 0 0 1-.833-.592l-.475-1.425a2.9 2.9 0 0 0-.717-1.166 3.2 3.2 0 0 0-1.166-.725L2.6 9.05a.9.9 0 0 1-.425-.308A.9.9 0 0 1 2 8.225a.83.83 0 0 1 .591-.833l1.434-.475a2.95 2.95 0 0 0 1.9-1.9L6.4 3.608A.83.83 0 0 1 7.192 3c.18 0 .355.05.509.142a.9.9 0 0 1 .333.425l.483 1.45a2.95 2.95 0 0 0 1.9 1.9l1.417.5a.83.83 0 0 1 .425.316.83.83 0 0 1 .175.509M16 14.51a.53.53 0 0 1-.09.29.53.53 0 0 1-.248.184l-.664.221a1.3 1.3 0 0 0-.496.306c-.138.14-.243.309-.306.494l-.227.653a.47.47 0 0 1-.185.247.53.53 0 0 1-.295.095.53.53 0 0 1-.301-.1.53.53 0 0 1-.18-.247l-.216-.658a1.3 1.3 0 0 0-.306-.49 1.16 1.16 0 0 0-.49-.305l-.66-.221a.5.5 0 0 1-.253-.184.525.525 0 0 1 .253-.774l.66-.216a1.32 1.32 0 0 0 .807-.805l.216-.647a.53.53 0 0 1 .169-.248.53.53 0 0 1 .29-.105.53.53 0 0 1 .3.084c.089.06.157.144.196.242l.222.674a1.31 1.31 0 0 0 .807.805l.66.227a.5.5 0 0 1 .242.184.5.5 0 0 1 .095.295M14.907 3.784A.55.55 0 0 0 15 3.505c0-.1-.033-.197-.093-.278a.55.55 0 0 0-.223-.163l-.344-.115a.6.6 0 0 1-.175-.109.5.5 0 0 1-.109-.18l-.114-.35a.55.55 0 0 0-.175-.223.5.5 0 0 0-.289-.087.55.55 0 0 0-.267.104.55.55 0 0 0-.153.218l-.114.338a.5.5 0 0 1-.11.18.5.5 0 0 1-.18.11l-.332.108a.5.5 0 0 0-.23.164A.5.5 0 0 0 12 3.5c0 .1.03.196.087.278.06.076.14.134.23.17l.338.108a.4.4 0 0 1 .174.11q.079.072.11.174l.114.338c.032.096.093.18.174.24a.6.6 0 0 0 .268.082.45.45 0 0 0 .283-.098.5.5 0 0 0 .158-.213l.12-.344a.46.46 0 0 1 .284-.283l.338-.115a.45.45 0 0 0 .23-.163"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MagicIcon;
|
||||
@@ -0,0 +1,15 @@
|
||||
function ShareIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13 8.19231H14.5C14.8978 8.19231 15.2794 8.34426 15.5607 8.61475C15.842 8.88523 16 9.25209 16 9.63462V16.5577C16 16.9402 15.842 17.3071 15.5607 17.5776C15.2794 17.848 14.8978 18 14.5 18H5.5C5.10218 18 4.72064 17.848 4.43934 17.5776C4.15804 17.3071 4 16.9402 4 16.5577V9.63462C4 9.25209 4.15804 8.88523 4.43934 8.61475C4.72064 8.34426 5.10218 8.19231 5.5 8.19231H7M13 5.88462L10 3M10 3L7 5.88462M10 3V12.8437"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShareIcon;
|
||||
@@ -144,7 +144,7 @@ export default function CreateSessionModal({ targetServerId }: Props) {
|
||||
|
||||
return (
|
||||
<form
|
||||
className="relative rounded-[2.222vw] w-[25vw] min-h-[calc(100dvh-2.222vw)] bg-[#F0F0F0] flex flex-col overflow-hidden"
|
||||
className="relative rounded-[2.222vw] w-[25vw] min-h-[calc(100dvh-2.222vw)] bg-[#F0F0F0] flex flex-col overflow-hidden "
|
||||
onSubmit={handleClickCreateSession}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,15 @@ function CurrentSessionModal({ session }: { session: ISession }) {
|
||||
api.put(`sessions/${session.id}`, {
|
||||
json: { status: "ending" },
|
||||
}),
|
||||
onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
|
||||
onMutate: () =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["sessions"],
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["last-sessions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const [now, setNow] = useState(Date.now());
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { ISession } from "../../types/ISession";
|
||||
import { format } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
import getIntervalDuration from "../../utils/interval-duration";
|
||||
import MagicIcon from "../icons/MagicIcon";
|
||||
import NewButton from "../NewButton";
|
||||
import ChevronRightIcon from "../icons/ChevronRightIcon";
|
||||
import Badge from "../Badge";
|
||||
import ClientCard from "../ClientCard";
|
||||
import DownloadIcon from "../icons/DownloadIcon";
|
||||
import ShareIcon from "../icons/ShareIcon";
|
||||
|
||||
function SessionModal({ session }: { session: ISession }) {
|
||||
console.log(session);
|
||||
return (
|
||||
<div className="bg-[#FFFFFF] w-[49.722vw] rounded-4xl">
|
||||
<div className="w-full flex justify-center items-center h-[4.861vw]">
|
||||
<div className="title-s font-medium flex">
|
||||
<p>{format(session.createdAt, "dd MMMM yyyy", { locale: ru })}</p>
|
||||
<p>, {format(session.createdAt, "HH:mm")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#F0F0F0] flex h-[calc(100vh-8.861vw)] overflow-hidden rounded-b-4xl">
|
||||
<div className="flex-1 flex flex-col gap-[0.833vw] px-[1.111vw] overflow-y-auto pr-[0.556vw] pb-[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="flex flex-col gap-[0.278vw] items-center">
|
||||
<p className="title-s font-medium">{session.owner.fullname}</p>
|
||||
<p className="caption-s text-[#BDBDBD] font-medium">
|
||||
Продолжительность:{" "}
|
||||
{getIntervalDuration(session.createdAt, session.updatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1.111vw] bg-white rounded-3xl p-[1.111vw]">
|
||||
<h3 className="title-s font-medium">Информация о сеансе</h3>
|
||||
<div className="flex flex-col gap-[0.556vw]">
|
||||
<p className="flex gap-[0.556vw]">
|
||||
<span className="caption-s font-medium text-[#BDBDBD]">
|
||||
Сценарии:
|
||||
</span>
|
||||
<span className="caption-s font-medium">Добавить в бэкенд</span>
|
||||
</p>
|
||||
<p className="flex gap-[0.556vw]">
|
||||
<span className="caption-s font-medium text-[#BDBDBD]">
|
||||
Интерактивный стол:
|
||||
</span>
|
||||
<span className="caption-s font-medium">
|
||||
{session.server.name}
|
||||
</span>
|
||||
</p>
|
||||
<p className="flex gap-[0.556vw]">
|
||||
<span className="caption-s font-medium text-[#BDBDBD]">
|
||||
Продолжительность сеанса:
|
||||
</span>
|
||||
<span className="caption-s font-medium">
|
||||
{getIntervalDuration(session.createdAt, session.updatedAt)}
|
||||
</span>
|
||||
</p>
|
||||
<p className="flex gap-[0.556vw]">
|
||||
<span className="caption-s font-medium text-[#BDBDBD]">
|
||||
Проект:
|
||||
</span>
|
||||
<span className="caption-s font-medium">
|
||||
{session.app.name}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<ClientCard client={session.client} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1.111vw] bg-white rounded-3xl p-[1.111vw]">
|
||||
<h3 className="title-s font-medium flex">
|
||||
Речевая
|
||||
<span className="text-[#7B60F3] flex">
|
||||
аналитика
|
||||
<span className="w-[1.389vw] h-[1.389vw] text-[#7B60F3]">
|
||||
<MagicIcon />
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
<div className="flex flex-col gap-[0.556vw]">
|
||||
<p className="flex gap-[0.556vw]">
|
||||
<span className="caption-s font-medium text-[#BDBDBD]">
|
||||
Эффективность встречи:
|
||||
</span>
|
||||
<span className="caption-s font-medium text-[#29AF61]">
|
||||
Высокая
|
||||
</span>
|
||||
</p>
|
||||
<p className="flex gap-[0.556vw]">
|
||||
<span className="caption-s font-medium text-[#BDBDBD]">
|
||||
Бюджет клиента:
|
||||
</span>
|
||||
<span className="caption-s font-medium">8 500 000 ₽</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[#F6F6F6] rounded-xl px-[1.111vw] py-[0.833vw] text-xs tracking-[-0.02em]">
|
||||
Клиент проявил высокий интерес к объекту, особенно к варианту с
|
||||
улучшенной отделкой. Основной вопрос для принятия решения —
|
||||
согласование с семьей и выбор этажа. Необходимо подготовить
|
||||
предварительный договор к следующей встрече.
|
||||
</div>
|
||||
<NewButton variant="primary" size="large">
|
||||
Весь отчёт по встречи <ChevronRightIcon />
|
||||
</NewButton>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[1.111vw] bg-white rounded-3xl p-[1.111vw]">
|
||||
<h3 className="title-s font-medium flex">
|
||||
Документы по сеансу <Badge count={4} />
|
||||
</h3>
|
||||
<div className="flex w-full gap-[0.556vw]">
|
||||
<NewButton variant="primary" size="large" className="w-full">
|
||||
<span className="w-[1.111vw] h-[1.111vw] text-[#7B60F3]">
|
||||
<DownloadIcon />
|
||||
</span>
|
||||
Скачать архивом
|
||||
</NewButton>
|
||||
<NewButton variant="primary" size="large">
|
||||
<span className="w-[1.111vw] h-[1.111vw] text-[#7B60F3]">
|
||||
<ShareIcon />
|
||||
</span>
|
||||
</NewButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="relative h-full">
|
||||
{session.comments.length > 0 ? (
|
||||
<div>Комменты</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-[1.111vw] items-center justify-center h-full">
|
||||
<img src="/images/smile-ghost.png" alt="ghost" />
|
||||
<div className="flex flex-col gap-[0.556vw] items-center">
|
||||
<h3 className="title-m font-medium">Оставьте заметку</h3>
|
||||
<p className="caption-s font-medium text-[#BDBDBD] text-center whitespace-pre-line">
|
||||
{`В дальнейшем это поможет быстро найти
|
||||
клиента и не запутаться.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[5.556vw] border-black border-t"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionModal;
|
||||
@@ -8,6 +8,7 @@ import { ISession } from "../types/ISession";
|
||||
import SessionCard from "../components/SessionCard";
|
||||
import NewButton from "../components/NewButton";
|
||||
import ChevronRightIcon from "../components/icons/ChevronRightIcon";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function DashboardPage() {
|
||||
const { data: me } = useQuery({
|
||||
@@ -23,7 +24,7 @@ function DashboardPage() {
|
||||
});
|
||||
|
||||
const { data: sessions } = useQuery({
|
||||
queryKey: ["sessions", { limit: 5 }],
|
||||
queryKey: ["last-sessions"],
|
||||
queryFn: () =>
|
||||
api.get("sessions", { searchParams: { limit: 5 } }).json<ISession[]>(),
|
||||
enabled: !!me,
|
||||
@@ -42,6 +43,10 @@ function DashboardPage() {
|
||||
// }
|
||||
// }
|
||||
|
||||
useEffect(() => {
|
||||
console.log(sessions);
|
||||
}, [sessions]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[5vw] min-h-dvh">
|
||||
<div className="w-full flex justify-between">
|
||||
@@ -63,7 +68,10 @@ function DashboardPage() {
|
||||
<div className="w-full flex flex-col gap-[0.833vw]">
|
||||
<div className="flex flex-col gap-[0.278vw]">
|
||||
{sessions
|
||||
?.filter((session) => session.status === "ended")
|
||||
?.filter(
|
||||
(session) =>
|
||||
session.status === "ended" || session.status === "ending"
|
||||
)
|
||||
.map((session) => (
|
||||
<SessionCard key={session.id} session={session} />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface IComment {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ownerId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IApp } from "./IApp";
|
||||
import { IComment } from "./IComments";
|
||||
import { IOwner } from "./IOwner";
|
||||
import { IServer } from "./IServer";
|
||||
import { IUser } from "./IUser";
|
||||
@@ -9,10 +10,12 @@ export interface ISession {
|
||||
serverId: string;
|
||||
clientId: string;
|
||||
companyId: string;
|
||||
comments: IComment[];
|
||||
status: "starting" | "started" | "restarted" | "ending" | "ended";
|
||||
server: IServer;
|
||||
client: IUser;
|
||||
app: IApp;
|
||||
owner: IOwner;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { intervalToDuration } from "date-fns";
|
||||
|
||||
function getIntervalDuration(start: Date, end: Date) {
|
||||
const duration = intervalToDuration({
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
const hours = (duration.hours || 0).toString().padStart(2, "0");
|
||||
const minutes = (duration.minutes || 0).toString().padStart(2, "0");
|
||||
const seconds = (duration.seconds || 0).toString().padStart(2, "0");
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export default getIntervalDuration;
|
||||
Reference in New Issue
Block a user