'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { useAuth } from '@/lib/auth/context' import { deleteOrganization, switchContext, getOrganizationMembers, getInvitations, sendInvitation, revokeInvitation, updateOrganization, getOrganization, OrganizationMember, OrganizationInvitation, Organization } from '@/lib/api/organization' import { getSubscription, createPortalSession, getInvoices, SubscriptionDetails, Invoice } from '@/lib/api/billing' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { motion, AnimatePresence } from 'framer-motion' import { AlertTriangleIcon, PlusIcon, BoxIcon, UserIcon, CheckIcon, XIcon, Captcha, BookOpenIcon, DownloadIcon, ExternalLinkIcon, LayoutDashboardIcon } from '@ciphera-net/ui' // @ts-ignore import { Button, Input } from '@ciphera-net/ui' export default function OrganizationSettings() { const { user } = useAuth() const router = useRouter() const searchParams = useSearchParams() const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing' | 'audit'>(() => { const tab = searchParams.get('tab') return tab === 'billing' || tab === 'members' || tab === 'audit' ? tab : 'general' }) const [showDeletePrompt, setShowDeletePrompt] = useState(false) const [deleteConfirm, setDeleteConfirm] = useState('') const [isDeleting, setIsDeleting] = useState(false) // Members State const [members, setMembers] = useState([]) const [invitations, setInvitations] = useState([]) const [isLoadingMembers, setIsLoadingMembers] = useState(true) // Billing State const [subscription, setSubscription] = useState(null) const [isLoadingSubscription, setIsLoadingSubscription] = useState(false) const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) const [invoices, setInvoices] = useState([]) const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) // Invite State const [inviteEmail, setInviteEmail] = useState('') const [inviteRole, setInviteRole] = useState('member') const [isInviting, setIsInviting] = useState(false) // Captcha State const [captchaId, setCaptchaId] = useState('') const [captchaSolution, setCaptchaSolution] = useState('') const [captchaToken, setCaptchaToken] = useState('') // Org Update State const [orgDetails, setOrgDetails] = useState(null) const [isEditing, setIsEditing] = useState(false) const [orgName, setOrgName] = useState('') const [orgSlug, setOrgSlug] = useState('') const [isSaving, setIsSaving] = useState(false) // Audit log State const [auditEntries, setAuditEntries] = useState([]) const [auditTotal, setAuditTotal] = useState(0) const [isLoadingAudit, setIsLoadingAudit] = useState(false) const [auditPage, setAuditPage] = useState(0) const auditPageSize = 20 const [auditActionFilter, setAuditActionFilter] = useState('') const [auditStartDate, setAuditStartDate] = useState('') const [auditEndDate, setAuditEndDate] = useState('') const getOrgIdFromToken = () => { return user?.org_id || null } const currentOrgId = getOrgIdFromToken() const loadMembers = useCallback(async () => { if (!currentOrgId) return try { const [membersData, invitesData, orgData] = await Promise.all([ getOrganizationMembers(currentOrgId), getInvitations(currentOrgId), getOrganization(currentOrgId) ]) setMembers(membersData) setInvitations(invitesData) setOrgDetails(orgData) setOrgName(orgData.name) setOrgSlug(orgData.slug) } catch (error) { console.error('Failed to load data:', error) // toast.error('Failed to load members') } finally { setIsLoadingMembers(false) } }, [currentOrgId]) const loadSubscription = useCallback(async () => { if (!currentOrgId) return setIsLoadingSubscription(true) try { const sub = await getSubscription() setSubscription(sub) } catch (error) { console.error('Failed to load subscription:', error) // toast.error('Failed to load subscription details') } finally { setIsLoadingSubscription(false) } }, [currentOrgId]) const loadInvoices = useCallback(async () => { if (!currentOrgId) return setIsLoadingInvoices(true) try { const invs = await getInvoices() setInvoices(invs) } catch (error) { console.error('Failed to load invoices:', error) } finally { setIsLoadingInvoices(false) } }, [currentOrgId]) useEffect(() => { if (currentOrgId) { loadMembers() } else { setIsLoadingMembers(false) } }, [currentOrgId, loadMembers]) useEffect(() => { const tab = searchParams.get('tab') if ((tab === 'billing' || tab === 'members' || tab === 'audit') && tab !== activeTab) { setActiveTab(tab) } }, [searchParams, activeTab]) useEffect(() => { if (activeTab === 'billing' && currentOrgId) { loadSubscription() loadInvoices() } }, [activeTab, currentOrgId, loadSubscription, loadInvoices]) const loadAudit = useCallback(async () => { if (!currentOrgId) return setIsLoadingAudit(true) try { const params: GetAuditLogParams = { limit: auditPageSize, offset: auditPage * auditPageSize, } if (auditActionFilter) params.action = auditActionFilter if (auditStartDate) params.start_date = auditStartDate if (auditEndDate) params.end_date = auditEndDate const { entries, total } = await getAuditLog(params) setAuditEntries(entries) setAuditTotal(total) } catch (error) { console.error('Failed to load audit log', error) toast.error(getAuthErrorMessage(error as Error) || 'Failed to load audit log') } finally { setIsLoadingAudit(false) } }, [currentOrgId, auditPage, auditActionFilter, auditStartDate, auditEndDate]) useEffect(() => { if (activeTab === 'audit' && currentOrgId) { loadAudit() } }, [activeTab, currentOrgId, loadAudit]) // If no org ID, we are in personal workspace, so don't show org settings if (!currentOrgId) { return (

