diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d655c72c..24a295ec 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@stripe/stripe-js": "^8.7.0", "axios": "^1.13.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1430,6 +1431,15 @@ "win32" ] }, + "node_modules/@stripe/stripe-js": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.7.0.tgz", + "integrity": "sha512-tNUerSstwNC1KuHgX4CASGO0Md3CB26IJzSXmVlSuFvhsBP4ZaEPpY4jxWOn9tfdDscuVT4Kqb8cZ2o9nLCgRQ==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index c038dd9c..672da33f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@stripe/stripe-js": "^8.7.0", "axios": "^1.13.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/frontend/src/api/accounts.ts b/frontend/src/api/accounts.ts new file mode 100644 index 00000000..07bffc9a --- /dev/null +++ b/frontend/src/api/accounts.ts @@ -0,0 +1,48 @@ +import apiClient from './client' +import type { Account, SubscriptionDetails, AccountMember, AccountInvite } from '@/types' + +export const accountsApi = { + async getMyAccount(): Promise { + const response = await apiClient.get('/accounts/me') + return response.data + }, + + async getMySubscription(): Promise { + const response = await apiClient.get('/accounts/me/subscription') + return response.data + }, + + async updateMyAccount(data: { name?: string }): Promise { + const response = await apiClient.patch('/accounts/me', data) + return response.data + }, + + async getMembers(): Promise { + const response = await apiClient.get('/accounts/me/members') + return response.data + }, + + async updateMemberRole(userId: string, role: string): Promise { + const response = await apiClient.patch( + `/accounts/me/members/${userId}/role`, + { role } + ) + return response.data + }, + + async removeMember(userId: string): Promise { + await apiClient.delete(`/accounts/me/members/${userId}`) + }, + + async createInvite(data: { email: string; role: string }): Promise { + const response = await apiClient.post('/accounts/me/invites', data) + return response.data + }, + + async getInvites(): Promise { + const response = await apiClient.get('/accounts/me/invites') + return response.data + }, +} + +export default accountsApi diff --git a/frontend/src/api/categories.ts b/frontend/src/api/categories.ts index f3b38085..e227e9c6 100644 --- a/frontend/src/api/categories.ts +++ b/frontend/src/api/categories.ts @@ -2,9 +2,9 @@ import apiClient from './client' import type { Category, CategoryListItem, CategoryCreate, CategoryUpdate } from '@/types' export const categoriesApi = { - async list(includeInactive = false, teamOnly = false): Promise { + async list(includeInactive = false, accountOnly = false): Promise { const response = await apiClient.get('/categories', { - params: { include_inactive: includeInactive, team_only: teamOnly }, + params: { include_inactive: includeInactive, account_only: accountOnly }, }) return response.data }, diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 267fafa1..a03da465 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -8,3 +8,4 @@ export { default as categoriesApi } from './categories' export { default as foldersApi } from './folders' export { default as stepsApi } from './steps' export { default as stepCategoriesApi } from './stepCategories' +export { default as accountsApi } from './accounts' diff --git a/frontend/src/api/tags.ts b/frontend/src/api/tags.ts index 2907209c..5dfe46fe 100644 --- a/frontend/src/api/tags.ts +++ b/frontend/src/api/tags.ts @@ -2,16 +2,16 @@ import apiClient from './client' import type { Tag, TagListItem, TagCreate, TagAssignment } from '@/types' export const tagsApi = { - async list(includeTeam = true): Promise { + async list(includeAccount = true): Promise { const response = await apiClient.get('/tags', { - params: { include_team: includeTeam }, + params: { include_account: includeAccount }, }) return response.data }, - async search(query: string, limit = 10, includeTeam = true): Promise { + async search(query: string, limit = 10, includeAccount = true): Promise { const response = await apiClient.get('/tags/search', { - params: { q: query, limit, include_team: includeTeam }, + params: { q: query, limit, include_account: includeAccount }, }) return response.data }, diff --git a/frontend/src/components/common/UpgradePrompt.tsx b/frontend/src/components/common/UpgradePrompt.tsx new file mode 100644 index 00000000..e67c17fa --- /dev/null +++ b/frontend/src/components/common/UpgradePrompt.tsx @@ -0,0 +1,32 @@ +import { cn } from '@/lib/utils' +import { useSubscription } from '@/hooks/useSubscription' + +interface UpgradePromptProps { + feature: string // e.g., "create more trees", "start more sessions" + className?: string +} + +export function UpgradePrompt({ feature, className }: UpgradePromptProps) { + const { plan } = useSubscription() + + return ( +
+

Plan Limit Reached

+

+ Your {plan} plan doesn't allow you to {feature}. Upgrade your plan to continue. +

+ +
+ ) +} diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index b543f913..fbec1a01 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -49,6 +49,7 @@ export function AppLayout() { const navItems = [ { path: '/trees', label: 'Trees' }, { path: '/sessions', label: 'Sessions' }, + { path: '/account', label: 'Account' }, { path: '/settings', label: 'Settings' }, ] @@ -98,12 +99,12 @@ export function AppLayout() { className={cn( 'hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-block', effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400', - effectiveRole === 'team_admin' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400', + effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400', effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400' )} > {effectiveRole === 'super_admin' ? 'Super Admin' : - effectiveRole === 'team_admin' ? 'Team Admin' : + effectiveRole === 'owner' ? 'Owner' : 'Viewer'} )} @@ -158,12 +159,12 @@ export function AppLayout() { className={cn( 'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium', effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400', - effectiveRole === 'team_admin' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400', + effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400', effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400' )} > {effectiveRole === 'super_admin' ? 'Super Admin' : - effectiveRole === 'team_admin' ? 'Team Admin' : + effectiveRole === 'owner' ? 'Owner' : 'Viewer'} )} diff --git a/frontend/src/components/layout/ProtectedRoute.tsx b/frontend/src/components/layout/ProtectedRoute.tsx index 96163c44..c432c3b9 100644 --- a/frontend/src/components/layout/ProtectedRoute.tsx +++ b/frontend/src/components/layout/ProtectedRoute.tsx @@ -27,7 +27,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) if (requiredRole) { const ROLE_HIERARCHY: Record = { super_admin: 4, - team_admin: 3, + owner: 3, engineer: 2, viewer: 1, } diff --git a/frontend/src/components/subscription/CheckoutButton.tsx b/frontend/src/components/subscription/CheckoutButton.tsx new file mode 100644 index 00000000..c3e2d332 --- /dev/null +++ b/frontend/src/components/subscription/CheckoutButton.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/lib/utils' + +interface CheckoutButtonProps { + plan: 'pro' | 'team' + className?: string +} + +export function CheckoutButton({ plan, className }: CheckoutButtonProps) { + const planLabels = { pro: 'Pro', team: 'Team' } + + return ( + + ) +} diff --git a/frontend/src/components/tree-editor/TreeMetadataForm.tsx b/frontend/src/components/tree-editor/TreeMetadataForm.tsx index eb99831d..2adc9fff 100644 --- a/frontend/src/components/tree-editor/TreeMetadataForm.tsx +++ b/frontend/src/components/tree-editor/TreeMetadataForm.tsx @@ -119,7 +119,7 @@ export function TreeMetadataForm() { {categories.map((cat) => ( ))} diff --git a/frontend/src/hooks/usePermissions.ts b/frontend/src/hooks/usePermissions.ts index 5dc39fb2..55706d4a 100644 --- a/frontend/src/hooks/usePermissions.ts +++ b/frontend/src/hooks/usePermissions.ts @@ -1,18 +1,18 @@ /** * Centralized permissions hook for ResolutionFlow. * - * Role hierarchy: super_admin > team_admin > engineer > viewer + * Role hierarchy: super_admin > owner > engineer > viewer * * Mirrors backend logic in backend/app/core/permissions.py */ import { useAuthStore } from '@/store/authStore' import type { User } from '@/types' -export type EffectiveRole = 'super_admin' | 'team_admin' | 'engineer' | 'viewer' +export type EffectiveRole = 'super_admin' | 'owner' | 'engineer' | 'viewer' const ROLE_HIERARCHY: Record = { super_admin: 4, - team_admin: 3, + owner: 3, engineer: 2, viewer: 1, } @@ -20,7 +20,7 @@ const ROLE_HIERARCHY: Record = { function getEffectiveRole(user: User | null): EffectiveRole { if (!user) return 'viewer' if (user.is_super_admin) return 'super_admin' - if (user.is_team_admin && user.team_id) return 'team_admin' + if (user.account_role === 'owner') return 'owner' return user.role as EffectiveRole } @@ -37,7 +37,7 @@ export function usePermissions() { return { effectiveRole, isSuperAdmin: effectiveRole === 'super_admin', - isTeamAdmin: effectiveRole === 'team_admin' || effectiveRole === 'super_admin', + isAccountOwner: effectiveRole === 'owner' || effectiveRole === 'super_admin', isEngineer: hasMinimumRole(user, 'engineer'), isViewer: effectiveRole === 'viewer', @@ -46,12 +46,12 @@ export function usePermissions() { canCreateSteps: hasMinimumRole(user, 'engineer'), // Resource-specific checks - canEditTree: (tree: { author_id: string | null; team_id?: string | null }) => { + canEditTree: (tree: { author_id: string | null; account_id?: string | null }) => { if (!user) return false if (user.is_super_admin) return true if (!hasMinimumRole(user, 'engineer')) return false if (tree.author_id && tree.author_id === user.id) return true - if (user.is_team_admin && tree.team_id === user.team_id && user.team_id) return true + if (user.account_role === 'owner' && tree.account_id === user.account_id && user.account_id) return true return false }, @@ -68,8 +68,8 @@ export function usePermissions() { }, // Management permissions - canManageCategories: hasMinimumRole(user, 'team_admin'), + canManageCategories: hasMinimumRole(user, 'owner'), canManageGlobalCategories: effectiveRole === 'super_admin', - canManageTeam: effectiveRole === 'super_admin' || (effectiveRole === 'team_admin'), + canManageAccount: effectiveRole === 'super_admin' || effectiveRole === 'owner', } } diff --git a/frontend/src/hooks/useSubscription.ts b/frontend/src/hooks/useSubscription.ts new file mode 100644 index 00000000..715a86bb --- /dev/null +++ b/frontend/src/hooks/useSubscription.ts @@ -0,0 +1,45 @@ +import { useAuthStore } from '@/store/authStore' + +export function useSubscription() { + const subscription = useAuthStore((s) => s.subscription) + + const plan = subscription?.subscription.plan ?? 'free' + const limits = subscription?.limits ?? null + const usage = subscription?.usage ?? null + const isActive = subscription?.subscription.status === 'active' || subscription?.subscription.status === 'trialing' + + const isPaidPlan = plan === 'pro' || plan === 'team' + + const canUseFeature = (feature: 'custom_branding' | 'priority_support'): boolean => { + if (!limits) return false + return limits[feature] + } + + const isAtTreeLimit = (): boolean => { + if (!limits || !usage) return false + if (limits.max_trees === null) return false // unlimited + return usage.tree_count >= limits.max_trees + } + + const isAtSessionLimit = (): boolean => { + if (!limits || !usage) return false + if (limits.max_sessions_per_month === null) return false + return usage.session_count_this_month >= limits.max_sessions_per_month + } + + const formatLimit = (value: number | null): string => { + return value === null ? 'Unlimited' : String(value) + } + + return { + plan, + limits, + usage, + isActive, + isPaidPlan, + canUseFeature, + isAtTreeLimit, + isAtSessionLimit, + formatLimit, + } +} diff --git a/frontend/src/pages/AccountSettingsPage.tsx b/frontend/src/pages/AccountSettingsPage.tsx new file mode 100644 index 00000000..d58e9058 --- /dev/null +++ b/frontend/src/pages/AccountSettingsPage.tsx @@ -0,0 +1,494 @@ +import { useEffect, useState } from 'react' +import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X } from 'lucide-react' +import { accountsApi } from '@/api' +import type { Account, AccountMember, AccountInvite } from '@/types' +import { cn } from '@/lib/utils' +import { usePermissions } from '@/hooks/usePermissions' +import { useSubscription } from '@/hooks/useSubscription' +import { useAuthStore } from '@/store/authStore' +import { CheckoutButton } from '@/components/subscription/CheckoutButton' + +export function AccountSettingsPage() { + const { isAccountOwner } = usePermissions() + const { plan, limits, usage } = useSubscription() + const subscription = useAuthStore((s) => s.subscription) + + const [account, setAccount] = useState(null) + const [members, setMembers] = useState([]) + const [invites, setInvites] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + // Account name editing + const [isEditingName, setIsEditingName] = useState(false) + const [editedName, setEditedName] = useState('') + const [isSavingName, setIsSavingName] = useState(false) + + // Invite form + const [inviteEmail, setInviteEmail] = useState('') + const [inviteRole, setInviteRole] = useState('engineer') + const [isInviting, setIsInviting] = useState(false) + const [inviteError, setInviteError] = useState(null) + const [inviteSuccess, setInviteSuccess] = useState(null) + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setIsLoading(true) + setError(null) + try { + const accountData = await accountsApi.getMyAccount() + setAccount(accountData) + setEditedName(accountData.name) + + if (isAccountOwner) { + const [membersData, invitesData] = await Promise.all([ + accountsApi.getMembers(), + accountsApi.getInvites(), + ]) + setMembers(membersData) + setInvites(invitesData) + } + } catch (err) { + setError('Failed to load account information') + console.error(err) + } finally { + setIsLoading(false) + } + } + + const handleSaveName = async () => { + if (!editedName.trim() || editedName === account?.name) { + setIsEditingName(false) + return + } + setIsSavingName(true) + try { + const updated = await accountsApi.updateMyAccount({ name: editedName.trim() }) + setAccount(updated) + setIsEditingName(false) + } catch (err) { + console.error('Failed to update account name:', err) + } finally { + setIsSavingName(false) + } + } + + const handleInvite = async (e: React.FormEvent) => { + e.preventDefault() + if (!inviteEmail.trim()) return + + setIsInviting(true) + setInviteError(null) + setInviteSuccess(null) + try { + await accountsApi.createInvite({ email: inviteEmail.trim(), role: inviteRole }) + setInviteSuccess(`Invitation sent to ${inviteEmail}`) + setInviteEmail('') + // Refresh invites list + const invitesData = await accountsApi.getInvites() + setInvites(invitesData) + } catch (err) { + setInviteError('Failed to send invitation') + console.error(err) + } finally { + setIsInviting(false) + } + } + + const handleRemoveMember = async (userId: string) => { + try { + await accountsApi.removeMember(userId) + setMembers(members.filter((m) => m.id !== userId)) + } catch (err) { + console.error('Failed to remove member:', err) + } + } + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (error) { + return ( +
+
+
+ + {error} +
+
+
+ ) + } + + const sub = subscription?.subscription + + return ( +
+
+
+ +

Account Settings

+
+

+ Manage your account, subscription, and team +

+
+ +
+ {/* Account Info Section */} +
+

Account Information

+ +
+ {/* Account Name */} +
+ + {isEditingName ? ( +
+ setEditedName(e.target.value)} + className={cn( + 'flex-1 rounded-md border border-input bg-background px-3 py-2', + 'text-foreground placeholder:text-muted-foreground', + 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary' + )} + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveName() + if (e.key === 'Escape') { + setEditedName(account?.name ?? '') + setIsEditingName(false) + } + }} + /> + + +
+ ) : ( +
+ {account?.name} + {isAccountOwner && ( + + )} +
+ )} +
+ + {/* Display Code */} +
+ +

+ {account?.display_code} +

+
+
+
+ + {/* Subscription Section */} +
+

Subscription

+ +
+ {/* Plan & Status */} +
+ + + {plan.charAt(0).toUpperCase() + plan.slice(1)} Plan + + {sub && ( + + {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} + + )} +
+ + {sub?.current_period_end && ( +

+ Current period ends: {new Date(sub.current_period_end).toLocaleDateString()} +

+ )} + + {/* Usage Stats */} + {limits && usage && ( +
+ + + +
+ )} + + {/* Upgrade buttons */} + {plan === 'free' && ( +
+ + +
+ )} + {plan === 'pro' && ( +
+ +
+ )} +
+
+ + {/* Team Members Section (owners only) */} + {isAccountOwner && ( +
+
+ +

Team Members

+
+ + {members.length === 0 ? ( +

No team members yet.

+ ) : ( +
+ {members.map((member) => ( +
+
+

{member.name}

+

{member.email}

+
+
+ + {member.account_role} + + {!member.is_active && ( + + Inactive + + )} + {member.account_role !== 'owner' && ( + + )} +
+
+ ))} +
+ )} +
+ )} + + {/* Invite Member Section (owners only) */} + {isAccountOwner && ( +
+
+ +

Invite Member

+
+ +
+
+ setInviteEmail(e.target.value)} + required + className={cn( + 'flex-1 rounded-md border border-input bg-background px-3 py-2', + 'text-foreground placeholder:text-muted-foreground', + 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary' + )} + /> + + +
+ + {inviteError && ( +

{inviteError}

+ )} + {inviteSuccess && ( +

{inviteSuccess}

+ )} +
+ + {/* Pending Invites */} + {invites.length > 0 && ( +
+

Pending Invites

+
+ {invites + .filter((inv) => !inv.used_at) + .map((invite) => ( +
+
+

{invite.email}

+

+ Expires {new Date(invite.expires_at).toLocaleDateString()} +

+
+ + {invite.role} + +
+ ))} +
+
+ )} +
+ )} +
+
+ ) +} + +/** Small helper component for usage stat display */ +function UsageStat({ + label, + current, + max, +}: { + label: string + current: number + max: number | null +}) { + const isUnlimited = max === null + const percentage = isUnlimited ? 0 : Math.min((current / max) * 100, 100) + const isNearLimit = !isUnlimited && percentage >= 80 + const isAtLimit = !isUnlimited && current >= max + + return ( +
+

{label}

+

+ {current} + + {' '}/ {isUnlimited ? 'Unlimited' : max} + +

+ {!isUnlimited && ( +
+
+
+ )} +
+ ) +} + +export default AccountSettingsPage diff --git a/frontend/src/pages/TreeEditorPage.tsx b/frontend/src/pages/TreeEditorPage.tsx index 374da3fb..781fa26f 100644 --- a/frontend/src/pages/TreeEditorPage.tsx +++ b/frontend/src/pages/TreeEditorPage.tsx @@ -102,7 +102,7 @@ export function TreeEditorPage() { setLoading(true) try { const tree = await treesApi.get(id) - if (!canEditTree({ author_id: tree.author_id, team_id: tree.team_id })) { + if (!canEditTree({ author_id: tree.author_id, account_id: tree.account_id })) { navigate('/trees') return } diff --git a/frontend/src/pages/TreeLibraryPage.tsx b/frontend/src/pages/TreeLibraryPage.tsx index 30b89c8c..84b0271c 100644 --- a/frontend/src/pages/TreeLibraryPage.tsx +++ b/frontend/src/pages/TreeLibraryPage.tsx @@ -137,6 +137,7 @@ export function TreeLibraryPage() { try { await treesApi.delete(treeToDelete.id) setTrees(trees.filter((t) => t.id !== treeToDelete.id)) + window.dispatchEvent(new Event('folder-changed')) } catch (err) { console.error('Failed to delete tree:', err) setError('Failed to delete tree') @@ -352,7 +353,7 @@ export function TreeLibraryPage() {
- {canEditTree({ author_id: tree.author_id, team_id: tree.team_id }) && ( + {canEditTree({ author_id: tree.author_id, account_id: tree.account_id }) && ( , }, + { + path: 'account', + element: , + }, ], }, ]) diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts index 457697c3..cb8976ee 100644 --- a/frontend/src/store/authStore.ts +++ b/frontend/src/store/authStore.ts @@ -1,11 +1,14 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -import type { User, Token, UserCreate, UserLogin } from '@/types' +import type { User, Token, UserCreate, UserLogin, Account, SubscriptionDetails } from '@/types' import { authApi } from '@/api' +import { apiClient } from '@/api' interface AuthState { user: User | null token: Token | null + account: Account | null + subscription: SubscriptionDetails | null isAuthenticated: boolean isLoading: boolean error: string | null @@ -25,6 +28,8 @@ export const useAuthStore = create()( (set, get) => ({ user: null, token: null, + account: null, + subscription: null, isAuthenticated: false, isLoading: false, error: null, @@ -70,15 +75,30 @@ export const useAuthStore = create()( } finally { localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') - set({ user: null, token: null, isAuthenticated: false, error: null }) + set({ user: null, token: null, account: null, subscription: null, isAuthenticated: false, error: null }) } }, fetchUser: async () => { set({ isLoading: true }) try { - const user = await authApi.me() - set({ user, isLoading: false }) + const [userResult, accountResult, subscriptionResult] = await Promise.allSettled([ + authApi.me(), + apiClient.get('/accounts/me').then(r => r.data), + apiClient.get('/accounts/me/subscription').then(r => r.data), + ]) + + const user = userResult.status === 'fulfilled' ? userResult.value : null + const account = accountResult.status === 'fulfilled' ? accountResult.value : null + const subscription = subscriptionResult.status === 'fulfilled' ? subscriptionResult.value : null + + if (!user) { + // User fetch failed — propagate the error + const reason = userResult.status === 'rejected' ? userResult.reason : new Error('Failed to fetch user') + throw reason + } + + set({ user, account, subscription, isLoading: false }) } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Failed to fetch user' set({ error: message, isLoading: false }) @@ -95,6 +115,8 @@ export const useAuthStore = create()( partialize: (state) => ({ token: state.token, isAuthenticated: state.isAuthenticated, + account: state.account, + subscription: state.subscription, }), } ) diff --git a/frontend/src/types/account.ts b/frontend/src/types/account.ts new file mode 100644 index 00000000..4841530e --- /dev/null +++ b/frontend/src/types/account.ts @@ -0,0 +1,62 @@ +export interface Account { + id: string + name: string + display_code: string + owner_id: string + created_at: string + updated_at: string +} + +export interface Subscription { + id: string + account_id: string + plan: 'free' | 'pro' | 'team' + status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'orphaned' + current_period_start: string | null + current_period_end: string | null + created_at: string + updated_at: string +} + +export interface PlanLimits { + plan: string + max_trees: number | null + max_sessions_per_month: number | null + max_users: number | null + custom_branding: boolean + priority_support: boolean + export_formats: string[] +} + +export interface SubscriptionDetails { + subscription: Subscription + limits: PlanLimits + usage: { + tree_count: number + session_count_this_month: number + user_count: number + } +} + +export interface AccountInvite { + id: string + account_id: string + email: string + role: 'engineer' | 'viewer' + code: string + invited_by_id: string + accepted_by_id: string | null + expires_at: string + used_at: string | null + created_at: string +} + +export interface AccountMember { + id: string + email: string + name: string + account_role: string + is_active: boolean + created_at: string + last_login: string | null +} diff --git a/frontend/src/types/category.ts b/frontend/src/types/category.ts index 6a318768..906a9b14 100644 --- a/frontend/src/types/category.ts +++ b/frontend/src/types/category.ts @@ -5,7 +5,7 @@ export interface Category { name: string slug: string description: string | null - team_id: string | null + account_id: string | null display_order: number is_active: boolean created_at: string @@ -18,7 +18,7 @@ export interface CategoryListItem { name: string slug: string description: string | null - team_id: string | null + account_id: string | null display_order: number is_active: boolean tree_count: number @@ -27,7 +27,7 @@ export interface CategoryListItem { export interface CategoryCreate { name: string description?: string | null - team_id?: string | null + account_id?: string | null } export interface CategoryUpdate { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 9c1f892b..0172e43d 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -7,6 +7,7 @@ export * from './tag' export * from './category' export * from './folder' export * from './step' +export type { Account, Subscription, PlanLimits, SubscriptionDetails, AccountInvite, AccountMember } from './account' // API response wrapper types export interface PaginatedResponse { diff --git a/frontend/src/types/step.ts b/frontend/src/types/step.ts index 19ead785..d9f12f00 100644 --- a/frontend/src/types/step.ts +++ b/frontend/src/types/step.ts @@ -56,7 +56,7 @@ export interface StepCategory { name: string description?: string display_order: number - team_id?: string + account_id?: string is_active: boolean } diff --git a/frontend/src/types/tag.ts b/frontend/src/types/tag.ts index e33e2a9a..4b8813c7 100644 --- a/frontend/src/types/tag.ts +++ b/frontend/src/types/tag.ts @@ -4,7 +4,7 @@ export interface Tag { id: string name: string slug: string - team_id: string | null + account_id: string | null usage_count: number created_at: string } @@ -13,13 +13,13 @@ export interface TagListItem { id: string name: string slug: string - team_id: string | null + account_id: string | null usage_count: number } export interface TagCreate { name: string - team_id?: string | null + account_id?: string | null } export interface TagAssignment { diff --git a/frontend/src/types/tree.ts b/frontend/src/types/tree.ts index 1774068e..f62789c6 100644 --- a/frontend/src/types/tree.ts +++ b/frontend/src/types/tree.ts @@ -67,7 +67,7 @@ export interface Tree { tags: string[] tree_structure: TreeStructure author_id: string | null - team_id: string | null + account_id: string | null is_active: boolean is_public: boolean is_default: boolean @@ -86,7 +86,7 @@ export interface TreeListItem { category_info: CategoryInfo | null tags: string[] author_id: string | null - team_id: string | null + account_id: string | null is_active: boolean is_public: boolean is_default: boolean diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index b5fe4a59..89136508 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -6,8 +6,8 @@ export interface User { name: string role: UserRole is_super_admin: boolean - is_team_admin: boolean - team_id: string | null + account_id: string + account_role: 'owner' | 'engineer' | 'viewer' created_at: string last_login: string | null }