164164 </CardContent >
165165 </Card >
166166
167+ <!-- Pending Team Invitations -->
168+ <Card
169+ v-if =" currentTeam && !isDefaultTeam(currentTeam) && (isLoadingInvitations || pendingInvitations.length > 0)"
170+ data-testid =" team-pending-invitations"
171+ >
172+ <CardHeader >
173+ <CardTitle class =" flex items-center gap-2" >
174+ <Icon name =" lucide:mail" class =" h-5 w-5" />
175+ Pending Invites
176+ </CardTitle >
177+ <CardDescription
178+ >Outstanding invitations for {{ currentTeam.name }} that have not yet been accepted.</CardDescription
179+ >
180+ </CardHeader >
181+ <CardContent >
182+ <div v-if =" isLoadingInvitations" class =" space-y-2" >
183+ <Skeleton class =" h-14 w-full" />
184+ <Skeleton class =" h-14 w-full" />
185+ </div >
186+ <div v-else class =" space-y-2" >
187+ <div
188+ v-for =" invitation in pendingInvitations"
189+ :key =" invitation.id"
190+ data-testid =" team-pending-invitation-row"
191+ class =" flex items-center justify-between rounded-lg border p-3"
192+ >
193+ <div class =" min-w-0" >
194+ <p class =" font-medium truncate" >{{ invitation.email }}</p >
195+ <p class =" text-xs text-muted-foreground" >
196+ Invited {{ formatDate(invitation.createdAt) }} · ; Expires {{ formatDate(invitation.expiresAt) }}
197+ </p >
198+ </div >
199+ <div class =" flex items-center gap-2 shrink-0" >
200+ <Button
201+ variant =" outline"
202+ size =" sm"
203+ :disabled =" resendingInvitationId === invitation.id || cancellingInvitationId === invitation.id"
204+ @click =" resendInvitation(invitation)"
205+ >
206+ <Icon
207+ v-if =" resendingInvitationId === invitation.id"
208+ name =" lucide:loader-2"
209+ class =" h-4 w-4 animate-spin"
210+ />
211+ <span v-else >Send reminder</span >
212+ </Button >
213+ <Button
214+ variant =" ghost"
215+ size =" sm"
216+ class =" text-red-600 hover:text-red-700"
217+ :disabled =" cancellingInvitationId === invitation.id || resendingInvitationId === invitation.id"
218+ @click =" cancelInvitation(invitation.id)"
219+ >
220+ <Icon
221+ v-if =" cancellingInvitationId === invitation.id"
222+ name =" lucide:loader-2"
223+ class =" h-4 w-4 animate-spin"
224+ />
225+ <span v-else >Revoke</span >
226+ </Button >
227+ </div >
228+ </div >
229+ </div >
230+ </CardContent >
231+ </Card >
232+
167233 <!-- Danger Zone -->
168234 <Card v-if =" currentTeam && !isDefaultTeam(currentTeam)" class =" border-red-200" >
169235 <CardHeader >
235301 </div >
236302 <DialogFooter >
237303 <Button variant =" outline" @click =" showInviteDialog = false" >Cancel</Button >
238- <Button :disabled =" isInviting || !inviteEmail.trim() " @click =" inviteToTeam" >
304+ <Button :disabled =" isInviting || !isValidInviteEmail " @click =" inviteToTeam" >
239305 <Icon v-if =" isInviting" name =" lucide:loader-2" class =" h-4 w-4 mr-2 animate-spin" />
240306 Send Invite
241307 </Button >
@@ -287,13 +353,17 @@ export default {
287353 teamName: ' ' ,
288354 teamSlug: ' ' ,
289355 teamMembers: [],
356+ pendingInvitations: [],
290357 isLoading: true ,
291358 isLoadingMembers: false ,
359+ isLoadingInvitations: false ,
292360 isSavingTeam: false ,
293361 isCreatingTeam: false ,
294362 isInviting: false ,
295363 isDeletingTeam: false ,
296364 switchingTeamId: null ,
365+ cancellingInvitationId: null ,
366+ resendingInvitationId: null ,
297367 showCreateTeamDialog: false ,
298368 showInviteDialog: false ,
299369 showDeleteTeamDialog: false ,
@@ -318,6 +388,9 @@ export default {
318388 (this .teamSlug .trim () && this .teamSlug .trim () !== this .currentTeam ? .slug )
319389 )
320390 },
391+ isValidInviteEmail () {
392+ return / ^ [^ \s @] + @[^ \s @] + \. [^ \s @] + $ / .test (this .inviteEmail .trim ())
393+ },
321394 additionalTeams () {
322395 return this .allTeams .filter ((t ) => ! this .isDefaultTeam (t))
323396 },
@@ -397,7 +470,7 @@ export default {
397470 async handleActiveTeamChanged () {
398471 await this .syncActiveTeamContext ()
399472 this .allTeams = await this .fetchAllTeams ()
400- await this .loadTeamMembers ()
473+ await Promise . all ([ this .loadTeamMembers (), this . loadPendingInvitations ()] )
401474 },
402475
403476 async resolveOrganizationId () {
@@ -447,7 +520,7 @@ export default {
447520 this .allTeams = allTeams
448521 this .organizations = organizations
449522
450- await this .loadTeamMembers ()
523+ await Promise . all ([ this .loadTeamMembers (), this . loadPendingInvitations ()] )
451524 } catch (error) {
452525 console .error (' Error during team settings initialization:' , error)
453526 toast .error (' Failed to load team settings' )
@@ -489,6 +562,40 @@ export default {
489562 }
490563 },
491564
565+ async resolveOrganizationSlug () {
566+ if (this .activeOrganization ? .slug ) {
567+ return this .activeOrganization .slug
568+ }
569+
570+ const organization = this .organizations .find ((entry ) => entry .id === this .currentTeam ? .organizationId )
571+ return organization? .slug || null
572+ },
573+
574+ async loadPendingInvitations () {
575+ if (! this .currentTeam ? .id || this .isDefaultTeam (this .currentTeam )) {
576+ this .pendingInvitations = []
577+ return
578+ }
579+
580+ const organizationSlug = await this .resolveOrganizationSlug ()
581+ if (! organizationSlug) {
582+ this .pendingInvitations = []
583+ return
584+ }
585+
586+ try {
587+ this .isLoadingInvitations = true
588+ const response = await $fetch (` /api/orgs/${ organizationSlug} /invitations` )
589+ const invitations = response? .data || []
590+ this .pendingInvitations = invitations .filter ((invitation ) => invitation .teamId === this .currentTeam .id )
591+ } catch (error) {
592+ console .error (' Error loading pending team invitations:' , error)
593+ this .pendingInvitations = []
594+ } finally {
595+ this .isLoadingInvitations = false
596+ }
597+ },
598+
492599 async switchToTeam (team ) {
493600 if (team .id === this .currentTeam ? .id || this .switchingTeamId ) return
494601
@@ -515,7 +622,7 @@ export default {
515622 this .activeOrganization = await this .fetchActiveOrganization ()
516623 }
517624
518- await this .loadTeamMembers ()
625+ await Promise . all ([ this .loadTeamMembers (), this . loadPendingInvitations ()] )
519626
520627 // Notify other components (sidebar TeamSwitcher, pages)
521628 if (import .meta.client) {
@@ -610,7 +717,7 @@ export default {
610717
611718 await this .syncActiveTeamContext ()
612719 this .allTeams = await this .fetchAllTeams ()
613- await this .loadTeamMembers ()
720+ await Promise . all ([ this .loadTeamMembers (), this . loadPendingInvitations ()] )
614721
615722 // Notify other components
616723 if (import .meta.client) {
@@ -656,6 +763,7 @@ export default {
656763 })
657764 this .showInviteDialog = false
658765 this .inviteEmail = ' '
766+ await this .loadPendingInvitations ()
659767 toast .success (' Invitation sent' )
660768 } catch (error) {
661769 console .error (' Error inviting team member:' , error)
@@ -668,6 +776,50 @@ export default {
668776 }
669777 },
670778
779+ async resendInvitation (invitation ) {
780+ const organizationId = await this .resolveOrganizationId ()
781+ if (! organizationId || ! this .currentTeam ? .id ) {
782+ toast .error (' No active workspace context' )
783+ return
784+ }
785+
786+ try {
787+ this .resendingInvitationId = invitation .id
788+ await $fetch (' /api/organizations/invite' , {
789+ method: ' POST' ,
790+ body: {
791+ organizationId,
792+ email: invitation .email ,
793+ role: invitation .role ,
794+ teamId: this .currentTeam .id ,
795+ resend: true ,
796+ },
797+ })
798+ await this .loadPendingInvitations ()
799+ toast .success (' Invitation reminder sent' )
800+ } catch (error) {
801+ console .error (' Error resending team invitation:' , error)
802+ const message = error? .data ? .statusMessage || error? .data ? .error ? .message || ' Failed to resend invitation'
803+ toast .error (message)
804+ } finally {
805+ this .resendingInvitationId = null
806+ }
807+ },
808+
809+ async cancelInvitation (invitationId ) {
810+ try {
811+ this .cancellingInvitationId = invitationId
812+ await authClient .organization .cancelInvitation ({ invitationId })
813+ this .pendingInvitations = this .pendingInvitations .filter ((invitation ) => invitation .id !== invitationId)
814+ toast .success (' Invitation revoked' )
815+ } catch (error) {
816+ console .error (' Error cancelling team invitation:' , error)
817+ toast .error (' Failed to revoke invitation' )
818+ } finally {
819+ this .cancellingInvitationId = null
820+ }
821+ },
822+
671823 async removeTeamMember (userId ) {
672824 if (! this .currentTeam ? .id ) return
673825 try {
@@ -696,7 +848,7 @@ export default {
696848 this .allTeams = this .allTeams .filter ((t ) => t .id !== deletedTeamId)
697849
698850 await this .syncActiveTeamContext ()
699- await this .loadTeamMembers ()
851+ await Promise . all ([ this .loadTeamMembers (), this . loadPendingInvitations ()] )
700852
701853 // Notify other components
702854 if (import .meta.client) {
0 commit comments