156 lines
5.7 KiB
TypeScript
156 lines
5.7 KiB
TypeScript
import { createClient } from "@/lib/supabase/server";
|
|
import { inviteUser, revokeInvitation } from "./actions";
|
|
import { Suspense } from "react";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { Trash2 } from "lucide-react";
|
|
|
|
async function TeamSettingsContent() {
|
|
const supabase = await createClient();
|
|
const { data: userData } = await supabase.auth.getUser();
|
|
const userId = userData?.user?.id;
|
|
|
|
if (!userId) return null;
|
|
|
|
// Get profile
|
|
const { data: profile } = await supabase
|
|
.from("profiles")
|
|
.select("organization_id, role")
|
|
.eq("id", userId)
|
|
.single();
|
|
|
|
if (!profile?.organization_id) return null;
|
|
|
|
// Get organization members
|
|
const { data: members } = await supabase
|
|
.from("profiles")
|
|
.select("id, email, role")
|
|
.eq("organization_id", profile.organization_id);
|
|
|
|
// Get pending invitations
|
|
const { data: invitations } = await supabase
|
|
.from("invitations")
|
|
.select("id, email, token, status, expires_at")
|
|
.eq("organization_id", profile.organization_id)
|
|
.eq("status", "pending")
|
|
.gt("expires_at", new Date().toISOString());
|
|
|
|
return (
|
|
<div className="p-8 max-w-4xl mx-auto space-y-8">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold mb-1 text-foreground">Team Management</h1>
|
|
<p className="text-sm text-muted-foreground">Manage your organization's members and invitations.</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-medium text-foreground border-b border-border pb-2">Members</h2>
|
|
<div className="bg-card border border-border rounded-lg overflow-hidden shadow-sm">
|
|
<ul className="divide-y divide-border">
|
|
{members?.map((m) => (
|
|
<li key={m.id} className="p-4 flex items-center justify-between hover:bg-muted/50 transition-colors">
|
|
<div>
|
|
<div className="text-sm font-medium">{m.email}</div>
|
|
<div className="text-xs text-muted-foreground capitalize">{m.role}</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{profile.role === "owner" && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-medium text-foreground border-b border-border pb-2">Invite New Member</h2>
|
|
<div className="bg-card border border-border rounded-lg p-4 shadow-sm">
|
|
<form action={inviteUser} className="flex gap-4">
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
placeholder="Email address"
|
|
required
|
|
className="flex-1 px-3 py-2 bg-background border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="bg-primary text-primary-foreground px-4 py-2 rounded-md text-sm font-medium hover:bg-primary/90 transition-colors flex-shrink-0"
|
|
>
|
|
Send Invite
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{profile.role === "owner" && invitations && invitations.length > 0 && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-medium text-foreground border-b border-border pb-2">Pending Invitations</h2>
|
|
<div className="bg-card border border-border rounded-lg overflow-hidden shadow-sm">
|
|
<ul className="divide-y divide-border">
|
|
{invitations.map((inv) => (
|
|
<li key={inv.id} className="p-4 flex items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
|
|
<div className="truncate flex-1">
|
|
<div className="text-sm font-medium">{inv.email}</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">Pending</div>
|
|
<form action={revokeInvitation}>
|
|
<input type="hidden" name="id" value={inv.id} />
|
|
<button type="submit" className="text-muted-foreground hover:text-destructive transition-colors p-1" title="Revoke Invitation">
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TeamSettingsSkeleton() {
|
|
return (
|
|
<div className="p-8 max-w-4xl mx-auto space-y-8">
|
|
<div>
|
|
<Skeleton className="h-8 w-48 mb-2" />
|
|
<Skeleton className="h-4 w-96" />
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="border-b border-border pb-2">
|
|
<Skeleton className="h-6 w-24" />
|
|
</div>
|
|
<div className="bg-card border border-border rounded-lg overflow-hidden shadow-sm p-4 space-y-4">
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-4 w-48" />
|
|
<Skeleton className="h-3 w-20" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-4 w-48" />
|
|
<Skeleton className="h-3 w-20" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="border-b border-border pb-2">
|
|
<Skeleton className="h-6 w-36" />
|
|
</div>
|
|
<div className="bg-card border border-border rounded-lg p-4 shadow-sm flex gap-4">
|
|
<Skeleton className="h-10 flex-1" />
|
|
<Skeleton className="h-10 w-28" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function TeamSettingsPage() {
|
|
return (
|
|
<Suspense fallback={<TeamSettingsSkeleton />}>
|
|
<TeamSettingsContent />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|