diff --git a/packages/web/src/components/admin/GrantList.tsx b/packages/web/src/components/admin/GrantList.tsx index ea17abae..27278494 100644 --- a/packages/web/src/components/admin/GrantList.tsx +++ b/packages/web/src/components/admin/GrantList.tsx @@ -4,18 +4,10 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { formatDateTime } from '@/lib/formatDate'; - -interface Grant { - id: string; - type: string; - startsAt?: string | number | Date; - expiresAt?: string | number | Date; - createdAt?: string | number | Date; - revokedAt?: string | number | Date | null; -} +import type { AdminOrgGrant } from '@/server/functions/admin-orgs.server'; interface GrantListProps { - grants: Grant[]; + grants: AdminOrgGrant[]; loading: boolean; isLoading: boolean; onRevoke: (_grantId: string) => void; diff --git a/packages/web/src/components/admin/OrgBillingReconcilePanel.tsx b/packages/web/src/components/admin/OrgBillingReconcilePanel.tsx index deaffc04..240cd87c 100644 --- a/packages/web/src/components/admin/OrgBillingReconcilePanel.tsx +++ b/packages/web/src/components/admin/OrgBillingReconcilePanel.tsx @@ -10,40 +10,7 @@ import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; -interface StuckState { - type: string; - severity: string; - description: string; - ageMinutes?: number; - threshold?: number; - subscriptionId?: string; - stripeSubscriptionId?: string; - stripeEventId?: string; - localStatus?: string; - stripeStatus?: string; -} - -interface ReconcileSummary { - stuckStateCount?: number; - failedWebhooks?: number; - ignoredWebhooks?: number; -} - -interface StripeComparison { - error?: string; - noActiveSubscription?: boolean; - match?: boolean; - localStatus?: string; - stripeStatus?: string; -} - -interface ReconcileData { - stuckStates?: StuckState[]; - summary?: ReconcileSummary; - stripeComparison?: StripeComparison; -} - -const getSeverityIcon = (severity: string) => { +const getSeverityIcon = (severity: string | undefined) => { switch (severity) { case 'critical': return AlertTriangleIcon; @@ -55,7 +22,7 @@ const getSeverityIcon = (severity: string) => { } }; -const getSeverityVariant = (severity: string) => { +const getSeverityVariant = (severity: string | undefined) => { switch (severity) { case 'critical': return 'destructive' as const; @@ -91,7 +58,13 @@ function ThresholdField({ ); } -export function OrgBillingReconcilePanel({ orgId }: { orgId: string }) { +export function OrgBillingReconcilePanel({ + orgId, + onHide, +}: { + orgId: string; + onHide?: () => void; +}) { const [incompleteThreshold, setIncompleteThreshold] = useState(30); const [checkoutNoSubThreshold, setCheckoutNoSubThreshold] = useState(15); const [processingLagThreshold, setProcessingLagThreshold] = useState(5); @@ -104,9 +77,9 @@ export function OrgBillingReconcilePanel({ orgId }: { orgId: string }) { processingLagThreshold, }); - const reconcileData = reconcileQuery.data as ReconcileData | undefined; + const reconcileData = reconcileQuery.data; const stuckStates = reconcileData?.stuckStates ?? []; - const summary = reconcileData?.summary ?? {}; + const summary = reconcileData?.summary; const isLoading = reconcileQuery.isLoading; return ( @@ -114,18 +87,25 @@ export function OrgBillingReconcilePanel({ orgId }: { orgId: string }) { title='Billing Reconciliation' padded action={ - + <> + {onHide && ( + + )} + + } >
@@ -163,7 +143,7 @@ export function OrgBillingReconcilePanel({ orgId }: { orgId: string }) {
- + s.severity === 'critical').length} @@ -178,12 +158,12 @@ export function OrgBillingReconcilePanel({ orgId }: { orgId: string }) { />
diff --git a/packages/web/src/components/admin/OrgBillingSummary.tsx b/packages/web/src/components/admin/OrgBillingSummary.tsx index e4c7ef15..12a60080 100644 --- a/packages/web/src/components/admin/OrgBillingSummary.tsx +++ b/packages/web/src/components/admin/OrgBillingSummary.tsx @@ -1,24 +1,10 @@ import { AdminPanel, AdminField, AdminFieldGrid } from '@/components/admin/ui'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; - -interface BillingPlan { - name?: string; - entitlements?: Record; - quotas?: Record; -} - -interface BillingState { - plan?: BillingPlan; - effectivePlanId?: string; - accessMode?: 'full' | 'readOnly'; - source?: 'free' | 'subscription' | 'grant'; - subscription?: { plan?: string } | null; - grant?: { type?: string } | null; -} +import type { AdminOrgBillingState } from '@/server/functions/admin-orgs.server'; interface OrgBillingSummaryProps { - billing: BillingState | null | undefined; + billing: AdminOrgBillingState | null | undefined; isLoading?: boolean; } @@ -40,7 +26,7 @@ export function OrgBillingSummary({ billing, isLoading }: OrgBillingSummaryProps const sourceReason = billingSource === 'subscription' && billing.subscription ? - `Active subscription (${billing.subscription.plan})` + `Active subscription (${billing.plan?.name})` : billingSource === 'grant' && billing.grant ? `Active grant (${billing.grant.type})` : 'No active subscription or grant'; diff --git a/packages/web/src/components/admin/SubscriptionList.tsx b/packages/web/src/components/admin/SubscriptionList.tsx index 8711aeb3..03de2bb0 100644 --- a/packages/web/src/components/admin/SubscriptionList.tsx +++ b/packages/web/src/components/admin/SubscriptionList.tsx @@ -4,29 +4,15 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { formatDateTime } from '@/lib/formatDate'; - -interface Subscription { - id: string; - plan: string; - status: string; - periodStart?: string | number | Date; - periodEnd?: string | number | Date; - cancelAtPeriodEnd?: boolean; - createdAt?: string | number | Date; - updatedAt?: string | number | Date | null; - canceledAt?: string | number | Date | null; - endedAt?: string | number | Date | null; - stripeCustomerId?: string; - stripeSubscriptionId?: string; -} +import type { AdminOrgSubscription } from '@/server/functions/admin-orgs.server'; interface SubscriptionListProps { - subscriptions?: Subscription[]; + subscriptions?: AdminOrgSubscription[]; effectiveSubscriptionId?: string; loading?: boolean; isLoading?: boolean; onCancel: (_subscriptionId: string) => void; - onEdit: (_subscription: Subscription) => void; + onEdit: (_subscription: AdminOrgSubscription) => void; } function StripeId({ label, value }: { label: string; value: string }) { @@ -92,7 +78,10 @@ export function SubscriptionList({ )} {subscription.stripeSubscriptionId && ( - + )} )} diff --git a/packages/web/src/components/admin/UserTable.tsx b/packages/web/src/components/admin/UserTable.tsx index 47bbd586..294c8740 100644 --- a/packages/web/src/components/admin/UserTable.tsx +++ b/packages/web/src/components/admin/UserTable.tsx @@ -6,20 +6,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip import { AdminDataTable, type AdminColumnDef } from '@/components/admin/ui'; import { Badge } from '@/components/ui/badge'; import { formatDate } from '@/lib/formatDate'; - -interface UserRow { - id: string; - name?: string; - username?: string; - email?: string; - emailVerified?: boolean; - avatarUrl?: string; - image?: string; - providers?: string[]; - banned?: boolean; - stripeCustomerId?: string; - createdAt?: string | number; -} +import type { AdminUserListItem } from '@/server/functions/admin-users.server'; interface ProviderInfo { name: string; @@ -33,7 +20,7 @@ const PROVIDER_INFO: Record = { }; interface UserTableProps { - users: UserRow[]; + users: AdminUserListItem[]; loading?: boolean; refreshing?: boolean; skeletonRows?: number; @@ -51,7 +38,7 @@ export function UserTable({ }: UserTableProps) { const navigate = useNavigate(); - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { accessorKey: 'name', @@ -61,7 +48,7 @@ export function UserTable({ return (
@@ -182,7 +169,7 @@ export function UserTable({ variant={variant} emptyState={emptyState ?? 'No users found'} enableSorting - onRowClick={(row: UserRow) => + onRowClick={(row: AdminUserListItem) => navigate({ to: '/admin/users/$userId' as string, params: { userId: row.id } as Record, diff --git a/packages/web/src/components/admin/orgs/OrgMembersSection.tsx b/packages/web/src/components/admin/orgs/OrgMembersSection.tsx new file mode 100644 index 00000000..2a042435 --- /dev/null +++ b/packages/web/src/components/admin/orgs/OrgMembersSection.tsx @@ -0,0 +1,91 @@ +import { Link } from '@tanstack/react-router'; +import { AdminEmpty, AdminPanel, ADMIN_TH, ADMIN_TD, ADMIN_TD_MUTED } from '@/components/admin/ui'; +import { UserAvatar } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from '@/components/ui/table'; +import { formatDate } from '@/lib/formatDate'; +import type { AdminOrgMember } from '@/server/functions/admin-orgs.server'; + +interface OrgMembersSectionProps { + members?: AdminOrgMember[]; + total: number; + isLoading?: boolean; +} + +export function OrgMembersSection({ members, total, isLoading }: OrgMembersSectionProps) { + const rows = members ?? []; + + return ( + + Showing the {rows.length} most recently joined. + + : undefined + } + > + {isLoading ? +
+ +
+ : rows.length === 0 ? + + : + + + User + Role + Joined + + + + {rows.map(member => ( + + +
+ +
+
+ } + className='text-foreground hover:text-primary font-medium transition-colors' + > + {member.userName || member.userEmail} + + {member.userBanned && Banned} +
+

{member.userEmail}

+
+
+
+ + + {member.role ?? 'member'} + + + + {formatDate(member.joinedAt)} + +
+ ))} +
+
+ } +
+ ); +} diff --git a/packages/web/src/components/admin/orgs/OrgProjectsSection.tsx b/packages/web/src/components/admin/orgs/OrgProjectsSection.tsx new file mode 100644 index 00000000..7b2d7ac1 --- /dev/null +++ b/packages/web/src/components/admin/orgs/OrgProjectsSection.tsx @@ -0,0 +1,82 @@ +import { Link } from '@tanstack/react-router'; +import { AdminEmpty, AdminPanel, ADMIN_TH, ADMIN_TD, ADMIN_TD_MUTED } from '@/components/admin/ui'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from '@/components/ui/table'; +import { formatDate } from '@/lib/formatDate'; +import type { AdminOrgProject } from '@/server/functions/admin-orgs.server'; + +interface OrgProjectsSectionProps { + projects?: AdminOrgProject[]; + total: number; + isLoading?: boolean; +} + +export function OrgProjectsSection({ projects, total, isLoading }: OrgProjectsSectionProps) { + const rows = projects ?? []; + + return ( + + Showing the {rows.length} most recently created. + + : undefined + } + > + {isLoading ? +
+ +
+ : rows.length === 0 ? + + : + + + Project + Created by + Created + + + + {rows.map(project => ( + + + } + className='text-foreground hover:text-primary font-medium transition-colors' + > + {project.name} + + + + {project.creatorName || project.creatorEmail ? + } + className='text-muted-foreground hover:text-primary transition-colors' + > + {project.creatorName || project.creatorEmail} + + : -} + + + {formatDate(project.createdAt)} + + + ))} + +
+ } +
+ ); +} diff --git a/packages/web/src/components/admin/projects/ProjectFilesSection.tsx b/packages/web/src/components/admin/projects/ProjectFilesSection.tsx index eb64e6aa..536bdc29 100644 --- a/packages/web/src/components/admin/projects/ProjectFilesSection.tsx +++ b/packages/web/src/components/admin/projects/ProjectFilesSection.tsx @@ -10,9 +10,9 @@ import { } from '@/components/ui/table'; import { formatFileSize } from '@corates/shared'; import { formatDate } from '@/lib/formatDate'; -import type { ProjectFile } from './types'; +import type { AdminProjectFile } from '@/server/functions/admin-projects.server'; -export function ProjectFilesSection({ files }: { files?: ProjectFile[] }) { +export function ProjectFilesSection({ files }: { files?: AdminProjectFile[] }) { const rows = files ?? []; return ( @@ -46,7 +46,7 @@ export function ProjectFilesSection({ files }: { files?: ProjectFile[] }) { params={{ userId: file.uploadedBy } as Record} className='text-primary hover:text-primary/80' > - {file.uploaderDisplayName || file.uploaderName} + {file.uploaderName} : -} diff --git a/packages/web/src/components/admin/projects/ProjectInfoSection.tsx b/packages/web/src/components/admin/projects/ProjectInfoSection.tsx index 593c4f69..210e2e7f 100644 --- a/packages/web/src/components/admin/projects/ProjectInfoSection.tsx +++ b/packages/web/src/components/admin/projects/ProjectInfoSection.tsx @@ -2,11 +2,11 @@ import { Link } from '@tanstack/react-router'; import { AdminPanel, AdminField, AdminFieldGrid, CopyButton } from '@/components/admin/ui'; import { formatFileSize } from '@corates/shared'; import { formatDateTime } from '@/lib/formatDate'; -import type { ProjectData } from './types'; +import type { AdminProjectDetails } from '@/server/functions/admin-projects.server'; interface ProjectInfoSectionProps { - project: ProjectData['project']; - stats: ProjectData['stats']; + project: AdminProjectDetails['project']; + stats: AdminProjectDetails['stats']; } export function ProjectInfoSection({ project, stats }: ProjectInfoSectionProps) { @@ -33,7 +33,7 @@ export function ProjectInfoSection({ project, stats }: ProjectInfoSectionProps) params={{ userId: project.createdBy } as Record} className='text-primary hover:text-primary/80' > - {project.creatorDisplayName || project.creatorName || project.creatorEmail} + {project.creatorName || project.creatorEmail} {formatDateTime(project.createdAt)} diff --git a/packages/web/src/components/admin/projects/ProjectInvitationsSection.tsx b/packages/web/src/components/admin/projects/ProjectInvitationsSection.tsx index b8b9a778..55ac5aad 100644 --- a/packages/web/src/components/admin/projects/ProjectInvitationsSection.tsx +++ b/packages/web/src/components/admin/projects/ProjectInvitationsSection.tsx @@ -10,12 +10,12 @@ import { TableCell, } from '@/components/ui/table'; import { formatDate } from '@/lib/formatDate'; -import type { ProjectInvitation } from './types'; +import type { AdminProjectInvitation } from '@/server/functions/admin-projects.server'; -function invitationStatus(invitation: ProjectInvitation): 'accepted' | 'pending' | 'expired' { +function invitationStatus(invitation: AdminProjectInvitation): 'accepted' | 'pending' | 'expired' { if (invitation.acceptedAt) return 'accepted'; if (!invitation.expiresAt) return 'pending'; - return new Date(invitation.expiresAt * 1000) > new Date() ? 'pending' : 'expired'; + return invitation.expiresAt > new Date() ? 'pending' : 'expired'; } const STATUS_BADGE = { @@ -24,7 +24,11 @@ const STATUS_BADGE = { expired: { variant: 'destructive', label: 'Expired' }, } as const; -export function ProjectInvitationsSection({ invitations }: { invitations?: ProjectInvitation[] }) { +export function ProjectInvitationsSection({ + invitations, +}: { + invitations?: AdminProjectInvitation[]; +}) { const rows = invitations ?? []; return ( @@ -62,7 +66,7 @@ export function ProjectInvitationsSection({ invitations }: { invitations?: Proje params={{ userId: invitation.invitedBy } as Record} className='text-primary hover:text-primary/80' > - {invitation.inviterDisplayName || invitation.inviterName} + {invitation.inviterName} diff --git a/packages/web/src/components/admin/projects/ProjectMembersSection.tsx b/packages/web/src/components/admin/projects/ProjectMembersSection.tsx index 6cd8cf39..7320cec8 100644 --- a/packages/web/src/components/admin/projects/ProjectMembersSection.tsx +++ b/packages/web/src/components/admin/projects/ProjectMembersSection.tsx @@ -13,12 +13,12 @@ import { TableCell, } from '@/components/ui/table'; import { formatDate } from '@/lib/formatDate'; -import type { ProjectMember } from './types'; +import type { AdminProjectMember } from '@/server/functions/admin-projects.server'; interface ProjectMembersSectionProps { - members?: ProjectMember[]; + members?: AdminProjectMember[]; loading: boolean; - onRemove: (member: ProjectMember) => void; + onRemove: (member: AdminProjectMember) => void; } export function ProjectMembersSection({ members, loading, onRemove }: ProjectMembersSectionProps) { @@ -43,8 +43,8 @@ export function ProjectMembersSection({ members, loading, onRemove }: ProjectMem
@@ -53,7 +53,7 @@ export function ProjectMembersSection({ members, loading, onRemove }: ProjectMem params={{ userId: member.userId } as Record} className='text-foreground hover:text-primary font-medium transition-colors' > - {member.userDisplayName || member.userName} + {member.userName}

{member.userEmail}

@@ -74,7 +74,7 @@ export function ProjectMembersSection({ members, loading, onRemove }: ProjectMem className='text-muted-foreground/70 hover:text-destructive' onClick={() => onRemove(member)} disabled={loading} - aria-label={`Remove ${member.userDisplayName || member.userName}`} + aria-label={`Remove ${member.userName}`} > diff --git a/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx b/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx index b901b292..eda97255 100644 --- a/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx +++ b/packages/web/src/components/admin/projects/WorkspaceStorageSection.tsx @@ -2,7 +2,7 @@ import { RefreshCwIcon } from 'lucide-react'; import { AdminPanel, AdminField, AdminFieldGrid, AdminStat } from '@/components/admin/ui'; import { Button } from '@/components/ui/button'; import { formatFileSize } from '@corates/shared'; -import type { WorkspaceStats } from './types'; +import type { WorkspaceStats } from '@/server/functions/admin-projects.server'; interface WorkspaceStorageSectionProps { stats?: WorkspaceStats; diff --git a/packages/web/src/components/admin/projects/types.ts b/packages/web/src/components/admin/projects/types.ts deleted file mode 100644 index 995ca9f3..00000000 --- a/packages/web/src/components/admin/projects/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -export interface ProjectMember { - id: string; - userId: string; - role: string; - userAvatar?: string; - userDisplayName?: string; - userName?: string; - userEmail?: string; - joinedAt?: string | number | Date; -} - -export interface ProjectFile { - id: string; - originalName?: string; - filename?: string; - fileType?: string; - fileSize?: number; - uploadedBy?: string; - uploaderDisplayName?: string; - uploaderName?: string; - createdAt?: string | number | Date; -} - -export interface ProjectInvitation { - id: string; - email: string; - role: string; - grantOrgMembership?: boolean; - acceptedAt?: string | number | Date | null; - expiresAt?: number; - invitedBy: string; - inviterDisplayName?: string; - inviterName?: string; - createdAt?: string | number | Date; -} - -export interface ProjectData { - project: { - id: string; - name: string; - orgId: string; - orgName: string; - orgSlug: string; - createdBy: string; - creatorDisplayName?: string; - creatorName?: string; - creatorEmail?: string; - createdAt?: string | number | Date; - updatedAt?: string | number | Date; - }; - stats: { - memberCount: number; - fileCount: number; - totalStorageBytes: number; - }; - members?: ProjectMember[]; - files?: ProjectFile[]; - invitations?: ProjectInvitation[]; -} - -/** The sync-engine workspace's admin stats (`workspaceAdmin(...).stats()`). */ -export interface WorkspaceStats { - workspaceId: string; - backendId: string; - schemaVersion: number; - currentVersion: number; - rows: { live: number; tombstones: number }; - mutationLogEntries: number; - knownClients: number; - databaseSizeBytes: number; - connections: { total: number; ready: number; presence: number }; - /** The Yjs fields add-on's stats, when mounted. */ - extension?: { - fields: number; - frozenFields: number; - fieldBytes: number; - pendingUpdates: number; - cachedDocs: number; - }; -} diff --git a/packages/web/src/components/admin/users/UserActions.tsx b/packages/web/src/components/admin/users/UserActions.tsx index a4293379..97a116b2 100644 --- a/packages/web/src/components/admin/users/UserActions.tsx +++ b/packages/web/src/components/admin/users/UserActions.tsx @@ -1,9 +1,9 @@ import { LogInIcon, UserCheckIcon, UserXIcon, Trash2Icon } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import type { UserData } from './types'; +import type { AdminUserDetails } from '@/server/functions/admin-users.server'; interface UserActionsProps { - user: UserData['user']; + user: AdminUserDetails['user']; loading: boolean; onImpersonate: () => void; onUnban: () => void; diff --git a/packages/web/src/components/admin/users/UserLinkedAccounts.tsx b/packages/web/src/components/admin/users/UserLinkedAccounts.tsx index f459d637..91260158 100644 --- a/packages/web/src/components/admin/users/UserLinkedAccounts.tsx +++ b/packages/web/src/components/admin/users/UserLinkedAccounts.tsx @@ -1,7 +1,7 @@ import { MailIcon } from 'lucide-react'; import { AdminEmpty, AdminPanel } from '@/components/admin/ui'; import { formatDate } from '@/lib/formatDate'; -import type { UserAccount } from './types'; +import type { AdminUserAccount } from '@/server/functions/admin-users.server'; const PROVIDER_LABEL: Record = { google: 'Google', @@ -14,7 +14,7 @@ const PROVIDER_LOGO: Record = { orcid: '/logos/orcid.svg', }; -export function UserLinkedAccounts({ accounts }: { accounts?: UserAccount[] }) { +export function UserLinkedAccounts({ accounts }: { accounts?: AdminUserAccount[] }) { const rows = accounts ?? []; return ( diff --git a/packages/web/src/components/admin/users/UserOrganizations.tsx b/packages/web/src/components/admin/users/UserOrganizations.tsx index 8e15c685..6f07c091 100644 --- a/packages/web/src/components/admin/users/UserOrganizations.tsx +++ b/packages/web/src/components/admin/users/UserOrganizations.tsx @@ -10,9 +10,9 @@ import { TableCell, } from '@/components/ui/table'; import { formatDate } from '@/lib/formatDate'; -import type { UserOrg } from './types'; +import type { AdminUserOrg } from '@/server/functions/admin-users.server'; -export function UserOrganizations({ orgs }: { orgs?: UserOrg[] }) { +export function UserOrganizations({ orgs }: { orgs?: AdminUserOrg[] }) { const rows = orgs ?? []; return ( @@ -50,9 +50,9 @@ export function UserOrganizations({ orgs }: { orgs?: UserOrg[] }) { {org.billing.accessMode} diff --git a/packages/web/src/components/admin/users/UserProfileSection.tsx b/packages/web/src/components/admin/users/UserProfileSection.tsx index 4c83ac7b..833651d0 100644 --- a/packages/web/src/components/admin/users/UserProfileSection.tsx +++ b/packages/web/src/components/admin/users/UserProfileSection.tsx @@ -1,9 +1,9 @@ import { ExternalLinkIcon } from 'lucide-react'; import { AdminPanel, AdminField, AdminFieldGrid, CopyButton } from '@/components/admin/ui'; import { formatDateTime } from '@/lib/formatDate'; -import type { UserData } from './types'; +import type { AdminUserDetails } from '@/server/functions/admin-users.server'; -export function UserProfileSection({ user }: { user: UserData['user'] }) { +export function UserProfileSection({ user }: { user: AdminUserDetails['user'] }) { return ( diff --git a/packages/web/src/components/admin/users/UserProjects.tsx b/packages/web/src/components/admin/users/UserProjects.tsx index e0a21a4a..1d436378 100644 --- a/packages/web/src/components/admin/users/UserProjects.tsx +++ b/packages/web/src/components/admin/users/UserProjects.tsx @@ -10,9 +10,9 @@ import { TableCell, } from '@/components/ui/table'; import { formatDate } from '@/lib/formatDate'; -import type { UserProject } from './types'; +import type { AdminUserProject } from '@/server/functions/admin-users.server'; -export function UserProjects({ projects }: { projects?: UserProject[] }) { +export function UserProjects({ projects }: { projects?: AdminUserProject[] }) { const rows = projects ?? []; return ( diff --git a/packages/web/src/components/admin/users/UserSessions.tsx b/packages/web/src/components/admin/users/UserSessions.tsx index 721d234e..1d9fabad 100644 --- a/packages/web/src/components/admin/users/UserSessions.tsx +++ b/packages/web/src/components/admin/users/UserSessions.tsx @@ -2,7 +2,7 @@ import { MonitorIcon, LogOutIcon } from 'lucide-react'; import { AdminEmpty, AdminPanel } from '@/components/admin/ui'; import { Button } from '@/components/ui/button'; import { formatDateTime } from '@/lib/formatDate'; -import type { UserSession } from './types'; +import type { AdminUserSession } from '@/server/functions/admin-users.server'; const parseUserAgent = (ua: string | undefined): { browser: string; os: string } => { if (!ua) return { browser: 'Unknown', os: 'Unknown' }; @@ -24,7 +24,7 @@ const parseUserAgent = (ua: string | undefined): { browser: string; os: string } }; interface UserSessionsProps { - sessions?: UserSession[]; + sessions?: AdminUserSession[]; loading: boolean; onRevoke: (sessionId: string) => void; onRevokeAll: () => void; @@ -55,7 +55,7 @@ export function UserSessions({ sessions, loading, onRevoke, onRevokeAll }: UserS {rows.length === 0 ? : rows.map(session => { - const { browser, os } = parseUserAgent(session.userAgent); + const { browser, os } = parseUserAgent(session.userAgent ?? undefined); return (
diff --git a/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx b/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx index 10297b56..91069609 100644 --- a/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx +++ b/packages/web/src/routes/_app/_protected/admin/billing.ledger.tsx @@ -24,26 +24,7 @@ import { SelectValue, } from '@/components/ui/select'; import { formatDateTime } from '@/lib/formatDate'; - -interface LedgerEntry { - receivedAt?: string | number; - processedAt?: string | number; - status: string; - type?: string; - stripeEventId?: string; - orgId?: string; - stripeCustomerId?: string; - stripeSubscriptionId?: string; - stripeCheckoutSessionId?: string; - requestId?: string; - error?: string; - httpStatus?: number; -} - -interface LedgerStats { - total?: number; - byStatus?: Record; -} +import type { AdminLedgerEntry } from '@/server/functions/admin-billing.server'; const STATUS_OPTIONS = [ { value: '', label: 'All Statuses' }, @@ -110,11 +91,11 @@ function AdminBillingLedgerPage() { type: debouncedTypeFilter || undefined, }); - const data = ledgerQuery.data as { entries: LedgerEntry[]; stats: LedgerStats } | undefined; + const data = ledgerQuery.data; const entries = data?.entries || []; - const stats = data?.stats || {}; + const stats = data?.stats; - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { accessorKey: 'receivedAt', @@ -318,12 +299,12 @@ function AdminBillingLedgerPage() { } > - + {STATUS_OPTIONS.slice(1, 5).map(option => ( ))} diff --git a/packages/web/src/routes/_app/_protected/admin/database.tsx b/packages/web/src/routes/_app/_protected/admin/database.tsx index 1168a595..989e341c 100644 --- a/packages/web/src/routes/_app/_protected/admin/database.tsx +++ b/packages/web/src/routes/_app/_protected/admin/database.tsx @@ -15,6 +15,7 @@ import { useAdminTableRows, useAdminTableSchema, } from '@/hooks/useAdminQueries'; +import type { AdminTableColumn } from '@/server/functions/admin-database.server'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -43,28 +44,6 @@ export const Route = createFileRoute('/_app/_protected/admin/database')({ const LIMIT_OPTIONS = [25, 50, 100]; -interface TableInfo { - name: string; - rowCount?: number; -} - -interface ColumnSchema { - name: string; - type?: string; - primaryKey?: boolean; - foreignKey?: { table: string; column: string } | null; -} - -interface TableRowsData { - rows: Array>; - pagination: { - page: number; - limit: number; - totalRows: number; - totalPages: number; - }; -} - const formatCellValue = (value: unknown): string => { if (value === null || value === undefined) return '-'; if (typeof value === 'boolean') return value ? 'true' : 'false'; @@ -83,14 +62,10 @@ function DatabaseViewerPage() { const [filterValue, setFilterValue] = useState(null); const tablesQuery = useAdminDatabaseTables(); - const tables = ((tablesQuery.data as { tables: TableInfo[] } | undefined)?.tables ?? - []) as TableInfo[]; + const tables = tablesQuery.data?.tables ?? []; const schemaQuery = useAdminTableSchema(selectedTable); - const schemaColumns = useMemo( - () => (schemaQuery.data as { columns: ColumnSchema[] } | undefined)?.columns ?? [], - [schemaQuery.data], - ); + const schemaColumns = useMemo(() => schemaQuery.data?.columns ?? [], [schemaQuery.data]); const rowsQuery = useAdminTableRows({ tableName: selectedTable ?? undefined, @@ -102,8 +77,10 @@ function DatabaseViewerPage() { filterValue, }); - const rowsData = rowsQuery.data as TableRowsData | undefined; - const rows = useMemo(() => rowsData?.rows ?? [], [rowsData]); + const rowsData = rowsQuery.data; + // The viewer renders whatever table was picked at runtime, so the rows are + // read by column name rather than as one known shape. + const rows = useMemo(() => (rowsData?.rows ?? []) as Record[], [rowsData]); const pagination = rowsData?.pagination ?? { page: 1, limit, totalRows: 0, totalPages: 0 }; const columns = useMemo(() => { @@ -112,7 +89,7 @@ function DatabaseViewerPage() { }, [rows]); const columnSchemaMap = useMemo(() => { - const map: Record = {}; + const map: Record = {}; for (const col of schemaColumns) { map[col.name] = col; } diff --git a/packages/web/src/routes/_app/_protected/admin/index.tsx b/packages/web/src/routes/_app/_protected/admin/index.tsx index deabbedc..86b52743 100644 --- a/packages/web/src/routes/_app/_protected/admin/index.tsx +++ b/packages/web/src/routes/_app/_protected/admin/index.tsx @@ -9,7 +9,7 @@ export const Route = createFileRoute('/_app/_protected/admin/')({ function AdminDashboard() { const statsQuery = useAdminStats(); - const stats = statsQuery.data as Record | undefined; + const stats = statsQuery.data; return ( diff --git a/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx b/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx index c264a8f5..86e877a3 100644 --- a/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx +++ b/packages/web/src/routes/_app/_protected/admin/orgs.$orgId.tsx @@ -23,7 +23,7 @@ import { AlertDialogAction, } from '@/components/ui/alert-dialog'; import { handleError } from '@/lib/error-utils'; -import { AdminError, AdminPage, AdminStat, AdminStatRow } from '@/components/admin/ui'; +import { AdminError, AdminPage } from '@/components/admin/ui'; import { formatDateInput } from '@/lib/formatDate'; import { OrgBillingSummary } from '@/components/admin/OrgBillingSummary'; import { OrgQuickActions } from '@/components/admin/OrgQuickActions'; @@ -32,6 +32,9 @@ import { SubscriptionDialog } from '@/components/admin/SubscriptionDialog'; import { GrantList } from '@/components/admin/GrantList'; import { GrantDialog } from '@/components/admin/GrantDialog'; import { OrgBillingReconcilePanel } from '@/components/admin/OrgBillingReconcilePanel'; +import { OrgMembersSection } from '@/components/admin/orgs/OrgMembersSection'; +import { OrgProjectsSection } from '@/components/admin/orgs/OrgProjectsSection'; +import type { AdminOrgSubscription } from '@/server/functions/admin-orgs.server'; import { queryKeys } from '@/lib/queryKeys'; const BACK_TO_ORGS = { to: '/admin/orgs', label: 'Back to Organizations' }; @@ -40,57 +43,19 @@ export const Route = createFileRoute('/_app/_protected/admin/orgs/$orgId')({ component: OrgDetailPage, }); -interface OrgDetails { - org?: { name?: string; slug?: string }; - stats?: { memberCount?: number; projectCount?: number }; -} - -interface SubscriptionRecord { - id: string; - plan: string; - status: string; - periodStart?: string | number | Date; - periodEnd?: string | number | Date; - cancelAtPeriodEnd?: boolean; - canceledAt?: string | number | Date | null; - endedAt?: string | number | Date | null; - stripeCustomerId?: string; - stripeSubscriptionId?: string; -} - -interface BillingData { - billing?: { - plan?: { - name?: string; - entitlements?: Record; - quotas?: Record; - }; - effectivePlanId?: string; - accessMode?: 'full' | 'readOnly'; - source?: 'free' | 'subscription' | 'grant'; - subscription?: { id?: string; plan?: string } | null; - grant?: { type?: string } | null; - }; - subscriptions?: SubscriptionRecord[]; - grants?: Array<{ - id: string; - type: string; - startsAt?: string | number | Date; - expiresAt?: string | number | Date; - createdAt?: string | number | Date; - revokedAt?: string | number | Date | null; - }>; -} - function OrgDetailPage() { const { orgId } = Route.useParams(); const queryClient = useQueryClient(); const orgDetailsQuery = useAdminOrgDetails(orgId); const billingQuery = useAdminOrgBilling(orgId); - const orgDetails = orgDetailsQuery.data as OrgDetails | undefined; - const billing = billingQuery.data as BillingData | undefined; + const orgDetails = orgDetailsQuery.data; + const billing = billingQuery.data; + + const memberCount = orgDetails?.stats?.memberCount ?? 0; + const projectCount = orgDetails?.stats?.projectCount ?? 0; + const [reconcileOpen, setReconcileOpen] = useState(false); const [subscriptionDialogOpen, setSubscriptionDialogOpen] = useState(false); const [grantDialogOpen, setGrantDialogOpen] = useState(false); const [confirmDialog, setConfirmDialog] = useState<{ @@ -99,7 +64,7 @@ function OrgDetailPage() { grantId?: string; } | null>(null); const [loading, setLoading] = useState(false); - const [editingSubscription, setEditingSubscription] = useState(null); + const [editingSubscription, setEditingSubscription] = useState(null); // Subscription form state const [subPlan, setSubPlan] = useState('team'); @@ -235,7 +200,7 @@ function OrgDetailPage() { } }; - const handleEditSubscription = (subscription: SubscriptionRecord) => { + const handleEditSubscription = (subscription: AdminOrgSubscription) => { setEditingSubscription(subscription); setSubPlan(subscription.plan); setSubStatus(subscription.status); @@ -342,21 +307,24 @@ function OrgDetailPage() { - - - - + + + @@ -388,7 +356,17 @@ function OrgDetailPage() { } /> - + {reconcileOpen ? + setReconcileOpen(false)} /> + : + } {/* Subscription Dialog */} { setSearch(value); setPage(1); }; - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { accessorKey: 'name', @@ -143,7 +128,7 @@ function AdminOrgList() { /> } enableSorting - onRowClick={(row: OrgRow) => + onRowClick={(row: AdminOrgListItem) => navigate({ to: '/admin/orgs/$orgId' as string, params: { orgId: row.id } as Record, diff --git a/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx b/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx index 108cdae6..c5d5cf8d 100644 --- a/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx +++ b/packages/web/src/routes/_app/_protected/admin/projects.$projectId.tsx @@ -10,7 +10,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { handleError } from '@/lib/error-utils'; import { queryKeys } from '@/lib/queryKeys'; import { AdminError, AdminPage, AdminPanel } from '@/components/admin/ui'; -import type { ProjectData, WorkspaceStats, ProjectMember } from '@/components/admin/projects/types'; +import type { AdminProjectMember } from '@/server/functions/admin-projects.server'; import { ProjectInfoSection } from '@/components/admin/projects/ProjectInfoSection'; import { WorkspaceStorageSection } from '@/components/admin/projects/WorkspaceStorageSection'; import { ProjectMembersSection } from '@/components/admin/projects/ProjectMembersSection'; @@ -33,17 +33,17 @@ function ProjectDetailPage() { const queryClient = useQueryClient(); const projectQuery = useAdminProjectDetails(projectId); - const projectData = projectQuery.data as ProjectData | undefined; + const projectData = projectQuery.data; // DO storage stats are fetched separately because they route through the // ProjectDoc DO and are slower than the D1 details query. Loading them as // a sibling query lets the rest of the page render immediately. const statsQuery = useAdminWorkspaceStats(projectId); - const workspaceStats = statsQuery.data as WorkspaceStats | undefined; + const workspaceStats = statsQuery.data; const [confirmDialog, setConfirmDialog] = useState<{ type: 'delete-project' | 'remove-member'; - member?: ProjectMember; + member?: AdminProjectMember; } | null>(null); const [loading, setLoading] = useState(false); @@ -156,9 +156,7 @@ function ProjectDetailPage() { open={confirmDialog?.type === 'remove-member'} onOpenChange={open => !open && setConfirmDialog(null)} memberName={ - confirmDialog?.member?.userDisplayName || - confirmDialog?.member?.userName || - confirmDialog?.member?.userEmail + confirmDialog?.member?.userName || confirmDialog?.member?.userEmail || undefined } onConfirm={() => { if (confirmDialog?.member?.id) { diff --git a/packages/web/src/routes/_app/_protected/admin/projects.index.tsx b/packages/web/src/routes/_app/_protected/admin/projects.index.tsx index 6f8b4022..9a45f7de 100644 --- a/packages/web/src/routes/_app/_protected/admin/projects.index.tsx +++ b/packages/web/src/routes/_app/_protected/admin/projects.index.tsx @@ -3,6 +3,7 @@ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; import { FolderIcon } from 'lucide-react'; import { useAdminProjects, useAdminOrgs } from '@/hooks/useAdminQueries'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; +import type { AdminProjectListItem } from '@/server/functions/admin-projects.server'; import { formatDate } from '@/lib/formatDate'; import { AdminDataTable, @@ -23,26 +24,6 @@ import { SelectValue, } from '@/components/ui/select'; -interface ProjectRow { - id: string; - name: string; - orgId: string; - orgName: string; - orgSlug: string; - createdBy: string; - creatorDisplayName?: string; - creatorName?: string; - creatorEmail?: string; - memberCount: number; - fileCount: number; - createdAt?: string | number; -} - -interface OrgOption { - id: string; - name: string; -} - const PAGE_SIZE = 25; const ALL_ORGS_VALUE = 'all'; @@ -63,15 +44,10 @@ function AdminProjectList() { search: debouncedSearch, orgId: selectedOrgId, }); - const projectsData = projectsQuery.data as - | { - projects: ProjectRow[]; - pagination: { page: number; total: number; totalPages: number }; - } - | undefined; + const projectsData = projectsQuery.data; const orgsQuery = useAdminOrgs({ page: 1, limit: 100, search: '' }); - const orgsData = orgsQuery.data as { orgs: OrgOption[] } | undefined; + const orgsData = orgsQuery.data; const projects = projectsData?.projects || []; const pagination = projectsData?.pagination; @@ -87,7 +63,7 @@ function AdminProjectList() { setPage(1); }; - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { accessorKey: 'name', @@ -112,11 +88,11 @@ function AdminProjectList() { }, }, { - accessorKey: 'creatorDisplayName', + accessorKey: 'creatorName', header: 'Created by', cell: info => { const project = info.row.original; - const name = project.creatorDisplayName || project.creatorName; + const name = project.creatorName; if (!name && !project.creatorEmail) { return -; } @@ -244,7 +220,7 @@ function AdminProjectList() { /> } enableSorting - onRowClick={(row: ProjectRow) => + onRowClick={(row: AdminProjectListItem) => navigate({ to: '/admin/projects/$projectId' as string, params: { projectId: row.id } as Record, diff --git a/packages/web/src/routes/_app/_protected/admin/storage.tsx b/packages/web/src/routes/_app/_protected/admin/storage.tsx index 490ddb0d..664ca4a6 100644 --- a/packages/web/src/routes/_app/_protected/admin/storage.tsx +++ b/packages/web/src/routes/_app/_protected/admin/storage.tsx @@ -44,22 +44,6 @@ export const Route = createFileRoute('/_app/_protected/admin/storage')({ component: StorageManagementPage, }); -interface StorageDocument { - key: string; - fileName: string; - size?: number; - projectId?: string; - studyId?: string; - uploaded?: string; - orphaned?: boolean; -} - -interface StorageDocumentsData { - documents: StorageDocument[]; - nextCursor?: string | null; - truncated?: boolean; -} - const PAGE_SIZE = 50; function StorageManagementPage() { @@ -78,7 +62,7 @@ function StorageManagementPage() { prefix, search: debouncedSearch, }); - const documentsData = documentsDataQuery.data as StorageDocumentsData | undefined; + const documentsData = documentsDataQuery.data; const documents = documentsData?.documents ?? []; const resetPaging = () => { diff --git a/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx b/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx index bea3002f..6a19df44 100644 --- a/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx +++ b/packages/web/src/routes/_app/_protected/admin/users.$userId.tsx @@ -19,7 +19,6 @@ import { handleError } from '@/lib/error-utils'; import { queryKeys } from '@/lib/queryKeys'; import { AdminError, AdminPage, AdminPanel } from '@/components/admin/ui'; import { Skeleton } from '@/components/ui/skeleton'; -import type { UserData } from '@/components/admin/users/types'; import { UserActions } from '@/components/admin/users/UserActions'; import { UserProfileSection } from '@/components/admin/users/UserProfileSection'; import { UserLinkedAccounts } from '@/components/admin/users/UserLinkedAccounts'; @@ -76,7 +75,7 @@ function UserDetailContent() { const qc = useQueryClient(); const { data } = useSuspenseQuery(adminUserDetailsQueryOptions(userId)); - const userData = data as unknown as UserData; + const userData = data; const user = userData.user; const [confirmDialog, setConfirmDialog] = useState<{ @@ -172,7 +171,11 @@ function UserDetailContent() { back={BACK_TO_USERS} title={ - + {user.name} } diff --git a/packages/web/src/routes/_app/_protected/admin/users.index.tsx b/packages/web/src/routes/_app/_protected/admin/users.index.tsx index f426cfcd..05feff8f 100644 --- a/packages/web/src/routes/_app/_protected/admin/users.index.tsx +++ b/packages/web/src/routes/_app/_protected/admin/users.index.tsx @@ -22,12 +22,7 @@ function AdminUserList() { limit: PAGE_SIZE, search: debouncedSearch, }); - const usersData = usersDataQuery.data as - | { - users: Array<{ id: string; [key: string]: unknown }>; - pagination: { limit: number; total: number; totalPages: number }; - } - | undefined; + const usersData = usersDataQuery.data; const handleSearchChange = (value: string) => { setSearch(value); diff --git a/packages/web/src/server/functions/admin-billing.server.ts b/packages/web/src/server/functions/admin-billing.server.ts index 8d529627..2e31cc2d 100644 --- a/packages/web/src/server/functions/admin-billing.server.ts +++ b/packages/web/src/server/functions/admin-billing.server.ts @@ -215,3 +215,5 @@ export async function getAdminBillingStuckStates( stuckOrgs, }; } + +export type AdminLedgerEntry = Awaited>['entries'][number]; diff --git a/packages/web/src/server/functions/admin-database.server.ts b/packages/web/src/server/functions/admin-database.server.ts index 4d975c13..5dd3b0e1 100644 --- a/packages/web/src/server/functions/admin-database.server.ts +++ b/packages/web/src/server/functions/admin-database.server.ts @@ -307,3 +307,5 @@ export async function getAdminTableRows( }, }; } + +export type AdminTableColumn = Awaited>['columns'][number]; diff --git a/packages/web/src/server/functions/admin-orgs.server.ts b/packages/web/src/server/functions/admin-orgs.server.ts index 6a0d7cfc..1251a811 100644 --- a/packages/web/src/server/functions/admin-orgs.server.ts +++ b/packages/web/src/server/functions/admin-orgs.server.ts @@ -1,7 +1,7 @@ import { captureError, info } from '@corates/workers/logger'; import { env } from 'cloudflare:workers'; import type { Database } from '@corates/db/client'; -import { organization, member, projects, subscription } from '@corates/db/schema'; +import { organization, member, projects, subscription, user } from '@corates/db/schema'; import { and, count, desc, eq, or, sql } from 'drizzle-orm'; import { containsInsensitive } from '@/server/lib/sqlSearch'; import { @@ -175,6 +175,10 @@ export async function listAdminOrgs( }; } +// The detail page shows counts from the real totals, so the rows themselves can +// stay capped without the page reporting a smaller org than it is. +const ORG_DETAIL_ROW_LIMIT = 50; + export async function getAdminOrgDetails(session: Session, db: Database, orgId: OrgId) { assertAdmin(session); @@ -197,6 +201,43 @@ export async function getAdminOrgDetails(session: Session, db: Database, orgId: .all(); const projectCount = projectCountResult?.count || 0; + const members = await db + .select({ + id: member.id, + userId: member.userId, + role: member.role, + joinedAt: member.createdAt, + userName: user.name, + userEmail: user.email, + userAvatar: user.avatarUrl, + userBanned: user.banned, + }) + .from(member) + .leftJoin(user, eq(member.userId, user.id)) + .where(eq(member.organizationId, orgId)) + .orderBy( + sql`CASE ${member.role} WHEN 'owner' THEN 0 WHEN 'admin' THEN 1 ELSE 2 END`, + desc(member.createdAt), + ) + .limit(ORG_DETAIL_ROW_LIMIT) + .all(); + + const orgProjects = await db + .select({ + id: projects.id, + name: projects.name, + createdBy: projects.createdBy, + creatorName: user.name, + creatorEmail: user.email, + createdAt: projects.createdAt, + }) + .from(projects) + .leftJoin(user, eq(projects.createdBy, user.id)) + .where(eq(projects.orgId, orgId)) + .orderBy(desc(projects.createdAt)) + .limit(ORG_DETAIL_ROW_LIMIT) + .all(); + const orgBilling = await resolveOrgAccess(db, orgId); const effectivePlan = orgBilling.source === 'grant' ? @@ -206,6 +247,8 @@ export async function getAdminOrgDetails(session: Session, db: Database, orgId: return { org, stats: { memberCount, projectCount }, + members, + projects: orgProjects, billing: { effectivePlanId: orgBilling.effectivePlanId, source: orgBilling.source, @@ -221,6 +264,15 @@ export async function getAdminOrgDetails(session: Session, db: Database, orgId: }; } +export type AdminOrgBilling = Awaited>; +export type AdminOrgBillingState = AdminOrgBilling['billing']; +export type AdminOrgSubscription = AdminOrgBilling['subscriptions'][number]; +export type AdminOrgGrant = AdminOrgBilling['grants'][number]; +export type AdminOrgListItem = Awaited>['orgs'][number]; +export type AdminOrgDetails = Awaited>; +export type AdminOrgMember = AdminOrgDetails['members'][number]; +export type AdminOrgProject = AdminOrgDetails['projects'][number]; + export async function getAdminOrgBilling(session: Session, db: Database, orgId: OrgId) { assertAdmin(session); diff --git a/packages/web/src/server/functions/admin-projects.server.ts b/packages/web/src/server/functions/admin-projects.server.ts index bfdd300a..c67cd6b9 100644 --- a/packages/web/src/server/functions/admin-projects.server.ts +++ b/packages/web/src/server/functions/admin-projects.server.ts @@ -220,6 +220,95 @@ export async function getAdminProjectDetails(session: Session, db: Database, pro }; } +export interface ProjectMember { + id: string; + userId: string; + role: string; + userAvatar?: string; + userDisplayName?: string; + userName?: string; + userEmail?: string; + joinedAt?: string | number | Date; +} + +export interface ProjectFile { + id: string; + originalName?: string; + filename?: string; + fileType?: string; + fileSize?: number; + uploadedBy?: string; + uploaderDisplayName?: string; + uploaderName?: string; + createdAt?: string | number | Date; +} + +export interface ProjectInvitation { + id: string; + email: string; + role: string; + grantOrgMembership?: boolean; + acceptedAt?: string | number | Date | null; + expiresAt?: number; + invitedBy: string; + inviterDisplayName?: string; + inviterName?: string; + createdAt?: string | number | Date; +} + +export interface ProjectData { + project: { + id: string; + name: string; + orgId: string; + orgName: string; + orgSlug: string; + createdBy: string; + creatorDisplayName?: string; + creatorName?: string; + creatorEmail?: string; + createdAt?: string | number | Date; + updatedAt?: string | number | Date; + }; + stats: { + memberCount: number; + fileCount: number; + totalStorageBytes: number; + }; + members?: ProjectMember[]; + files?: ProjectFile[]; + invitations?: ProjectInvitation[]; +} + +/** The sync-engine workspace's admin stats (`workspaceAdmin(...).stats()`). */ +export interface WorkspaceStats { + workspaceId: string; + backendId: string; + schemaVersion: number; + currentVersion: number; + rows: { live: number; tombstones: number }; + mutationLogEntries: number; + knownClients: number; + databaseSizeBytes: number; + connections: { total: number; ready: number; presence: number }; + /** The Yjs fields add-on's stats, when mounted. */ + extension?: { + fields: number; + frozenFields: number; + fieldBytes: number; + pendingUpdates: number; + cachedDocs: number; + }; +} + +export type AdminProjectListItem = Awaited< + ReturnType +>['projects'][number]; +export type AdminProjectDetails = Awaited>; +export type AdminProjectMember = AdminProjectDetails['members'][number]; +export type AdminProjectFile = AdminProjectDetails['files'][number]; +export type AdminProjectInvitation = AdminProjectDetails['invitations'][number]; + export async function getAdminWorkspaceStats(session: Session, db: Database, projectId: string) { assertAdmin(session); @@ -234,7 +323,7 @@ export async function getAdminWorkspaceStats(session: Session, db: Database, pro } const stats = await projectWorkspace(env, projectId).stats(); - return stats as import('@/components/admin/projects/types').WorkspaceStats; + return stats as WorkspaceStats; } export async function removeAdminProjectMember( diff --git a/packages/web/src/server/functions/admin-users.server.ts b/packages/web/src/server/functions/admin-users.server.ts index cb4ffa21..ef11c185 100644 --- a/packages/web/src/server/functions/admin-users.server.ts +++ b/packages/web/src/server/functions/admin-users.server.ts @@ -238,6 +238,13 @@ export async function getAdminUserDetails(session: Session, db: Database, userId }; } +export type AdminUserListItem = Awaited>['users'][number]; +export type AdminUserDetails = Awaited>; +export type AdminUserProject = AdminUserDetails['projects'][number]; +export type AdminUserSession = AdminUserDetails['sessions'][number]; +export type AdminUserAccount = AdminUserDetails['accounts'][number]; +export type AdminUserOrg = AdminUserDetails['orgs'][number]; + export async function deleteAdminUser(session: Session, db: Database, userId: string) { assertAdmin(session);