@@ -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' ;
1519import { useTheme } from '@mui/material/styles' ;
1620import { GridColDef } from '@mui/x-data-grid' ;
1721import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query' ;
1822import Button from '@mui/material/Button' ;
1923
2024import { teamsApi } from '../api/teams' ;
2125import { skillsApi } from '../api/skills' ;
26+ import { api } from '../api/api' ;
2227import { useAuth } from '../contexts/AuthContext' ;
23- import { NewTeam , Team } from '../types' ;
28+ import { NewTeam , Team , TeamMember , User } from '../types' ;
2429import {
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+
49219const 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