1- import { useState } from "react" ;
1+ import { useRef , useState } from "react" ;
22import { DialogHeader , DialogTitle , DialogDescription } from "@/components/ui/dialog" ;
33import { Button } from "@/components/ui/button" ;
44import { Badge } from "@/components/ui/badge" ;
55import { Input } from "@/components/ui/input" ;
6- import { UserPlus , AlertTriangle , Crown , User , Pencil , Check , X , Loader2 } from "lucide-react" ;
6+ import {
7+ UserPlus ,
8+ AlertTriangle ,
9+ Crown ,
10+ User ,
11+ Pencil ,
12+ Check ,
13+ X ,
14+ Loader2 ,
15+ CreditCard ,
16+ Users
17+ } from "lucide-react" ;
718import { TeamInviteDialog } from "./TeamInviteDialog" ;
819import { TeamMembersList } from "./TeamMembersList" ;
920import { getBillingService } from "@/billing/billingService" ;
21+ import { openBillingPortal } from "@/billing/billingPortal" ;
22+ import { useLocalState } from "@/state/useLocalState" ;
23+ import {
24+ formatTeamSeatMismatchMessage ,
25+ getTeamSeatCounts ,
26+ getTeamSeatMismatch
27+ } from "@/utils/teamSeats" ;
1028import { useQueryClient } from "@tanstack/react-query" ;
1129import type { TeamStatus } from "@/types/team" ;
1230
@@ -20,7 +38,11 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
2038 const [ editedName , setEditedName ] = useState ( "" ) ;
2139 const [ isSavingName , setIsSavingName ] = useState ( false ) ;
2240 const [ nameError , setNameError ] = useState < string | null > ( null ) ;
41+ const [ isPortalLoading , setIsPortalLoading ] = useState ( false ) ;
42+ const [ portalError , setPortalError ] = useState < string | null > ( null ) ;
43+ const membersSectionRef = useRef < HTMLDivElement > ( null ) ;
2344 const queryClient = useQueryClient ( ) ;
45+ const { billingStatus } = useLocalState ( ) ;
2446
2547 if ( ! teamStatus ) {
2648 return (
@@ -33,10 +55,15 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
3355 ) ;
3456 }
3557
36- const seatsUsed = teamStatus . seats_used || 0 ;
37- const seatsPurchased = teamStatus . seats_purchased || 0 ;
58+ const seatCounts = getTeamSeatCounts ( teamStatus ) ;
59+ const seatMismatch = getTeamSeatMismatch ( teamStatus ) ;
60+ const seatsUsed = seatCounts . memberCount ?? 0 ;
61+ const seatsPurchased = seatCounts . billedSeatCount ?? 0 ;
3862 const isAdmin = teamStatus . role === "admin" || teamStatus . is_team_admin === true ;
3963 const seatUsagePercentage = seatsPurchased > 0 ? ( seatsUsed / seatsPurchased ) * 100 : 0 ;
64+ const canOpenBillingPortal = billingStatus
65+ ? ! ! billingStatus . stripe_customer_id
66+ : ! ! teamStatus . has_team_subscription ;
4067
4168 const handleStartEdit = ( ) => {
4269 setEditedName ( teamStatus . team_name || "" ) ;
@@ -90,6 +117,29 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
90117 }
91118 } ;
92119
120+ const handleOpenBilling = async ( ) => {
121+ if ( ! canOpenBillingPortal ) return ;
122+
123+ try {
124+ setPortalError ( null ) ;
125+ setIsPortalLoading ( true ) ;
126+ await openBillingPortal ( ) ;
127+ await queryClient . invalidateQueries ( { queryKey : [ "billingStatus" ] } ) ;
128+ await queryClient . invalidateQueries ( { queryKey : [ "teamStatus" ] } ) ;
129+ } catch ( error ) {
130+ console . error ( "Failed to open billing portal:" , error ) ;
131+ setPortalError (
132+ "Unable to open subscription management. Please try again or contact support@trymaple.ai."
133+ ) ;
134+ } finally {
135+ setIsPortalLoading ( false ) ;
136+ }
137+ } ;
138+
139+ const handleReviewMembers = ( ) => {
140+ membersSectionRef . current ?. scrollIntoView ( { block : "nearest" } ) ;
141+ } ;
142+
93143 // Simplified view for non-admin members
94144 if ( ! isAdmin ) {
95145 return (
@@ -99,6 +149,20 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
99149 </ DialogHeader >
100150
101151 < div className = "mt-3 space-y-3 overflow-hidden" >
152+ { seatMismatch && (
153+ < div className = "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-destructive" >
154+ < div className = "flex items-start gap-2" >
155+ < AlertTriangle className = "mt-0.5 h-4 w-4 flex-shrink-0" />
156+ < div className = "space-y-1" >
157+ < p className = "text-sm font-medium" > Team usage is paused</ p >
158+ < p className = "text-xs leading-relaxed" >
159+ { formatTeamSeatMismatchMessage ( seatMismatch , "member" ) }
160+ </ p >
161+ </ div >
162+ </ div >
163+ </ div >
164+ ) }
165+
102166 { /* Compact team info */ }
103167 < div className = "bg-muted/50 rounded-lg p-3" >
104168 < div className = "flex items-center justify-between gap-2" >
@@ -137,11 +201,45 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
137201 </ DialogHeader >
138202
139203 < div className = "mt-3 space-y-3 overflow-hidden" >
140- { /* Seat limit exceeded warning */ }
141- { teamStatus . seat_limit_exceeded && (
142- < div className = "rounded-md bg-destructive/10 p-2.5 text-destructive text-xs flex items-start gap-2" >
143- < AlertTriangle className = "h-3.5 w-3.5 mt-0.5 flex-shrink-0" />
144- < span > Seat limit exceeded. Remove members or purchase additional seats.</ span >
204+ { seatMismatch && (
205+ < div className = "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-destructive" >
206+ < div className = "flex items-start gap-2" >
207+ < AlertTriangle className = "mt-0.5 h-4 w-4 flex-shrink-0" />
208+ < div className = "min-w-0 flex-1 space-y-2" >
209+ < div >
210+ < p className = "text-sm font-medium" > Team usage is paused</ p >
211+ < p className = "mt-1 text-xs leading-relaxed" >
212+ { formatTeamSeatMismatchMessage ( seatMismatch , "admin" ) }
213+ </ p >
214+ </ div >
215+ < div className = "flex flex-col gap-2 sm:flex-row" >
216+ < Button
217+ type = "button"
218+ variant = "outline"
219+ size = "sm"
220+ className = "h-8 border-destructive/30 text-destructive hover:text-destructive"
221+ onClick = { handleReviewMembers }
222+ >
223+ < Users className = "mr-1.5 h-3.5 w-3.5" />
224+ Manage Members
225+ </ Button >
226+ { canOpenBillingPortal && (
227+ < Button
228+ type = "button"
229+ variant = "outline"
230+ size = "sm"
231+ className = "h-8 border-destructive/30 text-destructive hover:text-destructive"
232+ onClick = { handleOpenBilling }
233+ disabled = { isPortalLoading }
234+ >
235+ < CreditCard className = "mr-1.5 h-3.5 w-3.5" />
236+ { isPortalLoading ? "Opening..." : "Add Seats" }
237+ </ Button >
238+ ) }
239+ </ div >
240+ { portalError && < p className = "text-xs leading-relaxed" > { portalError } </ p > }
241+ </ div >
242+ </ div >
145243 </ div >
146244 ) }
147245
@@ -233,7 +331,11 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
233331 </ div >
234332 < div className = "h-2 w-full overflow-hidden rounded-full bg-muted" >
235333 < div
236- className = "h-full transition-all bg-emerald-500"
334+ className = {
335+ seatMismatch
336+ ? "h-full bg-destructive transition-all"
337+ : "h-full bg-emerald-500 transition-all"
338+ }
237339 style = { {
238340 width : `${ Math . min ( seatUsagePercentage , 100 ) } %`
239341 } }
@@ -246,7 +348,7 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
246348 { isAdmin && (
247349 < Button
248350 onClick = { ( ) => setIsInviteDialogOpen ( true ) }
249- disabled = { teamStatus . seat_limit_exceeded }
351+ disabled = { ! ! seatMismatch }
250352 size = "sm"
251353 className = "w-full"
252354 >
@@ -256,7 +358,9 @@ export function TeamDashboard({ teamStatus }: TeamDashboardProps) {
256358 ) }
257359
258360 { /* Members list */ }
259- < TeamMembersList teamStatus = { teamStatus } />
361+ < div ref = { membersSectionRef } >
362+ < TeamMembersList teamStatus = { teamStatus } />
363+ </ div >
260364 </ div >
261365
262366 { /* Invite dialog */ }
0 commit comments