@@ -22,6 +22,8 @@ import {
2222 Input ,
2323 Textarea ,
2424 mergeClasses ,
25+ Radio ,
26+ RadioGroup ,
2527} from '@fluentui/react-components' ;
2628import { AddCircle20Regular , FolderOpen24Regular , Edit20Regular , Checkmark20Regular , Dismiss20Regular } from '@fluentui/react-icons' ;
2729import { useParams , useNavigate , useOutletContext } from 'react-router-dom' ;
@@ -112,7 +114,9 @@ export default function ProjectPage() {
112114 const [ deleteDialogOpen , setDeleteDialogOpen ] = useState ( false ) ;
113115 const [ allUsers , setAllUsers ] = useState < User [ ] > ( [ ] ) ;
114116 const [ selectedUserId , setSelectedUserId ] = useState < string > ( '' ) ;
117+ const [ selectedRole , setSelectedRole ] = useState < string > ( 'Member' ) ;
115118 const [ actionLoading , setActionLoading ] = useState ( false ) ;
119+ const [ addClientDialogOpen , setAddClientDialogOpen ] = useState ( false ) ;
116120 const [ refreshTrigger , setRefreshTrigger ] = useState ( 0 ) ;
117121 const [ isEditingProject , setIsEditingProject ] = useState ( false ) ;
118122 const [ editedName , setEditedName ] = useState ( '' ) ;
@@ -244,8 +248,13 @@ export default function ProjectPage() {
244248 setActionLoading ( true ) ;
245249 try {
246250 await projectsApi . addProjectMembers ( project . id , [ selectedUserId ] ) ;
251+ // Assign role/permission if specified
252+ if ( selectedRole !== 'Member' && selectedRole ) {
253+ await projectsApi . updateProjectMemberPermissions ( project . id , selectedUserId , selectedRole ) ;
254+ }
247255 setInviteDialogOpen ( false ) ;
248256 setSelectedUserId ( '' ) ;
257+ setSelectedRole ( 'Member' ) ;
249258 refreshProject ( ) ;
250259 } catch ( err ) {
251260 console . error ( 'Failed to invite member:' , err ) ;
@@ -255,6 +264,39 @@ export default function ProjectPage() {
255264 }
256265 } ;
257266
267+ const handleAddClient = async ( ) => {
268+ if ( ! selectedUserId || ! project ?. id ) return ;
269+ setActionLoading ( true ) ;
270+ try {
271+ // Add user as client to the project
272+ await projectsApi . addProjectMembers ( project . id , [ selectedUserId ] ) ;
273+ // Set their role as Client
274+ await projectsApi . updateProjectMemberPermissions ( project . id , selectedUserId , 'Client' ) ;
275+ setAddClientDialogOpen ( false ) ;
276+ setSelectedUserId ( '' ) ;
277+ refreshProject ( ) ;
278+ } catch ( err ) {
279+ console . error ( 'Failed to add client:' , err ) ;
280+ alert ( 'Failed to add client' ) ;
281+ } finally {
282+ setActionLoading ( false ) ;
283+ }
284+ } ;
285+
286+ const openAddClientDialog = async ( ) => {
287+ try {
288+ const users = await usersApi . getAllUsers ( ) ;
289+ // Filter out current members and manager
290+ const memberIds = new Set ( [ ...( project ?. teamMembers ?? [ ] ) , project ?. createdBy ] ) ;
291+ const availableUsers = users . filter ( ( u ) => ! memberIds . has ( u . id ) ) ;
292+ setAllUsers ( availableUsers ) ;
293+ setAddClientDialogOpen ( true ) ;
294+ } catch ( err ) {
295+ console . error ( 'Failed to fetch users:' , err ) ;
296+ alert ( 'Failed to load users' ) ;
297+ }
298+ } ;
299+
258300 const handleRemoveMember = async ( memberId : string ) => {
259301 if ( ! project ?. id ) return ;
260302 if ( ! confirm ( 'Remove this member from the project?' ) ) return ;
@@ -500,36 +542,47 @@ export default function ProjectPage() {
500542 </ Table >
501543 ) }
502544 { isOwner && (
503- < div >
545+ < div style = { { display : 'flex' , gap : tokens . spacingHorizontalS } } >
504546 < Button appearance = "secondary" icon = { < AddCircle20Regular /> } onClick = { openInviteDialog } >
505547 Invite Member
506548 </ Button >
549+ < Button appearance = "primary" icon = { < AddCircle20Regular /> } onClick = { openAddClientDialog } >
550+ Add Client
551+ </ Button >
507552 </ div >
508553 ) }
509554 </ section >
510555 </ >
511556 ) }
512557
513558 { /* Invite Member Dialog */ }
514- < Dialog open = { inviteDialogOpen } onOpenChange = { ( _ , data ) => setInviteDialogOpen ( data . open ) } >
559+ < Dialog open = { inviteDialogOpen } onOpenChange = { ( _ , data ) => { setInviteDialogOpen ( data . open ) ; if ( ! data . open ) { setSelectedUserId ( '' ) ; setSelectedRole ( 'Member' ) ; } } } >
515560 < DialogSurface >
516561 < DialogBody >
517562 < DialogTitle > Invite Member to Project</ DialogTitle >
518- < DialogContent >
563+ < DialogContent style = { { display : 'flex' , flexDirection : 'column' , gap : tokens . spacingVerticalM } } >
519564 < Dropdown
520565 placeholder = "Select a user"
521566 value = { allUsers . find ( ( u ) => u . id === selectedUserId ) ?. userName || '' }
522567 onOptionSelect = { ( _ , data ) => setSelectedUserId ( data . optionValue || '' ) }
523568 >
524569 { allUsers . map ( ( user ) => (
525- < Option key = { user . id } value = { user . id } text = { `${ user . userName } (${ user . email } )` } >
526- { user . userName } ({ user . email } )
570+ < Option key = { user . id } value = { user . id } text = { `${ user . firstName } ${ user . lastName } (${ user . email } )` } >
571+ { user . firstName } { user . lastName } ({ user . email } )
527572 </ Option >
528573 ) ) }
529574 </ Dropdown >
575+ < div >
576+ < label style = { { display : 'block' , marginBottom : tokens . spacingVerticalXS , fontWeight : tokens . fontWeightSemibold } } > Role</ label >
577+ < RadioGroup value = { selectedRole } onChange = { ( _ , data ) => setSelectedRole ( data . value ) } >
578+ < Radio value = "Member" label = "Member (View and edit tasks)" />
579+ < Radio value = "Editor" label = "Editor (Full project edit access)" />
580+ < Radio value = "Viewer" label = "Viewer (Read-only access)" />
581+ </ RadioGroup >
582+ </ div >
530583 </ DialogContent >
531584 < DialogActions >
532- < Button appearance = "secondary" onClick = { ( ) => setInviteDialogOpen ( false ) } >
585+ < Button appearance = "secondary" onClick = { ( ) => { setInviteDialogOpen ( false ) ; setSelectedUserId ( '' ) ; setSelectedRole ( 'Member' ) ; } } >
533586 Cancel
534587 </ Button >
535588 < Button appearance = "primary" onClick = { handleInviteMember } disabled = { ! selectedUserId || actionLoading } >
@@ -540,6 +593,41 @@ export default function ProjectPage() {
540593 </ DialogSurface >
541594 </ Dialog >
542595
596+ { /* Add Client Dialog */ }
597+ < Dialog open = { addClientDialogOpen } onOpenChange = { ( _ , data ) => { setAddClientDialogOpen ( data . open ) ; if ( ! data . open ) setSelectedUserId ( '' ) ; } } >
598+ < DialogSurface >
599+ < DialogBody >
600+ < DialogTitle > Add Client to Project</ DialogTitle >
601+ < DialogContent >
602+ < Dropdown
603+ placeholder = "Select a client"
604+ value = { allUsers . find ( ( u ) => u . id === selectedUserId ) ?. userName || '' }
605+ onOptionSelect = { ( _ , data ) => setSelectedUserId ( data . optionValue || '' ) }
606+ >
607+ { allUsers . map ( ( user ) => (
608+ < Option key = { user . id } value = { user . id } text = { `${ user . firstName } ${ user . lastName } (${ user . email } )` } >
609+ { user . firstName } { user . lastName } ({ user . email } )
610+ </ Option >
611+ ) ) }
612+ </ Dropdown >
613+ < div style = { { marginTop : tokens . spacingVerticalM , padding : tokens . spacingVerticalS , backgroundColor : tokens . colorNeutralBackground3 , borderRadius : tokens . borderRadiusMedium } } >
614+ < p style = { { margin : 0 , fontSize : tokens . fontSizeBase200 , color : tokens . colorNeutralForeground2 } } >
615+ Clients will have limited access and can only view analytics for this project.
616+ </ p >
617+ </ div >
618+ </ DialogContent >
619+ < DialogActions >
620+ < Button appearance = "secondary" onClick = { ( ) => { setAddClientDialogOpen ( false ) ; setSelectedUserId ( '' ) ; } } >
621+ Cancel
622+ </ Button >
623+ < Button appearance = "primary" onClick = { handleAddClient } disabled = { ! selectedUserId || actionLoading } >
624+ { actionLoading ? 'Adding...' : 'Add Client' }
625+ </ Button >
626+ </ DialogActions >
627+ </ DialogBody >
628+ </ DialogSurface >
629+ </ Dialog >
630+
543631 { /* Delete Project Dialog */ }
544632 < Dialog open = { deleteDialogOpen } onOpenChange = { ( _ , data ) => setDeleteDialogOpen ( data . open ) } >
545633 < DialogSurface >
0 commit comments