You are in your Personal Workspace. Switch to an Organization to manage its settings.

) } const handleManageSubscription = async () => { setIsRedirectingToPortal(true) try { const { url } = await createPortalSession() window.location.href = url } catch (error: any) { toast.error(getAuthErrorMessage(error) || error.message || 'Failed to redirect to billing portal') setIsRedirectingToPortal(false) } } const handleDelete = async () => { if (deleteConfirm !== 'DELETE') return setIsDeleting(true) try { await deleteOrganization(currentOrgId) toast.success('Organization deleted successfully') // * Clear sticky session localStorage.removeItem('active_org_id') // * Switch to personal context explicitly try { const { access_token } = await switchContext(null) localStorage.setItem('token', access_token) window.location.href = '/' } catch (switchErr) { console.error('Failed to switch to personal context after delete:', switchErr) // Fallback: reload and let backend handle invalid token if any window.location.href = '/' } } catch (err: any) { console.error(err) toast.error(getAuthErrorMessage(err) || err.message || 'Failed to delete organization') setIsDeleting(false) } } const handleSendInvite = async (e: React.FormEvent) => { e.preventDefault() if (!inviteEmail) return if (!captchaToken) { toast.error('Please complete the security check') return } setIsInviting(true) try { await sendInvitation(currentOrgId, inviteEmail, inviteRole, { captcha_id: captchaId, captcha_solution: captchaSolution, captcha_token: captchaToken }) toast.success(`Invitation sent to ${inviteEmail}`) setInviteEmail('') // Reset captcha setCaptchaId('') setCaptchaSolution('') setCaptchaToken('') loadMembers() // Refresh list } catch (error: any) { toast.error(getAuthErrorMessage(error) || error.message || 'Failed to send invitation') } finally { setIsInviting(false) } } const handleRevokeInvite = async (inviteId: string) => { try { await revokeInvitation(currentOrgId, inviteId) toast.success('Invitation revoked') loadMembers() // Refresh list } catch (error: any) { toast.error(getAuthErrorMessage(error) || error.message || 'Failed to revoke invitation') } } const handleUpdateOrg = async (e: React.FormEvent) => { e.preventDefault() if (!currentOrgId) return setIsSaving(true) try { await updateOrganization(currentOrgId, orgName, orgSlug) toast.success('Organization updated successfully') setIsEditing(false) loadMembers() } catch (error: any) { toast.error(getAuthErrorMessage(error) || error.message || 'Failed to update organization') } finally { setIsSaving(false) } } // Helper to find current org name (from members list if available, or just fallback) // Ideally we'd have a full org object, but we have ID. // We can find the current user's membership entry which has org name. const currentOrgName = members.find(m => m.user_id === user?.id)?.organization_name || 'Organization' return (

Organization Settings

Manage your organization workspace and members.

{/* Sidebar Navigation */} {/* Content Area */}
{activeTab === 'general' && (

General Information

Basic details about your organization.

setOrgName(e.target.value)} required minLength={2} maxLength={50} disabled={!isEditing} className={`bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-500' : ''}`} />
drop.ciphera.net/ setOrgSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} required minLength={3} maxLength={30} disabled={!isEditing} className={`rounded-l-none bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-500' : ''}`} />

Changing the slug will change your organization's URL.

{!isEditing ? ( ) : ( <> )}

Danger Zone

Irreversible actions for this organization.

Delete Organization

Permanently delete this organization and all its data.

)} {activeTab === 'members' && (
{/* Invite Section */}

Organization Members

Manage who has access to this organization.

Invite New Member

setInviteEmail(e.target.value)} required className="bg-white dark:bg-neutral-900" />
{ setCaptchaId(id) setCaptchaSolution(solution) setCaptchaToken(token || '') }} apiUrl={process.env.NEXT_PUBLIC_CAPTCHA_API_URL} />
{/* Members List */}

