Added Team mamagement with backend integration

This commit is contained in:
2026-04-30 11:11:11 +03:00
parent 8449da8373
commit 45c157b910
35 changed files with 1570 additions and 4671 deletions

View File

@@ -0,0 +1,6 @@
.TeamMembers {
margin-top: 10px;
gap: 10px;
display: flex;
flex-direction: column;
}

View File

@@ -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;

View 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;
}

View 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;