/ frontend / src / app / views / teams / Teams.jsx
Teams.jsx
  1  import { useState, useEffect } from "react";
  2  import { Box, Button, Chip, IconButton, Tooltip, Typography, styled } from "@mui/material";
  3  import { Add, Edit, Delete, Visibility, Groups } from "@mui/icons-material";
  4  import { useNavigate } from "react-router-dom";
  5  import { toast } from "react-toastify";
  6  import useAuth from "app/hooks/useAuth";
  7  import Breadcrumb from "app/components/Breadcrumb";
  8  import { useTranslation } from "react-i18next";
  9  import DataList from "app/components/DataList";
 10  import api from "app/utils/api";
 11  import { colors, rgba } from "app/utils/themeColors";
 12  
 13  const Container = styled("div")(({ theme }) => ({
 14    margin: "24px 48px",
 15    [theme.breakpoints.down("md")]: { margin: "24px 32px" },
 16    [theme.breakpoints.down("sm")]: { margin: 16 },
 17    "& .breadcrumb": { marginBottom: 24 },
 18  }));
 19  
 20  export default function Teams() {
 21    const { t } = useTranslation();
 22    const [teams, setTeams] = useState([]);
 23    const { user } = useAuth();
 24    const navigate = useNavigate();
 25    const isAdmin = user?.is_admin;
 26  
 27    const fetchTeams = async () => {
 28      try {
 29        const data = await api.get("/teams", user.token);
 30        setTeams(data.teams || []);
 31      } catch (e) { /* toasted */ }
 32    };
 33  
 34    useEffect(() => {
 35      document.title = `${process.env.REACT_APP_RESTAI_NAME || "RESTai"} - Teams`;
 36      fetchTeams();
 37    }, []);
 38  
 39    const userRole = (team) => {
 40      if (isAdmin) return "platform_admin";
 41      if ((team.admins || []).some((a) => a.username === user.username)) return "team_admin";
 42      return "member";
 43    };
 44  
 45    const handleDelete = (e, team) => {
 46      e.stopPropagation();
 47      if (!window.confirm(t("teams.deleteConfirm", { name: team.name }))) return;
 48      api.delete(`/teams/${team.id}`, user.token)
 49        .then(() => {
 50          toast.success(t("teams.deleted"));
 51          fetchTeams();
 52        })
 53        .catch(() => {});
 54    };
 55  
 56    const columns = [
 57      {
 58        key: "name",
 59        label: t("teams.columns.name"),
 60        sortable: true,
 61        render: (row) => (
 62          <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
 63            <Box
 64              sx={{
 65                width: 32, height: 32, borderRadius: "8px",
 66                display: "flex", alignItems: "center", justifyContent: "center",
 67                background: (theme) => theme.palette.mode === "dark" ? "rgba(99,102,241,0.15)" : "rgba(99,102,241,0.08)",
 68              }}
 69            >
 70              <Groups sx={{ fontSize: 18, color: "primary.main" }} />
 71            </Box>
 72            <Box sx={{ fontWeight: 500 }}>{row.name}</Box>
 73          </Box>
 74        ),
 75      },
 76      {
 77        key: "description",
 78        label: t("teams.columns.description"),
 79        render: (row) => (
 80          <Typography
 81            variant="body2"
 82            color="text.secondary"
 83            sx={{ maxWidth: 280, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
 84          >
 85            {row.description || "—"}
 86          </Typography>
 87        ),
 88      },
 89      {
 90        key: "users",
 91        label: t("teams.columns.users"),
 92        sortable: true,
 93        sortValue: (row) => (row.users || []).length,
 94        render: (row) => (row.users || []).length,
 95      },
 96      {
 97        key: "projects",
 98        label: t("teams.columns.projects"),
 99        sortable: true,
100        sortValue: (row) => (row.projects || []).length,
101        render: (row) => (row.projects || []).length,
102      },
103      {
104        key: "role",
105        label: t("teams.role.label"),
106        sortable: true,
107        sortValue: (row) => userRole(row),
108        render: (row) => {
109          const role = userRole(row);
110          const styles = {
111            platform_admin: { label: t("teams.role.platformAdmin"), color: colors.role.platformAdmin, bg: rgba.platformAdmin },
112            team_admin: { label: t("teams.role.teamAdmin"), color: colors.role.teamAdmin, bg: rgba.teamAdmin },
113            member: { label: t("teams.role.member"), color: colors.role.member, bg: rgba.member },
114          };
115          const s = styles[role];
116          return (
117            <Chip
118              label={s.label}
119              size="small"
120              sx={{ backgroundColor: s.bg, color: s.color, fontWeight: 600, fontSize: "0.7rem", height: 22 }}
121            />
122          );
123        },
124      },
125    ];
126  
127    return (
128      <Container>
129        <Box className="breadcrumb">
130          <Breadcrumb routeSegments={[{ name: t("nav.teams"), path: "/teams" }]} />
131        </Box>
132  
133        <DataList
134          title={t("teams.title")}
135          subtitle={t("teams.subtitle")}
136          data={teams}
137          columns={columns}
138          searchKeys={["name", "description"]}
139          onRowClick={(row) => navigate(`/team/${row.id}`)}
140          rowKey={(row) => row.id}
141          defaultSort={{ key: "name", direction: "asc" }}
142          headerAction={
143            isAdmin && (
144              <Button variant="contained" startIcon={<Add />} onClick={() => navigate("/teams/new")}>
145                {t("teams.new")}
146              </Button>
147            )
148          }
149          actions={(row) => {
150            const role = userRole(row);
151            const canEdit = role !== "member";
152            return (
153              <>
154                <Tooltip title={t("teams.actions.view")}>
155                  <IconButton size="small" onClick={() => navigate(`/team/${row.id}`)}>
156                    <Visibility fontSize="small" />
157                  </IconButton>
158                </Tooltip>
159                {canEdit && (
160                  <Tooltip title={t("teams.actions.edit")}>
161                    <IconButton size="small" onClick={() => navigate(`/team/${row.id}/edit`)}>
162                      <Edit fontSize="small" />
163                    </IconButton>
164                  </Tooltip>
165                )}
166                {isAdmin && (
167                  <Tooltip title={t("teams.actions.delete")}>
168                    <IconButton size="small" color="error" onClick={(e) => handleDelete(e, row)}>
169                      <Delete fontSize="small" />
170                    </IconButton>
171                  </Tooltip>
172                )}
173              </>
174            );
175          }}
176          emptyState={{
177            icon: Groups,
178            title: t("teams.emptyTitle"),
179            message: t("teams.emptyMessage"),
180            actionLabel: isAdmin ? t("teams.new") : undefined,
181            actionIcon: <Add fontSize="small" />,
182            onAction: isAdmin ? () => navigate("/teams/new") : undefined,
183          }}
184          emptyMessage={t("teams.noTeams")}
185        />
186      </Container>
187    );
188  }