Added Team mamagement with backend integration
This commit is contained in:
@@ -19,7 +19,11 @@
|
||||
color: var(--mantine-color-accent-filled);
|
||||
* {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,42 +6,38 @@ import { IconTrash } from "@tabler/icons-react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
|
||||
function ExperimentsListCard(props: Experiment) {
|
||||
function ExperimentsListCard(props: {
|
||||
experiment: Experiment;
|
||||
team: { team_id: number; team_name: string } | undefined;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const { selectExperiment, removeExperiment, tasks, teams } =
|
||||
useExperimentStore();
|
||||
const { removeExperiment, tasks } = useExperimentStore();
|
||||
|
||||
const team = teams.find((team) => {
|
||||
return team.id == props.team_id;
|
||||
});
|
||||
|
||||
const experiment_tasks = tasks.filter((a) => a.id in props.tasks_ids);
|
||||
const experiment_tasks = tasks.filter(
|
||||
(a) => a.id in props.experiment.tasks_ids,
|
||||
);
|
||||
|
||||
const handleDelete = () => {
|
||||
removeExperiment(props.id);
|
||||
removeExperiment(props.experiment.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="ExperimentsListCard"
|
||||
onClick={() => {
|
||||
console.log(props.id);
|
||||
selectExperiment(props.id);
|
||||
navigate(props.id.toString());
|
||||
navigate(props.experiment.id.toString());
|
||||
}}
|
||||
>
|
||||
<SimpleGrid cols={3}>
|
||||
<div className="ExperimentSectionWithLine">
|
||||
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||
{props.name}
|
||||
Эксперимент: {props.experiment.name}
|
||||
</Text>
|
||||
<Text mb="sm" size="md">
|
||||
Team: {team ? team.name : "ERROR"}
|
||||
Команда: {props.team?.team_name}
|
||||
</Text>
|
||||
<Pill className="ExperimentPill">
|
||||
<Text mb="sm" size="md">
|
||||
Статус: {props.experiment_status}
|
||||
</Text>
|
||||
<Text size="md">Статус: {props.experiment.experiment_status}</Text>
|
||||
</Pill>
|
||||
</div>
|
||||
<div className="ExperimentSectionWithLine">
|
||||
@@ -62,15 +58,7 @@ function ExperimentsListCard(props: Experiment) {
|
||||
);
|
||||
})}
|
||||
{experiment_tasks.length == 0 ? (
|
||||
<Pill
|
||||
size="md"
|
||||
className="ExperimentPill2"
|
||||
style={{
|
||||
textWrap: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Pill size="md" className="ExperimentPill2">
|
||||
Нет Задач
|
||||
</Pill>
|
||||
) : (
|
||||
@@ -81,8 +69,12 @@ function ExperimentsListCard(props: Experiment) {
|
||||
<div className="RightExperimentSection">
|
||||
<div className="TopRightContainer">
|
||||
<div className="dateContainer">
|
||||
<Text size="sm">{props.date_created.toLocaleDateString()} </Text>
|
||||
<Text size="sm">{props.date_created.toLocaleTimeString()}</Text>
|
||||
<Text size="sm">
|
||||
{props.experiment.date_created.toLocaleDateString("ru")}{" "}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{props.experiment.date_created.toLocaleTimeString("ru")}
|
||||
</Text>
|
||||
</div>
|
||||
<UnstyledButton
|
||||
onClick={(e: MouseEvent) => {
|
||||
@@ -100,16 +92,16 @@ function ExperimentsListCard(props: Experiment) {
|
||||
display: "flex",
|
||||
justifyContent: "right",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<Text>
|
||||
{" "}
|
||||
Тип эксперимента:{" "}
|
||||
<Pill className="ExperimentPill2">{props.experiment_type}</Pill>
|
||||
</Text>
|
||||
<Text> Тип эксперимента: </Text>
|
||||
<Pill className="ExperimentPill">
|
||||
{props.experiment.experiment_type}
|
||||
</Pill>
|
||||
</div>
|
||||
<Text color="secondary" size="sm">
|
||||
#{props.id}
|
||||
<Text c="dimmed" size="sm">
|
||||
#{props.experiment.id}
|
||||
</Text>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.TeamMembers {
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
MultiSelect,
|
||||
Pill,
|
||||
Stack,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCheck,
|
||||
IconCrown,
|
||||
IconForbid,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import type { Team, TeamMember } from "Types/Team/Team";
|
||||
import "./TeamMemberCard.css";
|
||||
import { useState } from "react";
|
||||
import { addMember, deleteMember } from "Api/QuantumBackend/TeamManagement";
|
||||
import { useTeamStore } from "Stores/TeamsStore";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
||||
const { updateTeam } = useTeamStore();
|
||||
const [is_editing, set_is_editing] = useState<boolean>(false);
|
||||
const [selected_permissions, set_selected_permissions] = useState<string[]>(
|
||||
props.member.permissions,
|
||||
);
|
||||
|
||||
return (
|
||||
<Card padding="sm" withBorder orientation="horizontal">
|
||||
<Card.Section
|
||||
inheritPadding
|
||||
px="xs"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
justifyContent:
|
||||
props.member.user.keycloak_id !=
|
||||
props.cur_team.creator.user.keycloak_id
|
||||
? "center"
|
||||
: "",
|
||||
}}
|
||||
>
|
||||
{props.member.user.keycloak_id ==
|
||||
props.cur_team.creator.user.keycloak_id && <IconCrown />}
|
||||
<Avatar radius="xl" src={props.member.user.profile_picture_path} />
|
||||
</Card.Section>
|
||||
<Card.Section
|
||||
withBorder
|
||||
inheritPadding
|
||||
px="xs"
|
||||
style={{ width: "250px" }}
|
||||
>
|
||||
<Stack>
|
||||
<Text size="xl">{props.member.user.username}</Text>
|
||||
<Text size="md">{props.member.user.email}</Text>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section
|
||||
inheritPadding
|
||||
px="md"
|
||||
withBorder
|
||||
style={{
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<div style={{ display: "flex", gap: "20px" }}>
|
||||
<Text size="md">Разрешения: </Text>
|
||||
|
||||
{props.member.user.keycloak_id !=
|
||||
props.cur_team.creator.user.keycloak_id &&
|
||||
(is_editing ? (
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
<CustomButton
|
||||
style="outline"
|
||||
text="Сохранить"
|
||||
textSize="sm"
|
||||
icon={<IconPencil size={15} />}
|
||||
onClick={() => {
|
||||
addMember({
|
||||
team_id: props.cur_team.id,
|
||||
user_id: props.member.user.keycloak_id,
|
||||
permissions: selected_permissions,
|
||||
})
|
||||
.then((member) => {
|
||||
if (member) {
|
||||
updateTeam(props.cur_team.id, {
|
||||
members: [
|
||||
...props.cur_team.members.filter(
|
||||
(member) =>
|
||||
member.user.keycloak_id !=
|
||||
props.member.user.keycloak_id,
|
||||
),
|
||||
member,
|
||||
],
|
||||
});
|
||||
set_is_editing(false);
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Права изменены успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Ошибка изменения прав",
|
||||
message: "",
|
||||
color: "red",
|
||||
icon: <IconForbid />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CustomButton
|
||||
style="outline"
|
||||
text="Отменить"
|
||||
color="red"
|
||||
textSize="sm"
|
||||
icon={<IconPencil size={15} />}
|
||||
onClick={() => {
|
||||
set_is_editing(false);
|
||||
set_selected_permissions(props.member.permissions);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<CustomButton
|
||||
style="outline"
|
||||
text="Изменить"
|
||||
textSize="sm"
|
||||
icon={<IconPencil size={15} />}
|
||||
onClick={() => {
|
||||
set_is_editing(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
gap: "10px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
{!is_editing ? (
|
||||
props.member.permissions.map((perm) => (
|
||||
<Pill className="TeamPill">{perm}</Pill>
|
||||
))
|
||||
) : (
|
||||
<MultiSelect
|
||||
value={selected_permissions}
|
||||
label=""
|
||||
placeholder="Выберите разрешения"
|
||||
searchable
|
||||
data={props.cur_team.creator.permissions}
|
||||
classNames={{ input: "MantineInput" }}
|
||||
onChange={(value) => {
|
||||
set_selected_permissions(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section
|
||||
inheritPadding
|
||||
px="md"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<UnstyledButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteMember({
|
||||
team_id: props.cur_team.id,
|
||||
user_id: props.member.user.keycloak_id,
|
||||
}).then(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Пользователь удален успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
updateTeam(props.cur_team.id, {
|
||||
members: [
|
||||
...props.cur_team.members.filter(
|
||||
(member) =>
|
||||
member.user.keycloak_id != props.member.user.keycloak_id,
|
||||
),
|
||||
],
|
||||
});
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
width: "20px",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</UnstyledButton>
|
||||
<Stack gap={"5px"}>
|
||||
<Text style={{ alignSelf: "flex-end" }}>Добавлен:</Text>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Text size="sm" style={{ textAlign: "right" }}>
|
||||
{new Date(props.member.joined_at).toLocaleDateString("ru")}
|
||||
</Text>
|
||||
<Text size="sm" style={{ textAlign: "right" }}>
|
||||
{new Date(props.member.joined_at).toLocaleTimeString("ru")}
|
||||
</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamMemberCard;
|
||||
56
src/Components/ListCard/TeamListCard/TeamsListCard.css
Normal file
56
src/Components/ListCard/TeamListCard/TeamsListCard.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.TeamsListCard {
|
||||
height: 120px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.TeamSection {
|
||||
border-right: 1px solid var(--mantine-color-contrast-filled);
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.TeamPill {
|
||||
background-color: var(--mantine-color-contrast-filled);
|
||||
color: var(--mantine-color-primary-filled);
|
||||
}
|
||||
|
||||
.TeamPill2 {
|
||||
border: 2px solid var(--mantine-color-accent-filled);
|
||||
background-color: transparent;
|
||||
color: var(--mantine-color-accent-filled);
|
||||
* {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.membersPills {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dateContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: right;
|
||||
gap: 5px;
|
||||
.p {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
.TopRightContainer {
|
||||
justify-content: right;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.RightTeamSection {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
211
src/Components/ListCard/TeamListCard/TeamsListCard.tsx
Normal file
211
src/Components/ListCard/TeamListCard/TeamsListCard.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Card, Pill, Text, UnstyledButton } from "@mantine/core";
|
||||
import "./TeamsListCard.css";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { Team, TeamMember } from "Types/Team/Team";
|
||||
import {
|
||||
IconCancel,
|
||||
IconCheck,
|
||||
IconDoorExit,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useTeamStore } from "Stores/TeamsStore";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import { deleteMember, deleteTeam } from "Api/QuantumBackend/TeamManagement";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
function TeamsListCard(props: Team) {
|
||||
const navigate = useNavigate();
|
||||
const { removeTeam, setTotalTeams, totalTeams } = useTeamStore();
|
||||
const { profile } = useAuthenticationStore();
|
||||
|
||||
const handleDelete = () => {
|
||||
// TODO: fix delete
|
||||
deleteTeam(props.id)
|
||||
.then(() => {
|
||||
removeTeam(props.id);
|
||||
setTotalTeams(totalTeams - 1);
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Команда удалена успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
color: "red",
|
||||
title: "Не удалось удалить команду",
|
||||
message: "",
|
||||
icon: <IconCancel />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleLeave = () => {
|
||||
// TODO: fix delete
|
||||
if (profile && profile.id) {
|
||||
deleteMember({ team_id: props.id, user_id: profile.id })
|
||||
.then(() => {
|
||||
removeTeam(props.id);
|
||||
setTotalTeams(totalTeams - 1);
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Успешно вышел из команды",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
color: "red",
|
||||
title: "Не удалось выйти из команды",
|
||||
message: "",
|
||||
icon: <IconCancel />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="TeamsListCard"
|
||||
onClick={() => {
|
||||
navigate(props.id.toString());
|
||||
}}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<Card.Section
|
||||
withBorder
|
||||
inheritPadding
|
||||
px="xs"
|
||||
style={{ width: "430px" }}
|
||||
>
|
||||
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||
Команда: {props.name}
|
||||
</Text>
|
||||
<Text mb="sm" size="md">
|
||||
Владелец:
|
||||
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||
<Text size="md">{props.creator.user.username}</Text>
|
||||
</Pill>
|
||||
</Text>
|
||||
<div className="membersPills">
|
||||
<Text>Члены:</Text>
|
||||
{props.members.map((team: TeamMember) => {
|
||||
return (
|
||||
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||
<Text
|
||||
size="md"
|
||||
style={{
|
||||
textWrap: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{team.user.username}
|
||||
</Text>
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card.Section>
|
||||
<Card.Section
|
||||
inheritPadding
|
||||
px="xs"
|
||||
withBorder
|
||||
style={{
|
||||
flexGrow: "1",
|
||||
gap: "15px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Text>Разрешения:</Text>
|
||||
<div
|
||||
className="membersPills"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{props.members
|
||||
.find((member) => {
|
||||
return profile && member.user.keycloak_id == profile?.id;
|
||||
})
|
||||
?.permissions.map((perm) => {
|
||||
return (
|
||||
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||
<Text
|
||||
size="md"
|
||||
style={{
|
||||
textWrap: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{perm}
|
||||
</Text>
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card.Section>
|
||||
<Card.Section
|
||||
inheritPadding
|
||||
px="md"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div className="TopRightContainer">
|
||||
<div className="dateContainer">
|
||||
<Text size="sm">
|
||||
{new Date(props.created_at).toLocaleDateString()}{" "}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(props.created_at).toLocaleTimeString()}
|
||||
</Text>
|
||||
</div>
|
||||
{props.creator.user.keycloak_id == profile?.id && (
|
||||
<UnstyledButton
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleDelete();
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</UnstyledButton>
|
||||
)}
|
||||
{props.creator.user.keycloak_id != profile?.id && (
|
||||
<UnstyledButton
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleLeave();
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<IconDoorExit size={20} />
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text c="dimmed" size="sm" style={{ alignSelf: "flex-end" }}>
|
||||
#{props.id}
|
||||
</Text>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsListCard;
|
||||
Reference in New Issue
Block a user