diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/index.ts
index 06729ecb88..91e4106026 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/index.ts
@@ -1,7 +1,11 @@
-export { MemberInvitationCard } from './member-invitation-card/member-invitation-card'
-export { NoOrganizationView } from './no-organization-view/no-organization-view'
-export { RemoveMemberDialog } from './remove-member-dialog/remove-member-dialog'
-export { TeamMembers } from './team-members/team-members'
-export { TeamSeats } from './team-seats/team-seats'
-export { TeamSeatsOverview } from './team-seats-overview/team-seats-overview'
-export { TeamUsage } from './team-usage/team-usage'
+export { MemberInvitationCard } from './member-invitation-card'
+export { MemberLimit } from './member-limit'
+export { NoOrganizationView } from './no-organization-view'
+export { OrganizationCreationDialog } from './organization-creation-dialog'
+export { OrganizationSettingsTab } from './organization-settings-tab'
+export { PendingInvitationsList } from './pending-invitations-list'
+export { RemoveMemberDialog } from './remove-member-dialog'
+export { TeamMembersList } from './team-members-list'
+export { TeamSeats } from './team-seats'
+export { TeamSeatsOverview } from './team-seats-overview'
+export { TeamUsage } from './team-usage'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/index.ts
new file mode 100644
index 0000000000..2a0bb699f6
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/index.ts
@@ -0,0 +1 @@
+export { MemberInvitationCard } from './member-invitation-card'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx
index 6e17e2bc24..a30e59dddd 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx
@@ -1,12 +1,12 @@
-import React, { useMemo, useState } from 'react'
-import { CheckCircle } from 'lucide-react'
+import React, { useMemo } from 'react'
+import { CheckCircle, ChevronDown, PlusCircle } from 'lucide-react'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
-import { quickValidateEmail } from '@/lib/email/validation'
import { cn } from '@/lib/utils'
type PermissionType = 'read' | 'write' | 'admin'
@@ -31,7 +31,10 @@ const PermissionSelector = React.memo(
return (
{permissionOptions.map((option, index) => (
(
disabled={disabled}
title={option.description}
className={cn(
- 'px-2.5 py-1.5 font-medium text-xs transition-colors focus:outline-none',
- 'first:rounded-l-[11px] last:rounded-r-[11px]',
+ 'relative px-3 py-1.5 font-medium text-sm transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
disabled && 'cursor-not-allowed opacity-50',
value === option.value
- ? 'bg-foreground text-background'
- : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',
+ ? 'z-10 bg-primary text-primary-foreground'
+ : 'text-muted-foreground hover:z-20 hover:bg-muted/50 hover:text-foreground',
index > 0 && 'border-input border-l'
)}
>
@@ -72,8 +74,6 @@ interface MemberInvitationCardProps {
onLoadUserWorkspaces: () => Promise
onWorkspaceToggle: (workspaceId: string, permission: string) => void
inviteSuccess: boolean
- availableSeats?: number
- maxSeats?: number
}
function ButtonSkeleton() {
@@ -94,137 +94,106 @@ export function MemberInvitationCard({
onLoadUserWorkspaces,
onWorkspaceToggle,
inviteSuccess,
- availableSeats = 0,
- maxSeats = 0,
}: MemberInvitationCardProps) {
const selectedCount = selectedWorkspaces.length
- const hasAvailableSeats = availableSeats > 0
- const [emailError, setEmailError] = useState('')
-
- // Email validation function using existing lib
- const validateEmailInput = (email: string) => {
- if (!email.trim()) {
- setEmailError('')
- return
- }
-
- const validation = quickValidateEmail(email.trim())
- if (!validation.isValid) {
- setEmailError(validation.reason || 'Please enter a valid email address')
- } else {
- setEmailError('')
- }
- }
-
- const handleEmailChange = (e: React.ChangeEvent) => {
- const value = e.target.value
- setInviteEmail(value)
- // Clear error when user starts typing again
- if (emailError) {
- setEmailError('')
- }
- }
-
- const handleInviteClick = () => {
- // Validate email before proceeding
- if (inviteEmail.trim()) {
- validateEmailInput(inviteEmail)
- const validation = quickValidateEmail(inviteEmail.trim())
- if (!validation.isValid) {
- return // Don't proceed if validation fails
- }
- }
-
- // If validation passes or email is empty, proceed with original invite
- onInviteMember()
- }
return (
-
- {/* Header - clean like account page */}
-
-
Invite Team Members
-
+
+
+ Invite Team Members
+
Add new members to your team and optionally give them access to specific workspaces
-
-
-
- {/* Main invitation input - clean layout */}
-
-
-
+
+
+
+
+
setInviteEmail(e.target.value)}
+ disabled={isInviting}
+ className='w-full'
/>
-
- {emailError &&
{emailError}
}
-
-
- {
- setShowWorkspaceInvite(!showWorkspaceInvite)
- if (!showWorkspaceInvite) {
- onLoadUserWorkspaces()
- }
- }}
- disabled={isInviting || !hasAvailableSeats}
- className='h-9 shrink-0 rounded-[8px] text-sm'
- >
- {showWorkspaceInvite ? 'Hide' : 'Add'} Workspaces
-
-
- {isInviting ? : null}
- {hasAvailableSeats ? 'Invite' : 'No Seats'}
-
-
-
- {showWorkspaceInvite && (
-
-
-
-
Workspace Access
-
- Optional
-
-
+
{
+ setShowWorkspaceInvite(!showWorkspaceInvite)
+ if (!showWorkspaceInvite) {
+ onLoadUserWorkspaces()
+ }
+ }}
+ disabled={isInviting}
+ className='h-9 shrink-0 gap-1 rounded-[8px]'
+ >
+ {showWorkspaceInvite ? 'Hide' : 'Add'} Workspaces
{selectedCount > 0 && (
- {selectedCount} selected
+
+ {selectedCount}
+
)}
-
-
- Grant access to specific workspaces. You can modify permissions later.
-
+
+
+
+ {isInviting ? : }
+ Invite
+
+
- {userWorkspaces.length === 0 ? (
-
-
No workspaces available
-
- You need admin access to workspaces to invite members
-
+ {showWorkspaceInvite && (
+
+
+
+
Workspace Access
+
+ Optional
+
+
+ {selectedCount > 0 && (
+
{selectedCount} selected
+ )}
- ) : (
-
- {userWorkspaces.map((workspace) => {
- const isSelected = selectedWorkspaces.some((w) => w.workspaceId === workspace.id)
- const selectedWorkspace = selectedWorkspaces.find(
- (w) => w.workspaceId === workspace.id
- )
+
+ Grant access to specific workspaces. You can modify permissions later.
+
- return (
-
-
+ {userWorkspaces.length === 0 ? (
+
+
No workspaces available
+
+ You need admin access to workspaces to invite members
+
+
+ ) : (
+
+ {userWorkspaces.map((workspace) => {
+ const isSelected = selectedWorkspaces.some((w) => w.workspaceId === workspace.id)
+ const selectedWorkspace = selectedWorkspaces.find(
+ (w) => w.workspaceId === workspace.id
+ )
+
+ return (
+
{workspace.name}
@@ -253,43 +222,42 @@ export function MemberInvitationCard({
)}
-
- {/* Always reserve space for permission selector to maintain consistent layout */}
-
{isSelected && (
-
onWorkspaceToggle(workspace.id, permission)}
- disabled={isInviting}
- className='w-auto'
- />
+
+
onWorkspaceToggle(workspace.id, permission)}
+ disabled={isInviting}
+ className='h-8'
+ />
+
)}
-
- )
- })}
-
- )}
-
- )}
+ )
+ })}
+
+ )}
+
+ )}
- {inviteSuccess && (
-
-
-
- Invitation sent successfully
- {selectedCount > 0 &&
- ` with access to ${selectedCount} workspace${selectedCount !== 1 ? 's' : ''}`}
-
-
- )}
-
+ {inviteSuccess && (
+
+
+
+ Invitation sent successfully
+ {selectedCount > 0 &&
+ ` with access to ${selectedCount} workspace${selectedCount !== 1 ? 's' : ''}`}
+
+
+ )}
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-limit/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-limit/index.ts
new file mode 100644
index 0000000000..f09d182a32
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-limit/index.ts
@@ -0,0 +1 @@
+export { MemberLimit } from './member-limit'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-limit/member-limit.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-limit/member-limit.tsx
new file mode 100644
index 0000000000..d78038fc67
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-limit/member-limit.tsx
@@ -0,0 +1,244 @@
+import { useEffect, useState } from 'react'
+import { AlertTriangle, DollarSign, User } from 'lucide-react'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+
+interface MemberLimitProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ member: {
+ userId: string
+ userName: string
+ userEmail: string
+ currentUsage: number
+ usageLimit: number
+ role: string
+ } | null
+ onSave: (userId: string, newLimit: number) => Promise
+ isLoading: boolean
+ planType?: string
+}
+
+export function MemberLimit({
+ open,
+ onOpenChange,
+ member,
+ onSave,
+ isLoading,
+ planType = 'team',
+}: MemberLimitProps) {
+ const [limitValue, setLimitValue] = useState('')
+ const [error, setError] = useState(null)
+
+ // Update limit value when member changes
+ useEffect(() => {
+ if (member) {
+ setLimitValue(member.usageLimit.toString())
+ setError(null)
+ }
+ }, [member])
+
+ // Get plan minimum based on plan type
+ const getPlanMinimum = (plan: string): number => {
+ switch (plan) {
+ case 'pro':
+ return 20
+ case 'team':
+ return 40
+ case 'enterprise':
+ return 100
+ default:
+ return 5
+ }
+ }
+
+ const planMinimum = getPlanMinimum(planType)
+
+ const handleSave = async () => {
+ if (!member) return
+
+ const newLimit = Number.parseFloat(limitValue)
+
+ if (Number.isNaN(newLimit) || newLimit < 0) {
+ setError('Please enter a valid positive number')
+ return
+ }
+
+ if (newLimit < planMinimum) {
+ setError(
+ `The limit cannot be below the ${planType} plan minimum of $${planMinimum.toFixed(2)}`
+ )
+ return
+ }
+
+ if (newLimit < member.currentUsage) {
+ setError(
+ `The new limit ($${newLimit.toFixed(2)}) cannot be lower than the member's current usage ($${member.currentUsage?.toFixed(2) || 0})`
+ )
+ return
+ }
+
+ try {
+ setError(null)
+ await onSave(member.userId, newLimit)
+ onOpenChange(false)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to update limit')
+ }
+ }
+
+ const formatCurrency = (amount: number) => `$${amount.toFixed(2)}`
+
+ if (!member) return null
+
+ const newLimit = Number.parseFloat(limitValue) || 0
+ const isIncrease = newLimit > member.usageLimit
+ const isDecrease = newLimit < member.usageLimit
+ const limitDifference = Math.abs(newLimit - member.usageLimit)
+
+ return (
+
+
+
+
+
+ Edit Usage Limit
+
+
+ Adjust the monthly usage limit for {member.userName}
+
+
+
+
+ {/* Member Info */}
+
+
+ {member.userName.charAt(0).toUpperCase()}
+
+
+
{member.userName}
+
{member.userEmail}
+
+
{member.role}
+
+
+ {/* Current Usage Stats */}
+
+
+
Current Usage
+
{formatCurrency(member.currentUsage)}
+
+
+
Current Limit
+
{formatCurrency(member.usageLimit)}
+
+
+
Plan Minimum
+
+ {formatCurrency(planMinimum)}
+
+
+
+
+ {/* New Limit Input */}
+
+
New Monthly Limit
+
+
+ setLimitValue(e.target.value)}
+ className='pl-9'
+ min={planMinimum}
+ max={10000}
+ step='1'
+ placeholder={planMinimum.toString()}
+ autoComplete='off'
+ data-form-type='other'
+ name='member-usage-limit'
+ />
+
+
+ Minimum limit for {planType} plan: ${planMinimum}
+
+
+
+ {/* Change Indicator */}
+ {limitValue && !Number.isNaN(newLimit) && limitDifference > 0 && (
+
+
+ {isIncrease ? '↗' : '↘'}
+ {isIncrease ? 'Increasing' : 'Decreasing'} limit by{' '}
+ {formatCurrency(limitDifference)}
+
+
+ {isIncrease
+ ? 'This will give the member more usage allowance.'
+ : "This will reduce the member's usage allowance."}
+
+
+ )}
+
+ {/* Warning for below plan minimum */}
+ {newLimit < planMinimum && newLimit > 0 && (
+
+
+
+ The limit cannot be below the {planType} plan minimum of{' '}
+ {formatCurrency(planMinimum)}.
+
+
+ )}
+
+ {/* Warning for decreasing below current usage */}
+ {newLimit < member.currentUsage && newLimit >= planMinimum && (
+
+
+
+ The new limit is below the member's current usage. The limit must be at least{' '}
+ {formatCurrency(member.currentUsage)}.
+
+
+ )}
+
+ {/* Error Display */}
+ {error && (
+
+
+ {error}
+
+ )}
+
+
+
+ onOpenChange(false)} disabled={isLoading}>
+ Cancel
+
+
+ {isLoading ? 'Updating...' : 'Update Limit'}
+
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/index.ts
new file mode 100644
index 0000000000..2d540c4f7e
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/index.ts
@@ -0,0 +1 @@
+export { NoOrganizationView } from './no-organization-view'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx
index 5c25c61eef..614b25b63a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx
@@ -1,15 +1,8 @@
import { RefreshCw } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
+import { OrganizationCreationDialog } from '../organization-creation-dialog'
interface NoOrganizationViewProps {
hasTeamPlan: boolean
@@ -42,48 +35,45 @@ export function NoOrganizationView({
}: NoOrganizationViewProps) {
if (hasTeamPlan || hasEnterprisePlan) {
return (
-
-
- {/* Header - matching settings page style */}
-
-
Create Your Team Workspace
-
+
+
+
Create Your Team Workspace
+
+
+
You're subscribed to a {hasEnterprisePlan ? 'enterprise' : 'team'} plan. Create your
workspace to start collaborating with your team.
-
-
- {/* Form fields - clean layout without card */}
-
-
-
- Team Name
-
-
-
-
-
- Team URL
-
-
-
- sim.ai/team/
-
+
+
+
+ Team Name
+
setOrgSlug(e.target.value)}
- placeholder='my-team'
- className='rounded-l-none'
+ id='orgName'
+ value={orgName}
+ onChange={onOrgNameChange}
+ placeholder='My Team'
/>
+
+
+
+ Team URL
+
+
+
+ sim.ai/team/
+
+
setOrgSlug(e.target.value)}
+ className='rounded-l-none'
+ />
+
+
{error && (
@@ -93,7 +83,7 @@ export function NoOrganizationView({
)}
-
+
-
-
-
- Create Team Organization
-
- Create a new team organization to manage members and billing.
-
-
-
-
- {error && (
-
- Error
- {error}
-
- )}
-
-
-
- Organization Name
-
-
-
-
-
-
- Organization Slug
-
- setOrgSlug(e.target.value)}
- disabled={isCreatingOrg}
- className='mt-1'
- />
-
-
-
- setCreateOrgDialogOpen(false)}
- disabled={isCreatingOrg}
- className='h-9 rounded-[8px]'
- >
- Cancel
-
-
- {isCreatingOrg && }
- Create Organization
-
-
-
-
-
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-creation-dialog/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-creation-dialog/index.ts
new file mode 100644
index 0000000000..6377972fa4
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-creation-dialog/index.ts
@@ -0,0 +1 @@
+export { OrganizationCreationDialog } from './organization-creation-dialog'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-creation-dialog/organization-creation-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-creation-dialog/organization-creation-dialog.tsx
new file mode 100644
index 0000000000..976f252dcf
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-creation-dialog/organization-creation-dialog.tsx
@@ -0,0 +1,100 @@
+import { RefreshCw } from 'lucide-react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Button } from '@/components/ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+
+interface OrganizationCreationDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ orgName: string
+ onOrgNameChange: (e: React.ChangeEvent
) => void
+ orgSlug: string
+ onOrgSlugChange: (slug: string) => void
+ onCreateOrganization: () => Promise
+ isCreating: boolean
+ error: string | null
+}
+
+export function OrganizationCreationDialog({
+ open,
+ onOpenChange,
+ orgName,
+ onOrgNameChange,
+ orgSlug,
+ onOrgSlugChange,
+ onCreateOrganization,
+ isCreating,
+ error,
+}: OrganizationCreationDialogProps) {
+ return (
+
+
+
+ Create Team Workspace
+
+ Create a workspace for your team to collaborate on projects.
+
+
+
+
+
+
+ Team Name
+
+
+
+
+
+
+ Team URL
+
+
+
+ sim.ai/team/
+
+
onOrgSlugChange(e.target.value)}
+ className='rounded-l-none'
+ />
+
+
+
+
+ {error && (
+
+ Error
+ {error}
+
+ )}
+
+
+ onOpenChange(false)}
+ disabled={isCreating}
+ className='h-9 rounded-[8px]'
+ >
+ Cancel
+
+
+ {isCreating && }
+ Create Team Workspace
+
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-settings-tab/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-settings-tab/index.ts
new file mode 100644
index 0000000000..5c4f4fb5e5
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-settings-tab/index.ts
@@ -0,0 +1 @@
+export { OrganizationSettingsTab } from './organization-settings-tab'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-settings-tab/organization-settings-tab.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-settings-tab/organization-settings-tab.tsx
new file mode 100644
index 0000000000..46d9507753
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/organization-settings-tab/organization-settings-tab.tsx
@@ -0,0 +1,136 @@
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import type { Organization, OrganizationFormData } from '@/stores/organization'
+
+interface OrganizationSettingsTabProps {
+ organization: Organization
+ isAdminOrOwner: boolean
+ userRole: string
+ orgFormData: OrganizationFormData
+ onOrgInputChange: (field: string, value: string) => void
+ onSaveOrgSettings: () => Promise
+ isSavingOrgSettings: boolean
+ orgSettingsError: string | null
+ orgSettingsSuccess: string | null
+}
+
+export function OrganizationSettingsTab({
+ organization,
+ isAdminOrOwner,
+ userRole,
+ orgFormData,
+ onOrgInputChange,
+ onSaveOrgSettings,
+ isSavingOrgSettings,
+ orgSettingsError,
+ orgSettingsSuccess,
+}: OrganizationSettingsTabProps) {
+ return (
+
+ {orgSettingsError && (
+
+ Error
+ {orgSettingsError}
+
+ )}
+
+ {orgSettingsSuccess && (
+
+ Success
+ {orgSettingsSuccess}
+
+ )}
+
+ {!isAdminOrOwner && (
+
+ Read Only
+
+ You need owner or admin permissions to modify team settings.
+
+
+ )}
+
+
+
+ Basic Information
+ Update your team's basic information and branding
+
+
+
+ Team Name
+ onOrgInputChange('name', e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && isAdminOrOwner && !isSavingOrgSettings) {
+ onSaveOrgSettings()
+ }
+ }}
+ placeholder='Enter team name'
+ disabled={!isAdminOrOwner || isSavingOrgSettings}
+ />
+
+
+
+
Team Slug
+
onOrgInputChange('slug', e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && isAdminOrOwner && !isSavingOrgSettings) {
+ onSaveOrgSettings()
+ }
+ }}
+ placeholder='team-slug'
+ disabled={!isAdminOrOwner || isSavingOrgSettings}
+ />
+
+ Used in URLs and API references. Can only contain lowercase letters, numbers, hyphens,
+ and underscores.
+
+
+
+
+ Logo URL (Optional)
+ onOrgInputChange('logo', e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && isAdminOrOwner && !isSavingOrgSettings) {
+ onSaveOrgSettings()
+ }
+ }}
+ placeholder='https://example.com/logo.png'
+ disabled={!isAdminOrOwner || isSavingOrgSettings}
+ />
+
+
+
+
+
+
+ Team Information
+
+
+
+ Team ID:
+ {organization.id}
+
+
+ Created:
+ {new Date(organization.createdAt).toLocaleDateString()}
+
+
+ Your Role:
+ {userRole}
+
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/pending-invitations-list/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/pending-invitations-list/index.ts
new file mode 100644
index 0000000000..213a663f83
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/pending-invitations-list/index.ts
@@ -0,0 +1 @@
+export { PendingInvitationsList } from './pending-invitations-list'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/pending-invitations-list/pending-invitations-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/pending-invitations-list/pending-invitations-list.tsx
new file mode 100644
index 0000000000..f183cd22dd
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/pending-invitations-list/pending-invitations-list.tsx
@@ -0,0 +1,53 @@
+import { X } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import type { Invitation, Organization } from '@/stores/organization'
+
+interface PendingInvitationsListProps {
+ organization: Organization
+ onCancelInvitation: (invitationId: string) => void
+}
+
+export function PendingInvitationsList({
+ organization,
+ onCancelInvitation,
+}: PendingInvitationsListProps) {
+ const pendingInvitations = organization.invitations?.filter(
+ (invitation) => invitation.status === 'pending'
+ )
+
+ if (!pendingInvitations || pendingInvitations.length === 0) {
+ return null
+ }
+
+ return (
+
+
Pending Invitations
+
+ {pendingInvitations.map((invitation: Invitation) => (
+
+
+
+
+ {invitation.email.charAt(0).toUpperCase()}
+
+
+
{invitation.email}
+
Invitation pending
+
+
+
+
+
onCancelInvitation(invitation.id)}
+ className='h-8 w-8 rounded-[8px] p-0'
+ >
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/remove-member-dialog/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/remove-member-dialog/index.ts
new file mode 100644
index 0000000000..2eed30381d
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/remove-member-dialog/index.ts
@@ -0,0 +1 @@
+export { RemoveMemberDialog } from './remove-member-dialog'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members-list/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members-list/index.ts
new file mode 100644
index 0000000000..8b0fb52066
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members-list/index.ts
@@ -0,0 +1 @@
+export { TeamMembersList } from './team-members-list'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members-list/team-members-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members-list/team-members-list.tsx
new file mode 100644
index 0000000000..7c79e6eb8b
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members-list/team-members-list.tsx
@@ -0,0 +1,68 @@
+import { UserX } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import type { Member, Organization } from '@/stores/organization'
+
+interface TeamMembersListProps {
+ organization: Organization
+ currentUserEmail: string
+ isAdminOrOwner: boolean
+ onRemoveMember: (member: Member) => void
+}
+
+export function TeamMembersList({
+ organization,
+ currentUserEmail,
+ isAdminOrOwner,
+ onRemoveMember,
+}: TeamMembersListProps) {
+ if (!organization.members || organization.members.length === 0) {
+ return (
+
+
Team Members
+
+ No members in this organization yet.
+
+
+ )
+ }
+
+ return (
+
+
Team Members
+
+ {organization.members.map((member: Member) => (
+
+
+
+
+ {(member.user?.name || member.user?.email || 'U').charAt(0).toUpperCase()}
+
+
+
{member.user?.name || 'Unknown'}
+
{member.user?.email}
+
+
+ {member.role.charAt(0).toUpperCase() + member.role.slice(1)}
+
+
+
+
+ {/* Only show remove button for non-owners and if current user is admin/owner */}
+ {isAdminOrOwner &&
+ member.role !== 'owner' &&
+ member.user?.email !== currentUserEmail && (
+
onRemoveMember(member)}
+ className='h-8 w-8 rounded-[8px] p-0'
+ >
+
+
+ )}
+
+ ))}
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members/team-members.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members/team-members.tsx
deleted file mode 100644
index 6e4eddfbc1..0000000000
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-members/team-members.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { UserX, X } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import type { Invitation, Member, Organization } from '@/stores/organization'
-
-interface ConsolidatedTeamMembersProps {
- organization: Organization
- currentUserEmail: string
- isAdminOrOwner: boolean
- onRemoveMember: (member: Member) => void
- onCancelInvitation: (invitationId: string) => void
-}
-
-interface TeamMemberItem {
- type: 'member' | 'invitation'
- id: string
- name: string
- email: string
- role: string
- usage?: string
- lastActive?: string
- member?: Member
- invitation?: Invitation
-}
-
-export function TeamMembers({
- organization,
- currentUserEmail,
- isAdminOrOwner,
- onRemoveMember,
- onCancelInvitation,
-}: ConsolidatedTeamMembersProps) {
- // Combine members and pending invitations into a single list
- const teamItems: TeamMemberItem[] = []
-
- // Add existing members
- if (organization.members) {
- organization.members.forEach((member: Member) => {
- teamItems.push({
- type: 'member',
- id: member.id,
- name: member.user?.name || 'Unknown',
- email: member.user?.email || '',
- role: member.role,
- usage: '$0.00', // TODO: Get real usage data
- lastActive: '8/26/2025', // TODO: Get real last active date
- member,
- })
- })
- }
-
- // Add pending invitations
- const pendingInvitations = organization.invitations?.filter(
- (invitation) => invitation.status === 'pending'
- )
- if (pendingInvitations) {
- pendingInvitations.forEach((invitation: Invitation) => {
- teamItems.push({
- type: 'invitation',
- id: invitation.id,
- name: invitation.email.split('@')[0], // Use email prefix as name
- email: invitation.email,
- role: 'pending',
- usage: '-',
- lastActive: '-',
- invitation,
- })
- })
- }
-
- if (teamItems.length === 0) {
- return No team members yet.
- }
-
- return (
-
- {/* Header - simple like account page */}
-
-
Team Members
-
-
- {/* Members list - clean like account page */}
-
- {teamItems.map((item) => (
-
- {/* Member info */}
-
- {/* Avatar */}
-
- {item.name.charAt(0).toUpperCase()}
-
-
- {/* Name and email */}
-
-
- {item.name}
- {item.type === 'member' && (
-
- {item.role.charAt(0).toUpperCase() + item.role.slice(1)}
-
- )}
- {item.type === 'invitation' && (
-
- Pending
-
- )}
-
-
{item.email}
-
-
- {/* Usage and stats - matching subscription layout */}
-
-
-
-
Active
-
{item.lastActive}
-
-
-
-
- {/* Actions */}
- {isAdminOrOwner && (
-
- {item.type === 'member' &&
- item.member?.role !== 'owner' &&
- item.email !== currentUserEmail && (
- onRemoveMember(item.member!)}
- className='h-8 w-8 rounded-[8px] p-0'
- >
-
-
- )}
-
- {item.type === 'invitation' && (
- onCancelInvitation(item.invitation!.id)}
- className='h-8 w-8 rounded-[8px] p-0'
- >
-
-
- )}
-
- )}
-
- ))}
-
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/index.ts
new file mode 100644
index 0000000000..c8f65cc4dc
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/index.ts
@@ -0,0 +1 @@
+export { TeamSeatsOverview } from './team-seats-overview'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx
index 1be9ceaca4..bea33520e9 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx
@@ -1,10 +1,9 @@
import { Building2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Skeleton } from '@/components/ui/skeleton'
-import { DEFAULT_TEAM_TIER_COST_LIMIT } from '@/lib/billing/constants'
import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils'
-import { env } from '@/lib/env'
type Subscription = {
id: string
@@ -30,25 +29,9 @@ interface TeamSeatsOverviewProps {
function TeamSeatsSkeleton() {
return (
-
-
+
+
+
)
}
@@ -63,86 +46,123 @@ export function TeamSeatsOverview({
onAddSeatDialog,
}: TeamSeatsOverviewProps) {
if (isLoadingSubscription) {
- return
+ return (
+
+
+ Team Seats Overview
+ Manage your team's seat allocation and billing
+
+
+
+
+
+ )
}
if (!subscriptionData) {
return (
-
-
-
-
-
-
-
No Team Subscription Found
-
- Your subscription may need to be transferred to this organization.
-
+
+
+ Team Seats Overview
+ Manage your team's seat allocation and billing
+
+
+
+
+
+
+
+
No Team Subscription Found
+
+ Your subscription may need to be transferred to this organization.
+
+
+
{
+ onConfirmTeamUpgrade(2) // Start with 2 seats as default
+ }}
+ disabled={isLoading}
+ className='h-9 rounded-[8px]'
+ >
+ Set Up Team Subscription
+
- {
- onConfirmTeamUpgrade(2) // Start with 2 seats as default
- }}
- disabled={isLoading}
- className='h-9 rounded-[8px]'
- >
- Set Up Team Subscription
-
-
-
+
+
)
}
return (
-
-
- {/* Seats info and usage - matching team usage layout */}
-
-
- Seats
-
- (${env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT}/month each)
-
-
-
-
{usedSeats} used
-
/
-
{subscriptionData.seats || 0} total
+
+
+ Team Seats Overview
+ Manage your team's seat allocation and billing
+
+
+
+
+
+
{subscriptionData.seats || 0}
+
Licensed Seats
+
+
+
{usedSeats}
+
Used Seats
+
+
+
{(subscriptionData.seats || 0) - usedSeats}
+
Available
+
-
- {/* Progress Bar - matching team usage component */}
-
+
+
+ Seat Usage
+
+ {usedSeats} of {subscriptionData.seats || 0} seats
+
+
+
+
- {/* Action buttons - below the usage display */}
- {checkEnterprisePlan(subscriptionData) ? (
-
-
- Contact enterprise for support usage limit changes
-
+
+ Seat Cost:
+
+ ${((subscriptionData.seats || 0) * 40).toFixed(2)}
+
- ) : (
-
-
- Remove Seat
-
-
- Add Seat
-
+
+ Individual usage limits may vary. See Subscription tab for team totals.
- )}
-
-
+
+ {checkEnterprisePlan(subscriptionData) ? (
+
+
Enterprise Plan
+
Contact support to modify seats
+
+ ) : (
+
+
+ Remove Seat
+
+
+ Add Seat
+
+
+ )}
+
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/index.ts
new file mode 100644
index 0000000000..5a45773dc2
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/index.ts
@@ -0,0 +1 @@
+export { TeamSeats } from './team-seats'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx
index 96cef39c42..231944ce63 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-seats/team-seats.tsx
@@ -16,8 +16,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
-import { DEFAULT_TEAM_TIER_COST_LIMIT } from '@/lib/billing/constants'
import { env } from '@/lib/env'
interface TeamSeatsProps {
@@ -31,7 +29,6 @@ interface TeamSeatsProps {
onConfirm: (seats: number) => Promise
confirmButtonText: string
showCostBreakdown?: boolean
- isCancelledAtPeriodEnd?: boolean
}
export function TeamSeats({
@@ -45,7 +42,6 @@ export function TeamSeats({
onConfirm,
confirmButtonText,
showCostBreakdown = false,
- isCancelledAtPeriodEnd = false,
}: TeamSeatsProps) {
const [selectedSeats, setSelectedSeats] = useState(initialSeats)
@@ -55,7 +51,7 @@ export function TeamSeats({
}
}, [open, initialSeats])
- const costPerSeat = env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT
+ const costPerSeat = env.TEAM_TIER_COST_LIMIT ?? 40
const totalMonthlyCost = selectedSeats * costPerSeat
const costChange = currentSeats ? (selectedSeats - currentSeats) * costPerSeat : 0
@@ -118,39 +114,19 @@ export function TeamSeats({
onOpenChange(false)} disabled={isLoading}>
Cancel
-
-
-
-
-
- {isLoading ? (
-
- ) : (
- {confirmButtonText}
- )}
-
-
-
- {isCancelledAtPeriodEnd && (
-
-
- To update seats, go to Subscription {'>'} Manage {'>'} Keep Subscription to
- reactivate
-
-
- )}
-
-
+
+ {isLoading ? (
+
+ ) : (
+ {confirmButtonText}
+ )}
+
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/index.ts
new file mode 100644
index 0000000000..635cd8a1c4
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/index.ts
@@ -0,0 +1 @@
+export { TeamUsage } from './team-usage'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/team-usage.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/team-usage.tsx
index 432e53b429..79514cec8d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/team-usage.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/team-usage/team-usage.tsx
@@ -1,16 +1,15 @@
-import { useCallback, useEffect, useRef } from 'react'
-import { AlertCircle } from 'lucide-react'
+import { useEffect, useState } from 'react'
+import { AlertCircle, Settings2 } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { useActiveOrganization } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
-import { UsageHeader } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/shared/usage-header'
-import {
- UsageLimit,
- type UsageLimitRef,
-} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components'
import { useOrganizationStore } from '@/stores/organization'
-import { useSubscriptionStore } from '@/stores/subscription/store'
+import type { MemberUsageData } from '@/stores/organization/types'
+import { MemberLimit } from '../member-limit'
const logger = createLogger('TeamUsage')
@@ -20,11 +19,14 @@ interface TeamUsageProps {
export function TeamUsage({ hasAdminAccess }: TeamUsageProps) {
const { data: activeOrg } = useActiveOrganization()
- const { getSubscriptionStatus } = useSubscriptionStore()
+ const [editDialogOpen, setEditDialogOpen] = useState(false)
+ const [selectedMember, setSelectedMember] = useState(null)
+ const [isUpdating, setIsUpdating] = useState(false)
const {
organizationBillingData: billingData,
loadOrganizationBillingData,
+ updateMemberUsageLimit,
isLoadingOrgBilling,
error,
} = useOrganizationStore()
@@ -35,35 +37,143 @@ export function TeamUsage({ hasAdminAccess }: TeamUsageProps) {
}
}, [activeOrg?.id, loadOrganizationBillingData])
- const handleLimitUpdated = useCallback(
- async (newLimit: number) => {
- // Reload the organization billing data to reflect the new limit
- if (activeOrg?.id) {
- await loadOrganizationBillingData(activeOrg.id, true)
+ const handleEditLimit = (member: MemberUsageData) => {
+ setSelectedMember(member)
+ setEditDialogOpen(true)
+ }
+
+ const handleSaveLimit = async (userId: string, newLimit: number): Promise => {
+ if (!activeOrg?.id) {
+ throw new Error('No active organization found')
+ }
+
+ try {
+ setIsUpdating(true)
+ const result = await updateMemberUsageLimit(userId, activeOrg.id, newLimit)
+
+ if (!result.success) {
+ logger.error('Failed to update usage limit', { error: result.error, userId, newLimit })
+ throw new Error(result.error || 'Failed to update usage limit')
}
- },
- [activeOrg?.id, loadOrganizationBillingData]
- )
- const usageLimitRef = useRef(null)
+ logger.info('Successfully updated member usage limit', {
+ userId,
+ newLimit,
+ organizationId: activeOrg.id,
+ })
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Failed to update usage limit'
+ logger.error('Failed to update usage limit', {
+ error,
+ userId,
+ newLimit,
+ organizationId: activeOrg.id,
+ })
+ throw new Error(errorMessage)
+ } finally {
+ setIsUpdating(false)
+ }
+ }
+
+ const handleCloseEditDialog = () => {
+ setEditDialogOpen(false)
+ setSelectedMember(null)
+ }
+
+ const formatCurrency = (amount: number) => `$${amount.toFixed(2)}`
+ const formatDate = (dateString: string | null) => {
+ if (!dateString) return 'Never'
+ return new Date(dateString).toLocaleDateString()
+ }
if (isLoadingOrgBilling) {
return (
-
-
-
-
-
-
-
-
-
-
/
-
+
+ {/* Table Skeleton */}
+
+
+
+ {/* Table Header Skeleton */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table Body Skeleton */}
+
+ {[...Array(3)].map((_, index) => (
+
+
+ {/* Member Info Skeleton */}
+
+
+
+ {/* Mobile-only usage info skeleton */}
+
+
+
+ {/* Role Skeleton */}
+
+
+
+
+ {/* Usage - Desktop Skeleton */}
+
+
+
+
+ {/* Limit - Desktop Skeleton */}
+
+
+
+
+ {/* Last Active - Desktop Skeleton */}
+
+
+
+
+ {/* Actions Skeleton */}
+
+
+
+
+
+ ))}
+
-
-
-
+
+
)
}
@@ -79,79 +189,160 @@ export function TeamUsage({ hasAdminAccess }: TeamUsageProps) {
}
if (!billingData) {
- return null
+ return (
+
+
+ No Data
+ No billing data available for this organization.
+
+ )
}
- const currentUsage = billingData.totalCurrentUsage || 0
- const currentCap = billingData.totalUsageLimit || billingData.minimumBillingAmount || 0
- const minimumBilling = billingData.minimumBillingAmount || 0
- const seatsCount = billingData.seatsCount || 1
- const percentUsed =
- currentCap > 0 ? Math.round(Math.min((currentUsage / currentCap) * 100, 100)) : 0
- const status: 'ok' | 'warning' | 'exceeded' =
- percentUsed >= 100 ? 'exceeded' : percentUsed >= 80 ? 'warning' : 'ok'
-
- const subscription = getSubscriptionStatus()
- const title = subscription.isEnterprise
- ? 'Enterprise'
- : subscription.isTeam
- ? 'Team'
- : (subscription.plan || 'Free').charAt(0).toUpperCase() +
- (subscription.plan || 'Free').slice(1)
+ const membersOverLimit = billingData.members?.filter((m) => m.isOverLimit).length || 0
+ const membersNearLimit =
+ billingData.members?.filter((m) => !m.isOverLimit && m.percentUsed >= 80).length || 0
return (
-
{
- if (!subscription.isEnterprise) usageLimitRef.current?.startEdit()
- }}
- seatsText={`${seatsCount} seats`}
- current={currentUsage}
- limit={currentCap}
- isBlocked={Boolean(billingData?.billingBlocked)}
- status={status}
- percentUsed={percentUsed}
- onResolvePayment={async () => {
- try {
- const res = await fetch('/api/billing/portal', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- context: 'organization',
- organizationId: activeOrg?.id,
- returnUrl: `${window.location.origin}/workspace?billing=updated`,
- }),
- })
- const data = await res.json()
- if (!res.ok || !data?.url)
- throw new Error(data?.error || 'Failed to start billing portal')
- window.location.href = data.url
- } catch (e) {
- alert(e instanceof Error ? e.message : 'Failed to open billing portal')
- }
- }}
- rightContent={
- hasAdminAccess && activeOrg?.id && !subscription.isEnterprise ? (
-
- ) : (
-
- ${currentCap.toFixed(0)}
-
- )
- }
- progressValue={percentUsed}
- />
+
+ {/* Alerts */}
+ {membersOverLimit > 0 && (
+
+
+
+
+
Usage Limits Exceeded
+
+ {membersOverLimit} team {membersOverLimit === 1 ? 'member has' : 'members have'}{' '}
+ exceeded their usage limits. Consider increasing their limits below.
+
+
+
+
+ )}
+
+ {/* Member Usage Table */}
+
+
+
+ {/* Table Header */}
+
+
+
Member
+
Role
+
Usage
+
Limit
+
Active
+
+
+
+
+ {/* Table Body */}
+
+ {billingData.members && billingData.members.length > 0 ? (
+ billingData.members.map((member) => (
+
+
+ {/* Member Info */}
+
+
+
+ {member.userName.charAt(0).toUpperCase()}
+
+
+
{member.userName}
+
+ {member.userEmail}
+
+
+
+
+ {/* Mobile-only usage info */}
+
+
+
Usage
+
+ {formatCurrency(member.currentUsage)}
+
+
+
+
Limit
+
+ {formatCurrency(member.usageLimit)}
+
+
+
+
+
+ {/* Role */}
+
+
+ {member.role}
+
+
+
+ {/* Usage - Desktop */}
+
+
+ {formatCurrency(member.currentUsage)}
+
+
+
+ {/* Limit - Desktop */}
+
+
+ {formatCurrency(member.usageLimit)}
+
+
+
+ {/* Last Active - Desktop */}
+
+
+ {formatDate(member.lastActive)}
+
+
+
+ {/* Actions */}
+
+ {hasAdminAccess && (
+ handleEditLimit(member)}
+ disabled={isUpdating}
+ className='h-8 w-8 p-0 opacity-0 transition-opacity group-hover:opacity-100 sm:opacity-100'
+ title='Edit usage limit'
+ >
+
+
+ )}
+
+
+
+ ))
+ ) : (
+
+
No team members found.
+
+ )}
+
+
+
+
+
+ {/* Edit Member Limit Dialog */}
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx
index 00ddfcfd71..72f5b75c21 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx
@@ -1,21 +1,31 @@
-import { useCallback, useEffect, useState } from 'react'
-import { Alert, AlertDescription, AlertTitle, Skeleton } from '@/components/ui'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import {
+ Alert,
+ AlertDescription,
+ AlertTitle,
+ Skeleton,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from '@/components/ui'
import { useSession } from '@/lib/auth-client'
-import { DEFAULT_TEAM_TIER_COST_LIMIT } from '@/lib/billing/constants'
import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateSlug, useOrganizationStore } from '@/stores/organization'
+import { useSubscriptionStore } from '@/stores/subscription/store'
import {
MemberInvitationCard,
NoOrganizationView,
+ OrganizationSettingsTab,
+ PendingInvitationsList,
RemoveMemberDialog,
- TeamMembers,
+ TeamMembersList,
TeamSeats,
TeamSeatsOverview,
TeamUsage,
-} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components'
-import { generateSlug, useOrganizationStore } from '@/stores/organization'
-import { useSubscriptionStore } from '@/stores/subscription/store'
+} from './components'
const logger = createLogger('TeamManagement')
@@ -27,14 +37,18 @@ export function TeamManagement() {
activeOrganization,
subscriptionData,
userWorkspaces,
+ orgFormData,
hasTeamPlan,
hasEnterprisePlan,
isLoading,
isLoadingSubscription,
isCreatingOrg,
isInviting,
+ isSavingOrgSettings,
error,
+ orgSettingsError,
inviteSuccess,
+ orgSettingsSuccess,
loadData,
createOrganization,
setActiveOrganization,
@@ -43,10 +57,12 @@ export function TeamManagement() {
cancelInvitation,
addSeats,
reduceSeats,
+ updateOrganizationSettings,
loadUserWorkspaces,
getUserRole,
isAdminOrOwner,
getUsedSeats,
+ setOrgFormData,
} = useOrganizationStore()
const { getSubscriptionStatus } = useSubscriptionStore()
@@ -65,6 +81,7 @@ export function TeamManagement() {
}>({ open: false, memberId: '', memberName: '', shouldReduceSeats: false })
const [orgName, setOrgName] = useState('')
const [orgSlug, setOrgSlug] = useState('')
+ const [activeTab, setActiveTab] = useState('members')
const [isAddSeatDialogOpen, setIsAddSeatDialogOpen] = useState(false)
const [newSeatCount, setNewSeatCount] = useState(1)
const [isUpdatingSeats, setIsUpdatingSeats] = useState(false)
@@ -72,7 +89,17 @@ export function TeamManagement() {
const userRole = getUserRole(session?.user?.email)
const adminOrOwner = isAdminOrOwner(session?.user?.email)
const usedSeats = getUsedSeats()
+ const subscription = getSubscriptionStatus()
+ const hasLoadedInitialData = useRef(false)
+ useEffect(() => {
+ if (!hasLoadedInitialData.current) {
+ loadData()
+ hasLoadedInitialData.current = true
+ }
+ }, [])
+
+ // Set default organization name for team/enterprise users
useEffect(() => {
if ((hasTeamPlan || hasEnterprisePlan) && session?.user?.name && !orgName) {
const defaultName = `${session.user.name}'s Team`
@@ -81,6 +108,7 @@ export function TeamManagement() {
}
}, [hasTeamPlan, hasEnterprisePlan, session?.user?.name, orgName])
+ // Load workspaces for admin users
const activeOrgId = activeOrganization?.id
useEffect(() => {
if (session?.user?.id && activeOrgId && adminOrOwner) {
@@ -96,39 +124,11 @@ export function TeamManagement() {
const handleCreateOrganization = useCallback(async () => {
if (!session?.user || !orgName.trim()) return
-
- try {
- const response = await fetch('/api/organizations', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- name: orgName.trim(),
- slug: orgSlug.trim(),
- }),
- })
-
- if (!response.ok) {
- throw new Error(`Failed to create organization: ${response.statusText}`)
- }
-
- const result = await response.json()
-
- if (!result.success || !result.organizationId) {
- throw new Error('Failed to create organization')
- }
-
- // Refresh organization data
- await loadData()
-
- setCreateOrgDialogOpen(false)
- setOrgName('')
- setOrgSlug('')
- } catch (error) {
- logger.error('Failed to create organization', error)
- }
- }, [session?.user?.id, orgName, orgSlug, loadData])
+ await createOrganization(orgName.trim(), orgSlug.trim())
+ setCreateOrgDialogOpen(false)
+ setOrgName('')
+ setOrgSlug('')
+ }, [session?.user?.id, orgName, orgSlug])
const handleInviteMember = useCallback(async () => {
if (!session?.user || !activeOrgId || !inviteEmail.trim()) return
@@ -221,6 +221,15 @@ export function TeamManagement() {
[subscriptionData?.id, activeOrgId, newSeatCount]
)
+ const handleOrgInputChange = useCallback((field: string, value: string) => {
+ setOrgFormData({ [field]: value })
+ }, [])
+
+ const handleSaveOrgSettings = useCallback(async () => {
+ if (!activeOrgId || !adminOrOwner) return
+ await updateOrganizationSettings()
+ }, [activeOrgId, adminOrOwner])
+
const confirmTeamUpgrade = useCallback(
async (seats: number) => {
if (!session?.user || !activeOrgId) return
@@ -232,12 +241,10 @@ export function TeamManagement() {
if (isLoading && !activeOrganization && !(hasTeamPlan || hasEnterprisePlan)) {
return (
-
-
-
-
-
-
+
+
+
+
)
}
@@ -262,104 +269,103 @@ export function TeamManagement() {
}
return (
-
-
- {error && (
-
- Error
- {error}
-
- )}
-
- {/* Team Usage Overview */}
-
-
- {/* Team Billing Information (only show for Team Plan, not Enterprise) */}
- {hasTeamPlan && !hasEnterprisePlan && (
-
-
-
How Team Billing Works
-
-
- Your team is billed a minimum of $
- {(subscriptionData?.seats || 0) *
- (env.TEAM_TIER_COST_LIMIT ?? DEFAULT_TEAM_TIER_COST_LIMIT)}
- /month for {subscriptionData?.seats || 0} licensed seats
-
- All team member usage is pooled together from a shared limit
-
- When pooled usage exceeds the limit, all members are blocked from using the
- service
-
- You can increase the usage limit to allow for higher usage
-
- Any usage beyond the minimum seat cost is billed as overage at the end of the
- billing period
-
-
-
+
+
+
Team Management
+
+ {organizations.length > 1 && (
+
+ setActiveOrganization(e.target.value)}
+ >
+ {organizations.map((org) => (
+
+ {org.name}
+
+ ))}
+
)}
+
- {/* Member Invitation Card */}
- {adminOrOwner && (
-
loadUserWorkspaces(session?.user?.id)}
- onWorkspaceToggle={handleWorkspaceToggle}
- inviteSuccess={inviteSuccess}
- availableSeats={Math.max(0, (subscriptionData?.seats || 0) - usedSeats.used)}
- maxSeats={subscriptionData?.seats || 0}
+ {error && (
+
+ Error
+ {error}
+
+ )}
+
+
+
+ Members
+ Usage
+ Settings
+
+
+
+ {adminOrOwner && (
+ loadUserWorkspaces(session?.user?.id)}
+ onWorkspaceToggle={handleWorkspaceToggle}
+ inviteSuccess={inviteSuccess}
+ />
+ )}
+
+ {adminOrOwner && (
+
+ )}
+
+
- )}
- {/* Team Seats Overview */}
- {adminOrOwner && (
- 0 && (
+
+ )}
+
+
+
+
+
+
+
+
- )}
-
- {/* Team Members */}
-
-
- {/* Team Information Section - at bottom of modal */}
-
-
-
- Team ID:
- {activeOrganization.id}
-
-
- Created:
- {new Date(activeOrganization.createdAt).toLocaleDateString()}
-
-
- Your Role:
- {userRole}
-
-
-
-
+
+
)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
index 39465046b3..959eb3f184 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx
@@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { getEnv, isTruthy } from '@/lib/env'
import { isHosted } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
+import { cn } from '@/lib/utils'
import {
Account,
ApiKeys,
@@ -46,34 +47,32 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const loadSettings = useGeneralStore((state) => state.loadSettings)
const { activeOrganization } = useOrganizationStore()
const hasLoadedInitialData = useRef(false)
- const hasLoadedGeneral = useRef(false)
const environmentCloseHandler = useRef<((open: boolean) => void) | null>(null)
- const credentialsCloseHandler = useRef<((open: boolean) => void) | null>(null)
useEffect(() => {
- async function loadGeneralIfNeeded() {
+ async function loadAllSettings() {
if (!open) return
- if (activeSection !== 'general') return
- if (hasLoadedGeneral.current) return
+
+ if (hasLoadedInitialData.current) return
+
setIsLoading(true)
+
try {
await loadSettings()
- hasLoadedGeneral.current = true
hasLoadedInitialData.current = true
} catch (error) {
- logger.error('Error loading general settings:', error)
+ logger.error('Error loading settings data:', error)
} finally {
setIsLoading(false)
}
}
if (open) {
- void loadGeneralIfNeeded()
+ loadAllSettings()
} else {
hasLoadedInitialData.current = false
- hasLoadedGeneral.current = false
}
- }, [open, activeSection, loadSettings])
+ }, [open, loadSettings])
useEffect(() => {
const handleOpenSettings = (event: CustomEvent<{ tab: SettingsSection }>) => {
@@ -101,8 +100,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const handleDialogOpenChange = (newOpen: boolean) => {
if (!newOpen && activeSection === 'environment' && environmentCloseHandler.current) {
environmentCloseHandler.current(newOpen)
- } else if (!newOpen && activeSection === 'credentials' && credentialsCloseHandler.current) {
- credentialsCloseHandler.current(newOpen)
} else {
onOpenChange(newOpen)
}
@@ -127,61 +124,44 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
{/* Content Area */}
- {activeSection === 'general' && (
-
-
-
- )}
- {activeSection === 'environment' && (
-
- {
- environmentCloseHandler.current = handler
- }}
- />
-
- )}
- {activeSection === 'account' && (
-
- )}
- {activeSection === 'credentials' && (
-
- {
- credentialsCloseHandler.current = handler
- }}
- />
-
- )}
- {activeSection === 'apikeys' && (
-
- )}
- {isSubscriptionEnabled && activeSection === 'subscription' && (
-
+
+
+
+
+ {
+ environmentCloseHandler.current = handler
+ }}
+ />
+
+
+
+
+
+
+ {isSubscriptionEnabled && (
+
)}
- {isBillingEnabled && activeSection === 'team' && (
-
+ {isBillingEnabled && (
+
)}
- {isHosted && activeSection === 'copilot' && (
-
+ {isHosted && (
+
)}
- {activeSection === 'privacy' && (
-
- )}
+
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx
index 40ff7b8182..f81f8359a8 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx
@@ -22,9 +22,8 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
-import { useSession } from '@/lib/auth-client'
+import { useSession, useSubscription } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
-import { useSubscriptionUpgrade } from '@/lib/subscription/upgrade'
import { cn } from '@/lib/utils'
import { useOrganizationStore } from '@/stores/organization'
import { useSubscriptionStore } from '@/stores/subscription/store'
@@ -44,7 +43,7 @@ interface PlanFeature {
export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps) {
const { data: session } = useSession()
- const { handleUpgrade } = useSubscriptionUpgrade()
+ const betterAuthSubscription = useSubscription()
const { activeOrganization } = useOrganizationStore()
const { loadData, getSubscriptionStatus, isLoading } = useSubscriptionStore()
@@ -57,15 +56,40 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps
const subscription = getSubscriptionStatus()
- const handleUpgradeWithErrorHandling = useCallback(
+ const handleUpgrade = useCallback(
async (targetPlan: 'pro' | 'team') => {
+ if (!session?.user?.id) return
+
+ const subscriptionData = useSubscriptionStore.getState().subscriptionData
+ const currentSubscriptionId = subscriptionData?.stripeSubscriptionId
+
+ let referenceId = session.user.id
+ if (subscription.isTeam && activeOrganization?.id) {
+ referenceId = activeOrganization.id
+ }
+
+ const currentUrl = window.location.origin + window.location.pathname
+
try {
- await handleUpgrade(targetPlan)
+ const upgradeParams: any = {
+ plan: targetPlan,
+ referenceId,
+ successUrl: currentUrl,
+ cancelUrl: currentUrl,
+ seats: targetPlan === 'team' ? 1 : undefined,
+ }
+
+ if (currentSubscriptionId) {
+ upgradeParams.subscriptionId = currentSubscriptionId
+ }
+
+ await betterAuthSubscription.upgrade(upgradeParams)
} catch (error) {
- alert(error instanceof Error ? error.message : 'Unknown error occurred')
+ logger.error('Failed to initiate subscription upgrade:', error)
+ alert('Failed to initiate upgrade. Please try again or contact support.')
}
},
- [handleUpgrade]
+ [session?.user?.id, subscription.isTeam, activeOrganization?.id, betterAuthSubscription]
)
const handleContactUs = () => {
@@ -100,7 +124,7 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps
{ text: 'Unlimited log retention', included: true, icon: Database },
],
isActive: subscription.isPro && !subscription.isTeam,
- action: subscription.isFree ? () => handleUpgradeWithErrorHandling('pro') : null,
+ action: subscription.isFree ? () => handleUpgrade('pro') : null,
},
{
name: 'Team',
@@ -113,7 +137,7 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps
{ text: 'Dedicated Slack channel', included: true, icon: MessageSquare },
],
isActive: subscription.isTeam,
- action: !subscription.isTeam ? () => handleUpgradeWithErrorHandling('team') : null,
+ action: !subscription.isTeam ? () => handleUpgrade('team') : null,
},
{
name: 'Enterprise',
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx
index 16e9648a8d..d4e27e7f25 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx
@@ -22,7 +22,7 @@ const PLAN_NAMES = {
} as const
interface UsageIndicatorProps {
- onClick?: () => void
+ onClick?: (badgeType: 'add' | 'upgrade') => void
}
export function UsageIndicator({ onClick }: UsageIndicatorProps) {
@@ -39,7 +39,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
// Show skeleton while loading
if (isLoading) {
return (
-
onClick?.()}>
+
onClick?.('upgrade')}>
{/* Plan and usage info skeleton */}
@@ -67,12 +67,12 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
: 'free'
// Determine badge to show
- const billingStatus = useSubscriptionStore.getState().getBillingStatus()
- const isBlocked = billingStatus === 'blocked'
- const badgeText = isBlocked ? 'Payment Failed' : planType === 'free' ? 'Upgrade' : undefined
+ const showAddBadge = planType !== 'free' && usage.percentUsed >= 50
+ const badgeText = planType === 'free' ? 'Upgrade' : 'Add'
+ const badgeType = planType === 'free' ? 'upgrade' : 'add'
return (
-
onClick?.()}>
+
onClick?.(badgeType)}>
{/* Plan and usage info */}