This commit is contained in:
2023-10-16 12:23:31 +05:00
parent 826ac1f621
commit 4c83933741
8 changed files with 435 additions and 95 deletions
+100 -43
View File
@@ -1,7 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from "react";
import SelectUser from "./SelectUser";
import MoreIcon from "./icons/MoreIcon";
import api from "../utils/api";
interface CardProps {
companyId: string;
buildId: string;
scheduledSessionId: string;
scheduleSessionStartAt: string;
client: {
name: string;
phone: string;
@@ -12,57 +20,106 @@ interface CardProps {
name: string;
avatar: string;
};
managers: any[];
handleSelect: (scheduledSessionId: string, managerId: string) => void;
}
function Card({ client, manager }: CardProps) {
function Card({
companyId,
buildId,
scheduledSessionId,
scheduleSessionStartAt,
client,
manager,
managers,
handleSelect,
}: CardProps) {
const [isShow, setIsShow] = useState<boolean>(false);
const [availableManagers, setAvailableManagers] = useState<any[]>();
async function getAvailableManagers() {
const result: any[] = await api
.get(
`companies/${companyId}/builds/${buildId}/scheduled_sessions/${scheduledSessionId}/availableManagers?startAt=${scheduleSessionStartAt}`
)
.json();
const filteredManagers = managers.filter(
(manager) => !result.includes(manager.id)
);
setAvailableManagers(filteredManagers);
}
useEffect(() => {
if (!isShow) return;
getAvailableManagers();
}, [isShow]);
return (
<div className="w-[264px] h-[128px] px-3 py-2 bg-white border-r border-b border-[#DAE0E5] flex flex-col gap-2">
<div className="flex justify-between">
<p className="text-sm font-semibold">{client.name}</p>
{manager ? (
<div className="bg-[#DAF2E4] rounded-full px-2 h-[20px] flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-[#27AE60]"></div>
<p className="text-[10px] font-semibold text-[#27AE60] pt-0.5">
Сеанс начат
</p>
</div>
) : (
<div className="bg-[#F2DADA] rounded-full px-2 h-[20px] flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-[#EB5757]"></div>
<p className="text-[10px] font-semibold text-[#EB5757] pt-0.5">
Нет менеджена
</p>
</div>
)}
</div>
<div className="border-b border-[#DAE0E5] pb-2">
<p className="text-xs text-[#77828C] leading-tight">{client.phone}</p>
<p className="text-xs text-[#77828C] leading-tight">{client.email}</p>
</div>
<div className="flex justify-between items-center py-1">
<div className="flex items-center gap-2">
<div className="relative flex flex-col gap-2">
<div className="w-[264px] h-[128px] px-3 py-2 bg-white border-r border-b border-[#DAE0E5] flex flex-col gap-2">
<div className="flex justify-between">
<p className="text-sm font-semibold">{client.name}</p>
{manager ? (
<div className="relative">
<img
src={manager.avatar}
alt=""
className="w-6 h-6 rounded-full"
/>
<div className="absolute right-0 bottom-0 bg-[#27AE60] border border-white rounded-full w-2 h-2"></div>
<div className="bg-[#DAF2E4] rounded-full px-2 h-[20px] flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-[#27AE60]"></div>
<p className="text-[10px] font-semibold text-[#27AE60] pt-0.5">
Сеанс начат
</p>
</div>
) : (
<div className="w-6 h-6 bg-[#ccc] rounded-full"></div>
<div className="bg-[#F2DADA] rounded-full px-2 h-[20px] flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-[#EB5757]"></div>
<p className="text-[10px] font-semibold text-[#EB5757] pt-0.5">
Нет менеджера
</p>
</div>
)}
<p className="text-xs font-semibold">
{manager ? manager.name : "Не назначен"}
</p>
</div>
<button
onClick={() => alert("Чё тыкаешь? В разработке еще!!")}
className="p-1 text-[#77828C] hover:bg-neutral-100 rounded-lg"
>
<MoreIcon />
</button>
<div className="border-b border-[#DAE0E5] pb-2">
<p className="text-xs text-[#77828C] leading-tight">{client.phone}</p>
<p className="text-xs text-[#77828C] leading-tight">{client.email}</p>
</div>
<div className="flex justify-between items-center py-1">
<div className=" flex items-center gap-2">
{manager ? (
<div className="relative">
<img
src={manager.avatar}
alt=""
className="w-6 h-6 rounded-full"
/>
{/* <div className="absolute right-0 bottom-0 bg-[#27AE60] border border-white rounded-full w-2 h-2"></div> */}
</div>
) : (
<div className="w-6 h-6 bg-[#ccc] rounded-full"></div>
)}
<p className="text-xs font-semibold">
{manager ? manager.name : "Не назначен"}
</p>
</div>
<button
onClick={() => setIsShow(!isShow)}
className="p-1 text-[#77828C] hover:bg-neutral-100 rounded-lg"
>
<MoreIcon />
</button>
</div>
</div>
<div className="absolute bottom-[2px] left-[272px] z-10">
{availableManagers?.length && (
<SelectUser
shown={isShow}
selectedManagerId={manager?.id}
managers={availableManagers}
handleClick={(managerId) => (
handleSelect(scheduledSessionId, managerId), setIsShow(false)
)}
handleShown={() => setIsShow((prev) => !prev)}
/>
)}
</div>
</div>
);
+64
View File
@@ -0,0 +1,64 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
interface SelectUserProps {
shown: boolean;
selectedManagerId?: string;
managers: any[];
handleClick: (managerId: string) => void;
handleShown: () => void;
}
import { Transition } from "react-transition-group";
import CheckIcon from "./icons/CheckIcon";
import useOutsideClick from "../hooks/useOutsideClick";
function SelectUser({
shown,
selectedManagerId,
managers,
handleClick,
handleShown,
}: SelectUserProps) {
const selectUserRef = useOutsideClick(handleShown);
return (
<Transition in={shown} timeout={150} mountOnEnter unmountOnExit>
{(state) => (
<div
ref={selectUserRef}
className={`bg-white w-[280px] max-h-[126px] overflow-auto rounded-lg py-2 flex flex-col gap-1 transition-opacity ${state}`}
style={{ boxShadow: "0px 1px 4px 0px rgba(0, 0, 0, 0.16)" }}
>
{managers.map((manager) => (
<button
onClick={() => handleClick(manager.id)}
className="px-4 flex justify-between gap-2 w-full py-1 transition-colors hover:bg-[#E6ECF2]"
>
<div className="flex items-center gap-2">
<div className="relative">
<img
src={
manager.avatar ||
"https://as2.ftcdn.net/v2/jpg/00/64/67/27/1000_F_64672736_U5kpdGs9keUll8CRQ3p3YaEv2M6qkVY5.jpg"
}
alt=""
className="w-6 h-6 rounded-full"
/>
{/* <div className="absolute right-0 bottom-0 bg-[#27AE60] border border-white rounded-full w-2 h-2"></div> */}
</div>
<p className="text-xs">{manager.name}</p>
</div>
{manager.id === selectedManagerId && (
<div className="text-[#49A1F5]">
<CheckIcon />
</div>
)}
</button>
))}
</div>
)}
</Transition>
);
}
export default SelectUser;
+20
View File
@@ -0,0 +1,20 @@
function CheckIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.7474 7.33565C20.1143 7.74843 20.0771 8.3805 19.6644 8.74742L10.6644 16.7474C10.2686 17.0992 9.66729 17.0815 9.29289 16.7071L4.29289 11.7071C3.90237 11.3166 3.90237 10.6834 4.29289 10.2929C4.68342 9.90238 5.31658 9.90238 5.70711 10.2929L10.0404 14.6262L18.3356 7.2526C18.7484 6.88568 19.3805 6.92286 19.7474 7.33565Z"
fill="currentColor"
/>
</svg>
);
}
export default CheckIcon;
@@ -0,0 +1,21 @@
function ChevronDown() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 11L12 16L17 11"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default ChevronDown;
+23
View File
@@ -0,0 +1,23 @@
import { useEffect, useRef } from "react";
function useOutsideClick(callback: () => void) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [callback]);
return ref;
}
export default useOutsideClick;
+107 -50
View File
@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import Card from "../components/Card";
import EmptyCard from "../components/EmptyCard";
import TabButton from "../components/TabButton";
@@ -48,6 +48,7 @@ function DashboardPage() {
);
const [isLoadingScheduledSessions, setIsLoadingScheduledSessions] =
useState(true);
const scheduledSessionsRef = useRef<HTMLDivElement>(null);
// const [selectedDate, setSelectedDate] = useState(
// format(new Date(), "d MMMM, yyyy", { locale: ru })
@@ -73,7 +74,6 @@ function DashboardPage() {
try {
const result: any = await api.get(`companies/${user.companyId}`).json();
console.log("getCompany result: ", result);
setCompany(result);
} catch (error) {
if (error instanceof Error) {
@@ -92,7 +92,6 @@ function DashboardPage() {
const result: any = await api
.get(`companies/${user.companyId}/builds`)
.json();
console.log("getBuilds result: ", result);
setBuilds(result);
} catch (error) {
if (error instanceof Error) {
@@ -109,7 +108,6 @@ function DashboardPage() {
try {
const result: any = await api.get(`companies/${company.id}/users`).json();
console.log("getManagers result: ", result);
setManagers(result);
} catch (error) {
if (error instanceof Error) {
@@ -118,20 +116,18 @@ function DashboardPage() {
}
}
async function getScheduledSessions() {
async function getScheduledSessions(useLoader?: boolean) {
if (!company) {
console.log("No ScheduledSessions");
return;
}
console.log("selectedBuild", selectedBuild);
if (!selectedBuild || !selectedBuild.id) {
console.log("No selectedBuild");
return;
}
setIsLoadingScheduledSessions(true);
if (useLoader) setIsLoadingScheduledSessions(true);
try {
const result: any = await api
@@ -141,16 +137,15 @@ function DashboardPage() {
}/scheduled_sessions?date=${selectedDate.toISOString()}`
)
.json();
console.log("getScheduledSessions result: ", result);
setScheduledSessions(result);
setIsLoadingScheduledSessions(false);
} catch (error) {
setIsLoadingScheduledSessions(false);
if (error instanceof Error) {
console.log("Error: ", error.message);
}
}
if (useLoader) setIsLoadingScheduledSessions(false);
}
function logout() {
@@ -185,6 +180,36 @@ function DashboardPage() {
setGeneratedScheduledSessions(arr);
}
async function updateScheduledSessionManager(
scheduledSessionId: string,
managerId: string
) {
if (!company || !scheduledSessions) return;
try {
const result: any = await api
.put(
`companies/${company.id}/scheduled_sessions/${scheduledSessionId}`,
{
json: { userId: managerId },
}
)
.json();
console.log(scheduledSessions, result);
setScheduledSessions(
scheduledSessions.map((scheduledSession) =>
scheduledSession.id === result.id ? result : scheduledSession
)
);
} catch (error) {
if (error instanceof Error) {
console.log("Error: ", error.message);
}
}
}
useEffect(() => {
getCompany();
@@ -210,18 +235,27 @@ function DashboardPage() {
useEffect(() => {
if (!managers || !selectedDate || !selectedBuild) return;
getScheduledSessions();
getScheduledSessions(true);
console.log(selectedBuild);
const interval = setInterval(() => {
getScheduledSessions();
}, 3000);
return () => {
clearInterval(interval);
};
}, [managers, selectedDate, selectedBuild]);
useEffect(() => {
if (!scheduledSessions) return;
generateScheduledSessions();
console.log(scheduledSessions);
}, [scheduledSessions]);
useEffect(() => {
// setIsLoadingScheduledSessions(true);
scheduledSessionsRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, [selectedDate, selectedBuild]);
return (
<div className="main h-screen flex">
<div className="left flex flex-col w-full">
@@ -282,6 +316,7 @@ function DashboardPage() {
</div>
<div
ref={scheduledSessionsRef}
className={`overflow-y-auto overflow-x-hidden flex-1 bg-[#F2F2F2] border-r border-[#DAE0E5]`}
>
<Transition
@@ -299,43 +334,65 @@ function DashboardPage() {
)}
</Transition>
{!isLoadingScheduledSessions &&
{company &&
selectedBuild &&
user &&
managers?.length &&
generatedScheduledSessions?.map(
(generatedScheduledSession, index) => (
<div key={index} className="flex">
{generatedScheduledSession.map((scheduledSession, index2) => {
if (index2 === 0) {
return (
<div
key={index2}
className="w-[84px] h-[128px] flex justify-center items-center text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5]"
>
{format(scheduledSession, "HH:mm")}
</div>
);
} else {
if (Object.keys(scheduledSession).length) {
return (
<Card
key={index2}
client={{
name: scheduledSession.clientName,
phone: scheduledSession.clientPhone,
email: scheduledSession.clientEmail,
}}
manager={managers?.find(
(manager) =>
manager.id === scheduledSession.userId
)}
/>
);
} else {
return <EmptyCard key={index2} />;
(generatedScheduledSession, index) => {
return (
<div key={index} className="flex">
{generatedScheduledSession.map(
(scheduledSession, index2) => {
if (index2 === 0) {
return (
<div
key={index2}
className="w-[84px] h-[128px] flex justify-center items-center text-sm font-semibold bg-[#F0F1F2] border-r border-b border-[#DAE0E5]"
>
{format(scheduledSession, "HH:mm")}
</div>
);
} else {
if (Object.keys(scheduledSession).length) {
const selectedManager = managers.find(
(manager) => manager.id == scheduledSession.userId
);
return (
<Card
key={index2}
companyId={company.id}
buildId={selectedBuild.id}
scheduledSessionId={scheduledSession.id}
scheduleSessionStartAt={
scheduledSession.startAt
}
client={{
name: scheduledSession.clientName,
phone: scheduledSession.clientPhone,
email: scheduledSession.clientEmail,
}}
manager={selectedManager}
managers={managers}
// managers={managers}
handleSelect={(scheduledSessionId, managerId) =>
updateScheduledSessionManager(
scheduledSessionId,
managerId
)
}
/>
);
} else {
return <EmptyCard key={index2} />;
}
}
}
}
})}
</div>
)
)}
</div>
);
}
)}
</div>
</div>