Skip to content

Commit 9539b8c

Browse files
committed
fix(users-teams): membership editor nei form Utenti e Team
I dialog di modifica Utenti e Team mancavano della UI per gestire l'associazione utente↔team, che pero' era gia' coperta dagli endpoint esistenti (/api/teams/:id/members + family). Ora: - Backend: aggiunto GET /api/users/:id/teams (admin-only) — riusa lo stesso DTO MyTeamMembership di /users/me/teams, query non-macro per non dipendere dalla cache sqlx offline. - Frontend Teams: nuovo blocco TeamMembersSection nel dialog di modifica. Lista membri con role (leader/contributor) e remove, riga per aggiungere user disponibili (filtra chi e' gia' membro). - Frontend Users: nuovo blocco UserTeamsSection nel dialog di modifica. Lista team a cui l'utente appartiene con role, indicatore primary e remove, riga per aggiungere a team disponibili (filtra team gia' presenti). Entrambe le sezioni sono definite a livello modulo, non inline (vedi memoria react-no-inline-component-defs). In create mode mostrano un hint "salva prima per gestire membri".
1 parent a42b44b commit 9539b8c

4 files changed

Lines changed: 429 additions & 4 deletions

File tree

vulnerability-manager-frontend/src/pages/Teams.tsx

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,26 @@ import {
66
Alert,
77
Box,
88
Dialog,
9-
DialogActions,
109
DialogContent,
1110
DialogTitle,
11+
IconButton,
12+
MenuItem,
13+
Select,
1214
Snackbar,
15+
TextField,
1316
Typography,
1417
} from '@mui/material';
18+
import DeleteIcon from '@mui/icons-material/DeleteOutline';
1519
import { useTheme } from '@mui/material/styles';
1620
import { GridColDef } from '@mui/x-data-grid';
1721
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
1822
import Button from '@mui/material/Button';
1923

2024
import { teamsApi } from '../api/teams';
2125
import { skillsApi } from '../api/skills';
26+
import { api } from '../api/api';
2227
import { useAuth } from '../contexts/AuthContext';
23-
import { NewTeam, Team } from '../types';
28+
import { NewTeam, Team, TeamMember, User } from '../types';
2429
import {
2530
DataTable,
2631
EntityForm,
@@ -46,6 +51,171 @@ const emptyValues: TeamFormValues = {
4651
skills: [],
4752
};
4853

54+
const MEMBER_ROLES: { value: string; label: string }[] = [
55+
{ value: 'leader', label: 'Leader' },
56+
{ value: 'contributor', label: 'Contributor' },
57+
];
58+
59+
interface TeamMembersSectionProps {
60+
teamId: string;
61+
onNotify: (message: string, severity: 'success' | 'error') => void;
62+
}
63+
64+
// Sezione "Membri" del team mostrata nel dialog di modifica. Definita
65+
// fuori dal componente Teams per evitare unmount/remount degli input.
66+
const TeamMembersSection: React.FC<TeamMembersSectionProps> = ({ teamId, onNotify }) => {
67+
const theme = useTheme();
68+
const queryClient = useQueryClient();
69+
const [newUserId, setNewUserId] = useState<string>('');
70+
const [newRole, setNewRole] = useState<string>('contributor');
71+
72+
const { data: members = [], isLoading: membersLoading } = useQuery({
73+
queryKey: ['team-members', teamId],
74+
queryFn: () => teamsApi.getMembers(teamId),
75+
});
76+
77+
const { data: allUsers = [] } = useQuery<User[]>({
78+
queryKey: ['users-for-membership'],
79+
queryFn: async () => {
80+
const r = await api.get<User[]>('/users');
81+
return r.data;
82+
},
83+
});
84+
85+
const availableUsers = useMemo(() => {
86+
const taken = new Set(members.map((m: TeamMember) => m.user_id).filter(Boolean));
87+
return allUsers.filter((u) => !taken.has(u.id));
88+
}, [allUsers, members]);
89+
90+
const addMutation = useMutation({
91+
mutationFn: () =>
92+
teamsApi.addMember(teamId, {
93+
team_id: teamId,
94+
user_id: newUserId,
95+
name: allUsers.find((u) => u.id === newUserId)?.username ?? '',
96+
email: allUsers.find((u) => u.id === newUserId)?.email ?? '',
97+
role: newRole,
98+
}),
99+
onSuccess: () => {
100+
queryClient.invalidateQueries({ queryKey: ['team-members', teamId] });
101+
queryClient.invalidateQueries({ queryKey: ['teams'] });
102+
setNewUserId('');
103+
setNewRole('contributor');
104+
onNotify('Membro aggiunto', 'success');
105+
},
106+
onError: () => onNotify('Errore aggiunta membro', 'error'),
107+
});
108+
109+
const removeMutation = useMutation({
110+
mutationFn: (memberId: string) => teamsApi.removeMember(teamId, memberId),
111+
onSuccess: () => {
112+
queryClient.invalidateQueries({ queryKey: ['team-members', teamId] });
113+
queryClient.invalidateQueries({ queryKey: ['teams'] });
114+
onNotify('Membro rimosso', 'success');
115+
},
116+
onError: () => onNotify('Errore rimozione membro', 'error'),
117+
});
118+
119+
return (
120+
<Box sx={{ mt: 2, pt: 2, borderTop: `1px solid ${theme.palette.divider}` }}>
121+
<Typography sx={{ fontSize: 12, fontWeight: 600, mb: 1 }}>Membri del team</Typography>
122+
123+
{membersLoading ? (
124+
<Typography sx={{ fontSize: 11, color: theme.palette.text.secondary }}>Caricamento…</Typography>
125+
) : members.length === 0 ? (
126+
<Typography sx={{ fontSize: 11, color: theme.palette.text.secondary, fontStyle: 'italic' }}>
127+
Nessun membro
128+
</Typography>
129+
) : (
130+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mb: 1.5 }}>
131+
{members.map((m: TeamMember) => (
132+
<Box
133+
key={m.id}
134+
sx={{
135+
display: 'flex',
136+
alignItems: 'center',
137+
gap: 1,
138+
px: 1,
139+
py: 0.5,
140+
borderRadius: 0.5,
141+
backgroundColor: theme.palette.action.hover,
142+
}}
143+
>
144+
<Typography sx={{ fontSize: 11, flex: 1 }}>
145+
{m.name} <Typography component="span" sx={{ fontSize: 10, color: theme.palette.text.secondary }}>({m.email})</Typography>
146+
</Typography>
147+
<Typography
148+
sx={{
149+
fontSize: 9,
150+
fontWeight: 600,
151+
textTransform: 'uppercase',
152+
px: 0.75,
153+
py: 0.125,
154+
borderRadius: 0.5,
155+
color: m.role === 'leader' ? theme.severity?.high : theme.palette.text.secondary,
156+
border: `1px solid ${theme.palette.divider}`,
157+
}}
158+
>
159+
{m.role ?? 'contributor'}
160+
</Typography>
161+
<IconButton
162+
size="small"
163+
onClick={() => removeMutation.mutate(m.id)}
164+
disabled={removeMutation.isPending}
165+
aria-label="Rimuovi membro"
166+
>
167+
<DeleteIcon fontSize="small" />
168+
</IconButton>
169+
</Box>
170+
))}
171+
</Box>
172+
)}
173+
174+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
175+
<TextField
176+
select
177+
size="small"
178+
label="Aggiungi utente"
179+
value={newUserId}
180+
onChange={(e) => setNewUserId(e.target.value)}
181+
sx={{ flex: 2 }}
182+
SelectProps={{ MenuProps: { PaperProps: { sx: { maxHeight: 300 } } } }}
183+
>
184+
{availableUsers.length === 0 ? (
185+
<MenuItem value="" disabled>
186+
Tutti gli utenti già membri
187+
</MenuItem>
188+
) : (
189+
availableUsers.map((u) => (
190+
<MenuItem key={u.id} value={u.id}>
191+
{u.username}{u.email}
192+
</MenuItem>
193+
))
194+
)}
195+
</TextField>
196+
<Select
197+
size="small"
198+
value={newRole}
199+
onChange={(e) => setNewRole(e.target.value)}
200+
sx={{ flex: 1, minWidth: 130 }}
201+
>
202+
{MEMBER_ROLES.map((r) => (
203+
<MenuItem key={r.value} value={r.value}>{r.label}</MenuItem>
204+
))}
205+
</Select>
206+
<Button
207+
variant="contained"
208+
size="small"
209+
disabled={!newUserId || addMutation.isPending}
210+
onClick={() => addMutation.mutate()}
211+
>
212+
Aggiungi
213+
</Button>
214+
</Box>
215+
</Box>
216+
);
217+
};
218+
49219
const Teams: React.FC = () => {
50220
const theme = useTheme();
51221
const { user } = useAuth();
@@ -328,6 +498,16 @@ const Teams: React.FC = () => {
328498
submitting={createMutation.isPending || updateMutation.isPending}
329499
showSideNav={false}
330500
/>
501+
{editing ? (
502+
<TeamMembersSection
503+
teamId={editing.id}
504+
onNotify={(message, severity) => setSnackbar({ open: true, message, severity })}
505+
/>
506+
) : (
507+
<Typography sx={{ fontSize: 10, color: theme.palette.text.secondary, mt: 2, fontStyle: 'italic' }}>
508+
Salva il team per poter aggiungere membri.
509+
</Typography>
510+
)}
331511
</DialogContent>
332512
</Dialog>
333513

0 commit comments

Comments
 (0)