This commit is contained in:
2025-07-07 11:12:02 +05:00
parent 2e5790d246
commit 1a8c25f092
11 changed files with 152 additions and 35 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+1 -1
View File
@@ -27,7 +27,7 @@ function Button({
onClick?.(e);
}}
className={clsx(
"transition-all flex outline-none 2xl:gap-[0.556vw] gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:text-[#D6D6D6]",
"transition-all flex outline-none 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" &&
+56 -3
View File
@@ -1,11 +1,64 @@
import { motion } from "motion/react";
import { Comment } from "../types/Comment";
import { format } from "date-fns";
import { format, isToday } from "date-fns";
import { Session } from "../types/Session";
import { ru } from "date-fns/locale";
import ChevronRightIcon from "./icons/ChevronRightIcon";
import useModalStore from "../stores/useModalStore";
import SessionModal from "./modals/SessionModal";
import { useQuery } from "@tanstack/react-query";
import api from "../utils/api";
function SessionCommentItem({
comment,
session,
}: {
comment: Comment;
session?: Session;
}) {
const { setModal } = useModalStore();
const { data: files } = useQuery({
queryKey: ["file-list", comment.sessionId],
enabled: !!session,
queryFn: () =>
api
.get("files", {
searchParams: { sessionId: comment.sessionId },
})
.json<{ filename: string; size: number }[]>(),
});
function SessionCommentItem({ comment }: { comment: Comment }) {
return (
<motion.div layout className="flex gap-[0.833vw] items-end">
<div className="relative flex flex-col gap-[0.556vw] p-[0.833vw] bg-white rounded-[0.833vw] w-full rounded-br-none">
{session && (
<div
className="relative bg-[#E1DEFC] rounded-[0.556vw] self-stretch p-[0.556vw] flex justify-between items-center cursor-pointer overflow-hidden"
onClick={() => setModal(<SessionModal session={session} />)}
>
<div className="h-full w-[0.139vw] bg-[#7B60F3] left-0 absolute" />
<div className="space-y-[0.278vw]">
<p className="text-[#7B60F3] caption-m font-medium">
Сеанс{" "}
{isToday(new Date(session.createdAt))
? "Сегодня"
: `от ${format(new Date(session.createdAt), "dd MMMM", {
locale: ru,
})}`}
</p>
{files && (
<p className="text-s">
{files?.length}{" "}
{files?.length === 1 ? "документ" : "документов"}
</p>
)}
</div>
<div className="size-[1.389vw] text-[#7B60F3]">
<ChevronRightIcon />
</div>
</div>
)}
<p className="button-m font-medium">{comment.manager.fullname}</p>
<div className="flex flex-col max-w-[19.583vw]">
<p className="caption-s break-words whitespace-pre-wrap overflow-hidden">
@@ -18,7 +71,7 @@ function SessionCommentItem({ comment }: { comment: Comment }) {
</div>
</div>
</div>
<div className="bg-white size-[2.222vw] rounded-full flex-shrink-0" />
<div className="bg-[url(/images/mock_manager_photo_c.png)] bg-cover bg-no-repeat bg-center size-[2.222vw] rounded-full flex-shrink-0" />
</motion.div>
);
}
+1 -1
View File
@@ -158,7 +158,7 @@ function SessionComments({ sessionId }: { sessionId: string }) {
onKeyDown={handleKeyDown}
/>
<Button variant="cta" size="large" type="submit" disabled={!value}>
<span className="size-[1.111vw] text-white">
<span className="size-[1.111vw]">
<SendIcon />
</span>
</Button>
+2 -2
View File
@@ -3,13 +3,13 @@ function SendIcon() {
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="m16 4-4.2 12-2.4-5.4L4 8.2z"
stroke="#fff"
stroke="currentColor"
strokeWidth={1.2}
strokeLinejoin="round"
/>
<path
d="m15 5-5.5 5.5"
stroke="#fff"
stroke="currentColor"
strokeWidth={1.2}
strokeLinecap="round"
strokeLinejoin="round"
+58 -10
View File
@@ -15,6 +15,7 @@ import SpinIcon from "../icons/SpinIcon";
import useModalStore from "../../stores/useModalStore";
import CreateSessionModal from "./CreateSessionModal";
import SessionModal from "./SessionModal";
import SessionCommentItem from "../SessionCommentItem";
function ClientModal({ client }: { client: Client }) {
const queryClient = useQueryClient();
@@ -41,7 +42,7 @@ function ClientModal({ client }: { client: Client }) {
<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 bg-[#F0F0F0] h-[calc(100vh-8.861vw)] rounded-b-[2.222vw] overflow-hidden">
<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]">
@@ -123,8 +124,12 @@ function ClientModal({ client }: { client: Client }) {
</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" />
{client.managers.map((manager) => (
<div
key={manager.id}
className="size-[2.222vw] rounded-full bg-[#F0F0F0] bg-[url(/images/mock_manager_photo_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]">
@@ -152,9 +157,7 @@ function ClientModal({ client }: { client: Client }) {
<div
key={session.id}
className="p-[0.278vw] border-b border-[#F6F6F6] cursor-pointer"
onClick={() => {
setModal(<SessionModal session={session} />);
}}
onClick={() => setModal(<SessionModal session={session} />)}
>
<div className="p-[0.833vw] flex justify-between items-center">
<div className="flex gap-[0.556vw] items-center">
@@ -166,9 +169,13 @@ function ClientModal({ client }: { client: Client }) {
<p className="caption-s text-[#BDBDBD] font-medium">
{isToday(new Date(session.updatedAt))
? "Сегодня"
: format(new Date(session.updatedAt), "d MMMM", {
locale: ru,
})}
: format(
new Date(session.updatedAt),
"dd.MM.yyyy",
{
locale: ru,
}
)}
</p>
</div>
</div>
@@ -196,7 +203,48 @@ function ClientModal({ client }: { client: Client }) {
</Button>
</div>
</div>
<div className="flex-1"></div>
<div className="flex-1 overflow-auto px-[1.111vw] py-[1.667vw] [scrollbar-width:thin] flex flex-col-reverse">
{client.sessions
.filter((session) => session.comments.length)
.map((session) => (
<div key={session.id} className="space-y-[1.111vw]">
<p className="text-center text-[#BDBDBD] caption-s font-medium">
{isToday(new Date(session.createdAt))
? "Сегодня"
: format(new Date(session.createdAt), "dd MMMM", {
locale: ru,
})}
</p>
<div className="space-y-[0.833vw]">
{session.comments.map((comment) => (
<SessionCommentItem
key={comment.id}
comment={comment}
session={session}
/>
))}
</div>
</div>
))}
{(client.sessions.length === 0 ||
client.sessions.filter((session) => session.comments.length)
.length === 0) && (
<div className="flex flex-col items-center gap-[1.111vw] w-[18.333vw] m-auto">
<div className="w-[13.889vw]">
<img src="/images/empty_ghost.png" alt="" />
</div>
<div className="space-y-[0.556vw]">
<p className="text-center title-m font-medium">
Пока что пусто
</p>
<p className="caption-s text-[#BDBDBD] font-medium text-center">
Здесь отображаются все комментарии по сеансам с текущим
клиентом
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
+4 -4
View File
@@ -46,7 +46,7 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
useEffect(() => {
setSelectedApp(
selectedServer?.sessions?.[0]?.app ||
selectedServer?.apps?.[0].app ||
selectedServer?.appsToServers?.[0].app ||
null
);
}, [selectedServer]);
@@ -244,15 +244,15 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
<div className="flex flex-col gap-y-[0.833vw]">
<p className="title-s font-medium">Выберите параметры сеанса</p>
{selectedServer &&
selectedServer?.apps &&
selectedServer?.apps?.length > 0 && (
selectedServer?.appsToServers &&
selectedServer?.appsToServers?.length > 0 && (
<ProjectSelector
activeProject={
selectedServer?.sessions?.[0]?.status === "started"
? selectedApp
: null
}
projects={selectedServer?.apps.map(({ app }) => app)}
projects={selectedServer?.appsToServers.map(({ app }) => app)}
selectedProject={selectedApp}
setSelectedProject={setSelectedApp}
/>
+22 -8
View File
@@ -1,28 +1,42 @@
import { useMutation } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import useModalStore from "../../stores/useModalStore";
import { Session } from "../../types/Session";
import Button from "../Button";
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 } = useModalStore();
const { setModal, setPosition } = useModalStore();
const { mutate: endSession, isPending } = useMutation({
mutationKey: ["sessions", session.id],
mutationFn: () =>
api.put(`sessions/${session.id}`, { json: { status: "ending" } }),
api
.put(`sessions/${session.id}`, { json: { status: "ending" } })
.json<Session>(),
onMutate: () => {
queryClient.invalidateQueries({ queryKey: ["sessions"] });
queryClient.invalidateQueries({ queryKey: ["last-started"] });
queryClient.invalidateQueries({ queryKey: ["servers"] });
// queryClient.invalidateQueries({ queryKey: ["sessions"] });
// queryClient.invalidateQueries({ queryKey: ["last-started"] });
// queryClient.invalidateQueries({ queryKey: ["servers"] });
setModal(null);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["servers"] });
queryClient.invalidateQueries({ queryKey: ["last-sessions"] });
setModal(null);
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} />);
}
},
});
+5 -5
View File
@@ -15,8 +15,8 @@ import DownloadIcon from "../icons/DownloadIcon";
import ShareIcon from "../icons/ShareIcon";
function SessionModal({ session }: { session: Session }) {
const { data } = useQuery({
queryKey: ["file-list"],
const { data: files } = useQuery({
queryKey: ["file-list", session.id],
queryFn: () =>
api
.get("files", {
@@ -122,13 +122,13 @@ function SessionModal({ session }: { session: Session }) {
</span>
</Button>
</div>
{data && (
{files && (
<div className="flex flex-col gap-[1.111vw] bg-white rounded-[1.667vw] p-[1.111vw]">
<h3 className="title-s flex items-center font-medium gap-[0.556vw]">
<span>Документы по сеансу</span>
<Badge count={data?.length} />
<Badge count={files?.length} />
</h3>
<SessionFiles files={data} session={session} />
<SessionFiles files={files} session={session} />
<div className="flex w-full gap-[0.556vw]">
<Button variant="primary" size="large" className="w-full">
<span className="size-[1.111vw] text-[#7B60F3]">
+2
View File
@@ -1,4 +1,5 @@
import { Manager } from "./Manager";
import { Session } from "./Session";
export interface Comment {
id: string;
@@ -8,4 +9,5 @@ export interface Comment {
managerId: string;
sessionId: string;
manager: Manager;
session: Session;
}
+1 -1
View File
@@ -8,7 +8,7 @@ export interface Server {
description: string;
companyId: string;
sessions?: Session[];
apps?: { app: App }[];
appsToServers?: { app: App }[];
status: "online" | "offline";
ipAddress: string;
}