import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { adminApi } from '../../api/admin';
import { AppLayout } from '../../components/layout/AppLayout';
import { PageLoader } from '../../components/ui/LoadingSpinner';
import { Building2, Search, ArrowUpRight, ArrowDownRight, Plus, X, Loader2, Trash2, AlertTriangle } from 'lucide-react';
import type { Organization } from '../../types';
export function AdminOrganizations() {
const [search, setSearch] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['admin-organizations'],
queryFn: () => adminApi.getOrganizations(),
});
const organizations = data?.items ?? [];
// Create mutation
const createMutation = useMutation({
mutationFn: (formData: {
name: string;
industry?: string;
annual_revenue?: number;
address?: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
phone?: string;
website?: string;
}) => adminApi.createOrganization(formData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-organizations'] });
setShowCreateModal(false);
},
});
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (orgId: string) => adminApi.deleteOrganization(orgId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-organizations'] });
setDeleteConfirm(null);
},
onError: (err: any) => {
setDeleteError(err?.response?.data?.error || err?.message || 'Failed to delete organization');
},
});
const filtered = organizations.filter((org) =>
org.name.toLowerCase().includes(search.toLowerCase()) ||
org.location.toLowerCase().includes(search.toLowerCase())
);
if (isLoading) {
return (
<AppLayout title="Organizations">
<PageLoader />
</AppLayout>
);
}
return (
<AppLayout title="Organizations">
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-surface-900">All Organizations</h1>
<p className="mt-1 text-sm text-surface-500">
{organizations.length} organization{organizations.length !== 1 ? 's' : ''} across all partners
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-surface-500" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search organizations..."
className="w-full rounded-lg border border-surface-300 py-2.5 pl-9 pr-4 text-sm outline-none focus:border-brand-500 sm:w-72"
/>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 min-h-[44px] w-full sm:w-auto"
>
<Plus className="h-4 w-4" />
Add Organization
</button>
</div>
</div>
{/* Organizations Table */}
{filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-surface-200 bg-white py-16">
<Building2 className="h-12 w-12 text-surface-300" />
<p className="mt-4 text-sm font-medium text-surface-700">
{search ? 'No organizations match your search' : 'No organizations yet'}
</p>
</div>
) : (
<div className="rounded-xl border border-surface-200 bg-white">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200">
<th className="pb-3 pl-5 text-left font-medium text-surface-500">Organization</th>
<th className="pb-3 px-4 text-left font-medium text-surface-500">Status</th>
<th className="pb-3 px-4 text-right font-medium text-surface-500">Revenue</th>
<th className="pb-3 px-4 text-right font-medium text-surface-500">Growth</th>
<th className="pb-3 px-4 text-center font-medium text-surface-500 pr-5">Projects</th>
<th className="pb-3 pr-5 w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-surface-100">
{filtered.map((org) => (
<tr
key={org.id}
className="hover:bg-surface-50 cursor-pointer"
onClick={() => navigate(`/admin/organizations/${org.id}`)}
>
<td className="py-3 pl-5">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand-100 text-brand-600">
<Building2 className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="font-medium text-surface-900 truncate">{org.name}</p>
<p className="text-xs text-surface-500">{org.location || 'No location'}</p>
</div>
</div>
</td>
<td className="py-3 px-4">
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
org.status === 'active'
? 'bg-success/10 text-success'
: 'bg-surface-200 text-surface-600'
}`}
>
{org.status}
</span>
</td>
<td className="py-3 px-4 text-right font-medium text-surface-900">
${org.annual_revenue.toLocaleString()}
</td>
<td className="py-3 px-4 text-right">
<div className="flex items-center justify-end gap-0.5">
{org.revenue_growth >= 0 ? (
<ArrowUpRight className="h-3 w-3 text-success" />
) : (
<ArrowDownRight className="h-3 w-3 text-error" />
)}
<span
className={`text-sm font-medium ${
org.revenue_growth >= 0 ? 'text-success' : 'text-error'
}`}
>
{Math.abs(org.revenue_growth).toFixed(1)}%
</span>
</div>
</td>
<td className="py-3 px-4 text-center text-surface-600 pr-5">
{org.active_projects}
</td>
<td className="py-3 pr-5 text-right">
<button
onClick={(e) => { e.stopPropagation(); setDeleteError(null); setDeleteConfirm(org.id); }}
className="inline-flex items-center justify-center rounded-md p-1.5 text-surface-400 hover:text-red-600 hover:bg-red-50 transition-colors"
title="Delete organization"
>
<Trash2 className="h-4 w-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Create Organization Modal */}
{showCreateModal && (
<CreateOrganizationModal
onClose={() => setShowCreateModal(false)}
onCreate={createMutation.mutate}
isPending={createMutation.isPending}
error={createMutation.error?.message ?? null}
/>
)}
{/* Delete Confirmation Modal */}
{deleteConfirm && (
<DeleteOrgConfirmModal
org={filtered.find(o => o.id === deleteConfirm)!}
onConfirm={() => deleteMutation.mutate(deleteConfirm!)}
onClose={() => { setDeleteConfirm(null); setDeleteError(null); }}
isPending={deleteMutation.isPending}
error={deleteError}
/>
)}
</AppLayout>
);
}
function CreateOrganizationModal({
onClose,
onCreate,
isPending,
error,
}: {
onClose: () => void;
onCreate: (data: {
name: string;
industry?: string;
annual_revenue?: number;
address?: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
phone?: string;
website?: string;
}) => void;
isPending: boolean;
error: string | null;
}) {
const [name, setName] = useState('');
const [industry, setIndustry] = useState('');
const [annual_revenue, setAnnualRevenue] = useState('');
const [address, setAddress] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [postal_code, setPostalCode] = useState('');
const [country, setCountry] = useState('');
const [phone, setPhone] = useState('');
const [website, setWebsite] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onCreate({
name,
industry: industry || undefined,
annual_revenue: annual_revenue ? parseFloat(annual_revenue) : undefined,
address: address || undefined,
city: city || undefined,
state: state || undefined,
postal_code: postal_code || undefined,
country: country || undefined,
phone: phone || undefined,
website: website || undefined,
});
};
return createPortal(
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-xl bg-white shadow-xl">
<div className="flex items-center justify-between border-b border-surface-200 p-5">
<h2 className="text-lg font-semibold text-surface-900">Add Organization</h2>
<button onClick={onClose} className="rounded-md p-1 text-surface-400 hover:text-surface-600">
<X className="h-5 w-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4">
{error && (
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Organization Name *</label>
<input
type="text"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="Acme Corp"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Industry</label>
<input
type="text"
value={industry}
onChange={(e) => setIndustry(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="e.g. Roofing, HVAC, Plumbing"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Annual Revenue ($)</label>
<input
type="number"
value={annual_revenue}
onChange={(e) => setAnnualRevenue(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="500000"
min="0"
step="1000"
/>
</div>
<div className="border-t border-surface-200 pt-4 space-y-3">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide">Contact & Address (optional)</p>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Phone</label>
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="(555) 123-4567"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Website</label>
<input
type="url"
value={website}
onChange={(e) => setWebsite(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="https://example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Address</label>
<input
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="123 Main St"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">City</label>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="New York"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">State</label>
<input
type="text"
value={state}
onChange={(e) => setState(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="NY"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Postal Code</label>
<input
type="text"
value={postal_code}
onChange={(e) => setPostalCode(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="10001"
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Country</label>
<input
type="text"
value={country}
onChange={(e) => setCountry(e.target.value)}
className="w-full rounded-lg border border-surface-300 px-3 py-2 text-sm outline-none focus:border-brand-500"
placeholder="US"
/>
</div>
</div>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-surface-200">
<button
type="button"
onClick={onClose}
className="rounded-lg px-4 py-2.5 text-sm font-medium text-surface-700 hover:bg-surface-100 min-h-[44px]"
>
Cancel
</button>
<button
type="submit"
disabled={isPending}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 min-h-[44px]"
>
{isPending && <Loader2 className="h-4 w-4 animate-spin" />}
Create Organization
</button>
</div>
</form>
</div>
</div>,
document.body
);
}
// Delete confirmation modal
function DeleteOrgConfirmModal({
org,
onConfirm,
onClose,
isPending,
error,
}: {
org: Organization;
onConfirm: () => void;
onClose: () => void;
isPending: boolean;
error: string | null;
}) {
return createPortal(
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative w-full max-w-md rounded-xl bg-white shadow-xl">
<div className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-100">
<AlertTriangle className="h-5 w-5 text-red-600" />
</div>
<h3 className="text-lg font-semibold text-surface-900">Delete Organization</h3>
</div>
<p className="text-sm text-surface-600 mb-2">
Are you sure you want to delete <span className="font-semibold text-surface-900">{org.name}</span>?
</p>
<div className="rounded-lg bg-amber-50 border border-amber-200 px-4 py-3 text-xs text-amber-800 mb-4">
This will permanently remove the organization and all associated data including users, projects, and invoices. This action cannot be undone.
</div>
{error && (
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700 mb-4">
{error}
</div>
)}
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onClose}
disabled={isPending}
className="rounded-lg px-4 py-2.5 text-sm font-medium text-surface-700 hover:bg-surface-100 disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
disabled={isPending}
className="inline-flex items-center gap-2 rounded-lg bg-red-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
>
{isPending && <Loader2 className="h-4 w-4 animate-spin" />}
Delete Organization
</button>
</div>
</div>
</div>
</div>,
document.body
);
}