Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ supabase/.temp
# linter cache
.eslintcache

# Expo cache (any Expo app in the repo)
**/.expo/

# husky generated hook shims
.husky/_/

Expand All @@ -117,3 +120,11 @@ run-milvus-test.sh
.beads/
.env*.local
.tests/

# Ignore stray .env-prefixed files but preserve committed example/test/envrc files.
.env
.env.*
!.env.example
!.env.*.example
!.env.test
!.envrc
13 changes: 7 additions & 6 deletions apps/web/src/app/admin/api/organizations/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useInvalidateAllOrganizationData } from '@/app/api/organizations/hooks';
import { useTRPC } from '@/lib/trpc/utils';
import type { OrganizationPlan } from '@/lib/organizations/organization-types';
import type { StripeSubscriptionStatusValue } from '@/lib/admin/stripe-subscription-statuses';

export function useDeleteOrganization() {
const queryClient = useQueryClient();
Expand Down Expand Up @@ -69,9 +70,9 @@ type UseOrganizationsListParams = {
sortBy: OrganizationSortableField;
sortOrder: 'asc' | 'desc';
search: string;
seatsRequired?: string;
hasBalance?: string;
status?: string;
mode?: 'paying' | 'trial' | 'all';
include_deleted?: boolean;
stripe_status?: string;
plan?: string;
};

Expand All @@ -84,9 +85,9 @@ export function useOrganizationsList(params: UseOrganizationsListParams) {
sortBy: params.sortBy,
sortOrder: params.sortOrder,
search: params.search,
seatsRequired: params.seatsRequired as '' | 'true' | 'false' | undefined,
hasBalance: params.hasBalance as '' | 'true' | 'false' | undefined,
status: params.status as 'active' | 'all' | 'incomplete' | 'deleted' | undefined,
mode: params.mode,
include_deleted: params.include_deleted ?? false,
stripe_status: params.stripe_status as StripeSubscriptionStatusValue | '' | undefined,
plan: params.plan as '' | OrganizationPlan | undefined,
})
);
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/app/admin/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Users,
DollarSign,
Building2,
Clock,
Shield,
Ban,
Database,
Expand Down Expand Up @@ -66,6 +67,11 @@ const userManagementItems: MenuItem[] = [
url: '/admin/organizations',
icon: () => <Building2 />,
},
{
title: () => 'Trial Organizations',
url: '/admin/organizations/trials',
icon: () => <Clock />,
},
{
title: () => 'Bulk Block',
url: '/admin/bulk-block',
Expand Down
164 changes: 65 additions & 99 deletions apps/web/src/app/admin/components/OrganizationFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
Expand All @@ -13,17 +14,21 @@ import {
import { UserSearchInput } from './UserSearchInput';
import { X, Filter } from 'lucide-react';

import {
STRIPE_SUBSCRIPTION_STATUSES,
getStripeStatusLabel,
} from '@/lib/admin/stripe-subscription-statuses';

interface OrganizationFiltersProps {
search: string;
onSearchChange: (searchTerm: string) => void;
isLoading: boolean;
seatsRequired?: string;
hasBalance?: string;
status?: string;
plan?: string;
onSeatsRequiredChange: (value: string) => void;
onHasBalanceChange: (value: string) => void;
onStatusChange: (value: string) => void;
includeDeleted: boolean;
stripeStatus: string;
plan: string;
showStripeStatus?: boolean;
onIncludeDeletedChange: (value: boolean) => void;
onStripeStatusChange: (value: string) => void;
onPlanChange: (value: string) => void;
onResetFilters: () => void;
totalCount?: number;
Expand All @@ -34,31 +39,26 @@ export function OrganizationFilters({
search,
onSearchChange,
isLoading,
seatsRequired,
hasBalance,
status,
includeDeleted,
stripeStatus,
plan,
onSeatsRequiredChange,
onHasBalanceChange,
onStatusChange,
showStripeStatus = true,
onIncludeDeletedChange,
onStripeStatusChange,
onPlanChange,
onResetFilters,
totalCount,
filteredCount,
}: OrganizationFiltersProps) {
const activeFiltersCount = [
seatsRequired,
hasBalance,
status !== 'all',
plan && plan !== 'all',
].filter(Boolean).length;
const hasActiveFilters = activeFiltersCount > 0;
const hasActiveFilters = includeDeleted || !!stripeStatus || (!!plan && plan !== 'all');

const stripeStatusLabel = stripeStatus ? getStripeStatusLabel(stripeStatus) : undefined;

return (
<div className="space-y-4">
{/* Filter Controls Row */}
<div className="flex flex-wrap items-end gap-4">
{/* Main Search - Leftmost */}
{/* Main Search */}
<div className="space-y-2">
<Label className="text-sm font-medium">Search Organizations</Label>
<div className="w-80">
Expand All @@ -71,57 +71,28 @@ export function OrganizationFilters({
</div>
</div>

{/* Seats Required Filter */}
<div className="space-y-2">
<Label className="text-sm font-medium">Seats Required</Label>
<Select
value={seatsRequired || 'all'}
onValueChange={value => onSeatsRequiredChange(value === 'all' ? '' : value)}
>
<SelectTrigger className="w-32">
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="true">Yes</SelectItem>
<SelectItem value="false">No</SelectItem>
</SelectContent>
</Select>
</div>

{/* Has Balance Filter */}
<div className="space-y-2">
<Label className="text-sm font-medium">Has Balance</Label>
<Select
value={hasBalance || 'all'}
onValueChange={value => onHasBalanceChange(value === 'all' ? '' : value)}
>
<SelectTrigger className="w-32">
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="true">Yes</SelectItem>
<SelectItem value="false">No</SelectItem>
</SelectContent>
</Select>
</div>

{/* Status Filter */}
<div className="space-y-2">
<Label className="text-sm font-medium">Status</Label>
<Select value={status || 'all'} onValueChange={value => onStatusChange(value)}>
<SelectTrigger className="w-32">
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="active">Subscribed</SelectItem>
<SelectItem value="deleted">Deleted</SelectItem>
<SelectItem value="incomplete">Unsubscribed</SelectItem>
</SelectContent>
</Select>
</div>
{/* Stripe Status Filter */}
{showStripeStatus && (
<div className="space-y-2">
<Label className="text-sm font-medium">Stripe Status</Label>
<Select
value={stripeStatus || 'all'}
onValueChange={value => onStripeStatusChange(value === 'all' ? '' : value)}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Any" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Any</SelectItem>
{STRIPE_SUBSCRIPTION_STATUSES.map(s => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}

{/* Plan Filter */}
<div className="space-y-2">
Expand All @@ -141,10 +112,21 @@ export function OrganizationFilters({
</Select>
</div>

{/* Include Deleted Checkbox */}
<div className="flex items-center gap-2 pb-1">
<Checkbox
id="include-deleted"
checked={includeDeleted}
onCheckedChange={checked => onIncludeDeletedChange(checked === true)}
/>
<Label htmlFor="include-deleted" className="cursor-pointer text-sm font-medium">
Include deleted
</Label>
</div>

{/* Reset Filters Button */}
{hasActiveFilters && (
<div className="space-y-2">
<Label className="text-sm font-medium opacity-0">Reset</Label>
<div className="pb-1">
<Button variant="outline" size="sm" onClick={onResetFilters} className="h-9">
<X className="mr-1 h-4 w-4" />
Reset Filters
Expand All @@ -156,52 +138,37 @@ export function OrganizationFilters({
{/* Active Filters and Count Display */}
{(hasActiveFilters || (totalCount !== undefined && filteredCount !== undefined)) && (
<div className="flex items-center justify-between">
{/* Active Filters Badges */}
{hasActiveFilters && (
<div className="flex items-center gap-2">
<Filter className="text-muted-foreground h-4 w-4" />
<span className="text-muted-foreground text-sm">Active filters:</span>
{seatsRequired && (
{stripeStatus && (
<Badge variant="secondary" className="text-xs">
Seats Required: {seatsRequired === 'true' ? 'Yes' : 'No'}
Status: {stripeStatusLabel ?? stripeStatus}
<button
onClick={() => onSeatsRequiredChange('')}
onClick={() => onStripeStatusChange('')}
className="hover:bg-secondary-foreground/20 ml-1 rounded-full p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{hasBalance && (
<Badge variant="secondary" className="text-xs">
Has Balance: {hasBalance === 'true' ? 'Yes' : 'No'}
<button
onClick={() => onHasBalanceChange('')}
className="hover:bg-secondary-foreground/20 ml-1 rounded-full p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{status && status !== 'all' && (
{plan && plan !== 'all' && (
<Badge variant="secondary" className="text-xs">
Status:{' '}
{status === 'active'
? 'Subscribed'
: status.charAt(0).toUpperCase() + status.slice(1)}
Plan: {plan.charAt(0).toUpperCase() + plan.slice(1)}
<button
onClick={() => onStatusChange('all')}
onClick={() => onPlanChange('')}
className="hover:bg-secondary-foreground/20 ml-1 rounded-full p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{plan && plan !== 'all' && (
{includeDeleted && (
<Badge variant="secondary" className="text-xs">
Plan: {plan.charAt(0).toUpperCase() + plan.slice(1)}
Includes deleted
<button
onClick={() => onPlanChange('')}
onClick={() => onIncludeDeletedChange(false)}
className="hover:bg-secondary-foreground/20 ml-1 rounded-full p-0.5"
>
<X className="h-3 w-3" />
Expand All @@ -211,7 +178,6 @@ export function OrganizationFilters({
</div>
)}

{/* Results Count */}
{totalCount !== undefined && filteredCount !== undefined && (
<div className="text-muted-foreground text-sm">
Showing {filteredCount.toLocaleString()} of {totalCount.toLocaleString()}{' '}
Expand Down
Loading