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

@@ -15,7 +15,12 @@ function DevicesPage() {
/>
</Helmet>
<div className="DevicesPage">
<PaginationContainer numberOfPages={1}>
<PaginationContainer
numberOfPages={1}
isLoading={false}
activePage={1}
setPage={() => {}}
>
<Alert title="Подключенные устройства не найдены" color="blue">
Для того, чтобы добавить устройство, простмотрите{" "}
<Link to={"documentaion"} className="invisible_link">

View File

@@ -6,25 +6,17 @@ import { useState } from "react";
import { Helmet } from "react-helmet";
import { IconPlus, IconSettings } from "@tabler/icons-react";
import { useParams } from "react-router";
import {
useExperimentStore,
useSelectedExperiment,
} from "Stores/ExperimentStore";
import CustomButton from "Components/CustomButton/CustomButton";
import { useExperimentStore } from "Stores/ExperimentStore";
function ExperimentPage() {
const [isOpen, setIsOpen] = useState(false);
const { experiment_id } = useParams();
const { selectedExperimentId, selectExperiment } = useExperimentStore();
const { experiments } = useExperimentStore();
if (
!selectedExperimentId ||
(Number(experiment_id) != selectedExperimentId && experiment_id)
) {
console.log(Number(experiment_id));
selectExperiment(Number(experiment_id));
}
const experiment = useSelectedExperiment();
const experiment = experiments.find((exp) => {
return exp.id == Number(experiment_id);
});
return (
<>
@@ -74,7 +66,12 @@ function ExperimentPage() {
</div>
</div>
{experiment.tasks_ids.length > 0 && (
<PaginationContainer numberOfPages={1}>
<PaginationContainer
numberOfPages={1}
isLoading={false}
activePage={1}
setPage={() => {}}
>
{experiment.tasks_ids.map((task: number) => {
return <div>{task}</div>;
})}

View File

@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet";
import "./ExperimentsPage.css";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import { useState } from "react";
import { useEffect, useState } from "react";
import { IconMicroscope } from "@tabler/icons-react";
import { NewExperimentModal } from "Modals/NewExperiment/NewExperiment";
import ExperimentsListCard from "Components/ListCard/ExperimentsListCard";
@@ -9,10 +9,29 @@ import { useExperimentStore } from "Stores/ExperimentStore";
import type { Experiment } from "Types/Experiment/Experiment";
import CustomButton from "Components/CustomButton/CustomButton";
import { Alert } from "@mantine/core";
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
import { useAuthenticationStore } from "Stores/AuthenticationStore";
function ExperimentsPage() {
const [isOpen, setIsOpen] = useState(false);
const { experiments, teams } = useExperimentStore();
const { experiments } = useExperimentStore();
const [teams, setTeams] =
useState<{ team_id: number; team_name: string }[]>();
const { profile, is_loading } = useAuthenticationStore();
useEffect(() => {
if (profile || !is_loading)
getShortTeamsList()
.then((teams) => {
if (teams) {
setTeams(teams);
}
})
.catch(() => {
setTeams([]);
});
}, [profile, is_loading]);
return (
<>
@@ -23,7 +42,11 @@ function ExperimentsPage() {
content="See the list of all of users experiments"
/>
</Helmet>
<NewExperimentModal isOpened={isOpen} setIsOpened={setIsOpen} />
<NewExperimentModal
isOpened={isOpen}
setIsOpened={setIsOpen}
teams={teams}
/>
<div className="ExperimentsPage">
<div
style={{
@@ -33,7 +56,7 @@ function ExperimentsPage() {
marginBottom: "20px",
}}
>
{teams.length > 0 && (
{teams && teams.length > 0 && (
<div className="experimentsButtons">
<CustomButton
color="contrast"
@@ -46,27 +69,40 @@ function ExperimentsPage() {
</div>
)}
</div>
<PaginationContainer numberOfPages={1}>
<PaginationContainer
numberOfPages={1}
isLoading={teams == undefined}
activePage={1}
setPage={() => {}}
>
{experiments.map((exp: Experiment) => {
return (
<ExperimentsListCard
id={exp.id}
name={exp.name}
description={exp.description}
team_id={exp.team_id}
tasks_ids={exp.tasks_ids}
date_created={exp.date_created}
experiment_status={exp.experiment_status}
experiment_type="A"
experiment={{
id: exp.id,
name: exp.name,
description: exp.description,
team_id: exp.team_id,
tasks_ids: exp.tasks_ids,
date_created: exp.date_created,
experiment_status: exp.experiment_status,
experiment_type: "A",
}}
team={teams?.find((team) => team.team_id == exp.team_id)}
/>
);
})}
{teams.length == 0 && (
{teams && teams.length == 0 && (
<Alert title="Команды не найдены" color="red">
Создайте или войдите в команду чтобы начать работу с
экспериментами
</Alert>
)}
{teams && teams.length != 0 && experiments.length == 0 && (
<Alert title="Экспериментов нету" color="blue">
Вы еще не создали не один эксперимент
</Alert>
)}
</PaginationContainer>
</div>
</>

View File

@@ -0,0 +1,11 @@
.TeamPage {
flex-grow: 1;
display: flex;
flex-direction: column;
position: relative;
padding: 10px;
}
.TeamData {
margin-bottom: 15px;
}

View File

@@ -0,0 +1,228 @@
import {
Alert,
Avatar,
Card,
Center,
Grid,
LoadingOverlay,
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
import "./TeamPage.css";
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { IconCheck, IconPlus } from "@tabler/icons-react";
import { useParams } from "react-router";
import CustomButton from "Components/CustomButton/CustomButton";
import { useTeamStore } from "Stores/TeamsStore";
import { getTeam, updateTeamRequest } from "Api/QuantumBackend/TeamManagement";
import type { Team, TeamMember } from "Types/Team/Team";
import { useAuthenticationStore } from "Stores/AuthenticationStore";
import TeamMemberCard from "Components/ListCard/TeamListCard/TeamMemberCard/TeamMemberCard";
import { AddMember } from "Modals/AddMember/AddMember";
import { notifications } from "@mantine/notifications";
function TeamPage() {
const [isOpen, setIsOpen] = useState(false);
const { team_id } = useParams();
const { addTeam, teams, updateTeam } = useTeamStore();
const { profile } = useAuthenticationStore();
const [is_loading, set_is_loading] = useState(true);
const [cur_team, set_cur_team] = useState<Team>();
const [team_name, set_team_name] = useState(cur_team?.name);
const [team_descr, set_team_descr] = useState(cur_team?.description);
useEffect(() => {
if (!cur_team && profile) {
getTeam(Number(team_id))
.then((team) => {
if (team) {
addTeam(team);
set_cur_team(team);
set_team_name(team.name);
set_team_descr(team.description);
set_is_loading(false);
}
})
.catch(() => {
set_is_loading(false);
});
} else {
if (profile) {
set_is_loading(false);
}
}
if (teams.find((e) => e.id === Number(team_id)) != cur_team) {
set_cur_team(teams.find((e) => e.id === Number(team_id)));
}
}, [cur_team, profile, teams]);
return (
<>
<Helmet>
<title>
{cur_team ? "Team " + cur_team.id + " | QMolSim" : "Error | QmolSim"}
</title>
<meta
name="description"
content="See the information on the currently selected team"
/>
</Helmet>
<AddMember
isOpened={isOpen}
setIsOpened={setIsOpen}
permissions={cur_team?.creator.permissions}
team={cur_team}
/>
<div className="TeamPage">
<LoadingOverlay
visible={is_loading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
loaderProps={{ size: 50, type: "dots" }}
/>
<Card className="TeamData">
<Grid>
<Grid.Col span={3}>
<Title size={"xl"}>Имя команды:</Title>
</Grid.Col>
<Grid.Col span={9}>
<TextInput
size="md"
value={team_name}
onChange={(val) => {
set_team_name(val.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={3}>
<Title size={"xl"}>Описание:</Title>
</Grid.Col>
<Grid.Col span={9}>
<Textarea
size="md"
value={team_descr ? team_descr : ""}
onChange={(val) => {
set_team_descr(val.target.value);
}}
minRows={2}
maxRows={4}
/>
</Grid.Col>
<Grid.Col span={3}>
<Title size={"xl"}>Создатель:</Title>
</Grid.Col>
<Grid.Col
span={9}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "10px",
}}
>
<Avatar
radius="xl"
src={cur_team?.creator.user.profile_picture_path}
/>
<Text size="md" style={{ textDecoration: "underline" }}>
{cur_team?.creator.user.username} (
{cur_team?.creator.user.email})
</Text>
</Grid.Col>
<Grid.Col span={6}>
<CustomButton
color="accent"
text="Сохранить"
disabled={
cur_team?.name == team_name &&
cur_team?.description == team_descr
}
onClick={() => {
if (cur_team && team_name)
updateTeamRequest({
team_id: cur_team.id,
name: team_name,
description: team_descr,
}).then((team) => {
updateTeam(team.team_id, {
name: team.name,
description: team.description,
});
notifications.show({
radius: "md",
title: "Информация команды обновлена успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
});
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<CustomButton
color="error"
text="Отменить"
disabled={
cur_team?.name == team_name &&
cur_team?.description == team_descr
}
onClick={() => {
set_team_name(cur_team?.name);
set_team_descr(cur_team?.description);
}}
/>
</Grid.Col>
</Grid>
</Card>
<div
style={{
display: "flex",
flexWrap: "nowrap",
gap: "20px",
alignItems: "center",
}}
>
<Title order={2} style={{ textWrap: "nowrap" }}>
Члены команды:{" "}
</Title>
<div>
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconPlus />}
text="Добавить члена"
/>
</div>
</div>
{cur_team && cur_team.members.length > 0 && (
<div className="TeamMembers">
{cur_team.members.map((member: TeamMember) => {
return <TeamMemberCard cur_team={cur_team} member={member} />;
})}
</div>
)}
{!cur_team && !is_loading && (
<Alert color="red">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Ошибка. Команда не найдена
</Text>{" "}
</Center>
</Alert>
)}
</div>
</>
);
}
export default TeamPage;

View File

@@ -3,14 +3,79 @@ import "./TeamsPage.css";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import CustomButton from "Components/CustomButton/CustomButton";
import { IconUsersGroup } from "@tabler/icons-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Alert } from "@mantine/core";
import { NewTeamModal } from "Modals/NewTeam/NewTeam";
import { useTeamStore } from "Stores/TeamsStore";
import { getTeams } from "Api/QuantumBackend/TeamManagement";
import { useAuthenticationStore as useAuthenticationStore } from "Stores/AuthenticationStore";
import TeamsListCard from "Components/ListCard/TeamListCard/TeamsListCard";
function TeamsPage() {
const [, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const { teams, setTeams, setTotalTeams, totalTeams, removeTeam } =
useTeamStore();
const [isLoading, setIsLoading] = useState(true);
const { profile } = useAuthenticationStore();
const [cur_page, set_cur_page] = useState<number>(1);
const [page_size, set_page_size] = useState<number>(256);
useEffect(() => {
setIsLoading(true);
if (profile) {
getTeams({ page_num: cur_page })
.then((teams) => {
setTeams(teams.teams);
set_cur_page(teams.cur_page);
setTotalTeams(teams.total_teams);
set_page_size(teams.page_size);
setIsLoading(false);
})
.catch(() => setIsLoading(false));
}
}, [profile]);
useEffect(() => {
if (profile) {
if (teams && teams.length > page_size) {
removeTeam(teams[teams.length - 1].id);
}
if (teams && cur_page > Math.ceil(totalTeams / page_size)) {
setIsLoading(true);
getTeams({ page_num: Math.max(1, cur_page - 1) })
.then((teams) => {
setTeams(teams.teams);
set_cur_page(teams.cur_page);
setTotalTeams(teams.total_teams);
set_page_size(teams.page_size);
setIsLoading(false);
scroll({ top: 0 });
})
.catch(() => setIsLoading(false));
set_cur_page(Math.max(1, cur_page - 1));
}
if (
teams &&
teams.length < Math.min(page_size, totalTeams) &&
cur_page != Math.ceil(totalTeams / page_size)
) {
getTeams({ page_num: cur_page })
.then((teams) => {
setTeams(teams.teams);
set_cur_page(teams.cur_page);
setTotalTeams(teams.total_teams);
set_page_size(teams.page_size);
setIsLoading(false);
scroll({ top: 0 });
})
.catch(() => setIsLoading(false));
}
}
}, [profile, teams]);
return (
<>
<NewTeamModal isOpened={isOpen} setIsOpened={setIsOpen} />
<Helmet>
<title>Teams Page | QMolSim</title>
<meta
@@ -26,13 +91,35 @@ function TeamsPage() {
setIsOpen(true);
}}
icon={<IconUsersGroup />}
text="Создать эксперимент"
text="Создать Команду"
/>
</div>
<PaginationContainer numberOfPages={1}>
<Alert title="Команды не найдены" color="blue">
Создайте или войдите в команду чтобы начать работу с экспериментами
</Alert>
<PaginationContainer
numberOfPages={Math.ceil(totalTeams / page_size)}
isLoading={isLoading}
activePage={cur_page}
setPage={(page) => {
getTeams({ page_num: page })
.then((teams) => {
setTeams(teams.teams);
set_cur_page(teams.cur_page);
setTotalTeams(teams.total_teams);
set_page_size(teams.page_size);
setIsLoading(false);
scroll({ top: 0 });
})
.catch(() => setIsLoading(false));
}}
>
{teams.length == 0 && !isLoading && (
<Alert title="Команды не найдены" color="blue">
Создайте или войдите в команду чтобы начать работу с
экспериментами
</Alert>
)}
{teams.map((team) => {
return <TeamsListCard {...team} />;
})}
</PaginationContainer>
</div>
</>

View File

@@ -23,7 +23,7 @@ import keycloak, {
} from "Api/Keycloak/Keycloak";
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { AuthenticationStore } from "Stores/AuthenticationStore";
import { useAuthenticationStore } from "Stores/AuthenticationStore";
import "./UserPage.css";
import { useUserPreferencesStore } from "Stores/PreferencesStore";
import CustomButton from "Components/CustomButton/CustomButton";
@@ -35,7 +35,8 @@ import {
} from "Api/QuantumBackend/UserManagement";
import type { UserData } from "Types/User/User";
function UserPage() {
const { profile, is_loading, profile_picture_path } = AuthenticationStore();
const { profile, is_loading, profile_picture_path } =
useAuthenticationStore();
const { theme, set_theme } = useUserPreferencesStore();
const [username, set_username] = useState<string>("");
const [email, set_email] = useState<string>("");
@@ -49,15 +50,13 @@ function UserPage() {
((profile.username && profile.username != username) ||
(profile.email && profile.email != email))
) {
console.log("A");
const prof_1 = profile;
prof_1.email = email;
prof_1.username = username;
AuthenticationStore.setState({
useAuthenticationStore.setState({
is_loading: true,
});
updateUserData(prof_1).then((data) => {
console.log(data);
if (data) {
notifications.show({
radius: "md",
@@ -67,7 +66,7 @@ function UserPage() {
style: { paddingLeft: "5px" },
});
keycloak.loadUserProfile().then((profile) => {
AuthenticationStore.setState({
useAuthenticationStore.setState({
is_loading: false,
profile: profile,
});
@@ -81,7 +80,7 @@ function UserPage() {
UpdateCurrentUserInfo(pfp_path).then(() => {
GetCurrentUserInfo().then((info: UserData | undefined) => {
if (info && info.profile_picture_path)
AuthenticationStore.setState({
useAuthenticationStore.setState({
profile_picture_path: info.profile_picture_path,
});
});
@@ -184,6 +183,12 @@ function UserPage() {
<CustomButton
color="error"
text="Отменить"
onClick={() => {
if (profile && profile.email && profile.username) {
set_username(profile.username);
set_email(profile.email);
}
}}
disabled={
!(
profile != null &&