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 }