Active Members

{isLoadingMembers ? (
Loading members...
) : members.length === 0 ? (
No members found.
) : ( members.map((member) => (
{member.user_email?.[0].toUpperCase() || '?'}
{member.user_email || 'Unknown User'}
Joined {new Date(member.joined_at).toLocaleDateString()}
{member.role}
)) )}
{/* Pending Invitations */} {invitations.length > 0 && (

Pending Invitations

{invitations.map((invite) => (
{invite.email}
Invited as {invite.role} • Expires {new Date(invite.expires_at).toLocaleDateString()}
))}
)}
)} {activeTab === 'billing' && (

Billing & Subscription

Manage your subscription plan and payment methods.

{isLoadingSubscription ? (
Loading subscription details...
) : !subscription ? (

Could not load subscription details.

) : (
{/* Current Plan */}

Current Plan

{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan {subscription.subscription_status === 'trialing' ? 'Trial Active' : (subscription.subscription_status || 'Free')}
{subscription.has_payment_method && ( )}
Sites
{typeof subscription.sites_count === 'number' ? subscription.plan_id === 'solo' ? `${subscription.sites_count} / 1` : `${subscription.sites_count}` : '—'}
Pageviews this period
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' ? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}` : '—'}
Billing Interval
{subscription.billing_interval ? `${subscription.billing_interval}ly` : '—'}
Pageview Limit
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / month` : 'Unlimited'}
{subscription.subscription_status === 'trialing' ? 'Trial Ends On' : 'Renews On'}
{(() => { const raw = subscription.current_period_end const d = raw ? new Date(raw as string) : null const ts = d ? d.getTime() : NaN return raw && !Number.isNaN(ts) && ts !== 0 ? (d as Date).toLocaleDateString() : '—' })()}
{!subscription.has_payment_method && (

Upgrade to Pro

Get higher limits, more data retention, and priority support.

)} {/* Invoice History */}

Invoice History

{isLoadingInvoices ? (
Loading invoices...
) : invoices.length === 0 ? (
No invoices found.
) : (
{invoices.map((invoice) => (
{(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })}
{new Date(invoice.created * 1000).toLocaleDateString()}
{invoice.status} {invoice.invoice_pdf && ( )} {invoice.hosted_invoice_url && ( )}
))}
)}
)}
)} {activeTab === 'audit' && (

Audit log

Who did what and when for this organization.

{/* Filters */}
setAuditActionFilter(e.target.value)} className="w-40 px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white" />
setAuditStartDate(e.target.value)} className="px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white" />
setAuditEndDate(e.target.value)} className="px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white" />
{/* Table */}
{isLoadingAudit ? (
Loading audit log...
) : auditEntries.length === 0 ? (
No audit events found.
) : (
{auditEntries.map((entry) => ( ))}
Time Actor Action Resource ID
{new Date(entry.occurred_at).toLocaleString()} {entry.actor_id ? entry.actor_id : 'System'} {entry.action} {entry.resource_type} {entry.resource_id || '—'}
)} {/* Pagination */} {auditTotal > auditPageSize && (
{auditPage * auditPageSize + 1}–{Math.min((auditPage + 1) * auditPageSize, auditTotal)} of {auditTotal}
)}
)}
{/* Delete Confirmation Modal */} {showDeletePrompt && (

Delete Organization?

This action cannot be undone. This will permanently delete the organization, all stored files, and remove all members.

setDeleteConfirm(e.target.value)} className="w-full px-3 py-2 bg-neutral-100 dark:bg-neutral-800 border-none rounded-lg focus:ring-2 focus:ring-red-500 outline-none text-neutral-900 dark:text-white font-mono" placeholder="DELETE" />
)}
) }