File size: 8,800 Bytes
cd32143 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "../lib/apiClient";
import {
Building2,
Plus,
Trash2,
ExternalLink,
Users,
Megaphone,
Mail,
ArrowUpRight,
ShieldCheck
} from "lucide-react";
import { useState } from "react";
import FormDrawer from "../components/FormDrawer";
import ConfirmModal from "../components/ConfirmModal";
import StatusBadge from "../components/StatusBadge";
import toast from "react-hot-toast";
export default function Clients() {
const queryClient = useQueryClient();
const [isAddOpen, setIsAddOpen] = useState(false);
const [selectedForDelete, setSelectedForDelete] = useState<string | null>(null);
const { data: clients = [], isLoading, refetch } = useQuery({
queryKey: ["clients"],
queryFn: async () => {
const res = await apiClient.get("/clients");
return res.data;
},
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
const res = await apiClient.delete(`/clients/${id}`);
return res.data;
},
onSuccess: () => {
toast.success("Client account removed");
setSelectedForDelete(null);
refetch();
},
});
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
<Building2 className="w-6 h-6 text-indigo-400" />
Agency Client Sub-Accounts
</h1>
<p className="text-zinc-400 text-sm mt-1">
Isolate campaigns, custom domains, leads, and analytics across distinct client sub-portals.
</p>
</div>
<button
onClick={() => setIsAddOpen(true)}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-all shadow-md shadow-indigo-600/20 flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Add Client Portal
</button>
</div>
{/* Client Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{clients.map((client: any) => (
<div
key={client.id}
className="bg-zinc-900 border border-zinc-800 rounded-xl p-5 shadow-sm hover:border-zinc-700 transition-all flex flex-col justify-between"
>
<div>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 flex items-center justify-center font-bold text-base">
{client.name ? client.name[0] : "C"}
</div>
<div>
<h3 className="font-semibold text-zinc-100 text-sm">{client.name}</h3>
<span className="text-xs text-zinc-500 font-mono">{client.email}</span>
</div>
</div>
<StatusBadge status={client.status || "ACTIVE"} size="sm" />
</div>
{client.website && (
<div className="mt-3 text-xs text-zinc-400 flex items-center gap-1.5 font-mono">
<ExternalLink className="w-3.5 h-3.5 text-zinc-500" />
<a
href={client.website}
target="_blank"
rel="noreferrer"
className="hover:text-indigo-400 transition-colors truncate"
>
{client.website.replace("https://", "")}
</a>
</div>
)}
{/* Metrics */}
<div className="grid grid-cols-2 gap-3 mt-5 pt-4 border-t border-zinc-800/80 text-xs">
<div className="p-2.5 bg-zinc-950 rounded-lg border border-zinc-800/60">
<span className="text-zinc-500 flex items-center gap-1 text-[11px]">
<Megaphone className="w-3 h-3 text-indigo-400" /> Campaigns
</span>
<span className="text-base font-bold text-zinc-200 mt-1 block font-mono">
{client.campaigns_count || 3}
</span>
</div>
<div className="p-2.5 bg-zinc-950 rounded-lg border border-zinc-800/60">
<span className="text-zinc-500 flex items-center gap-1 text-[11px]">
<Users className="w-3 h-3 text-teal-400" /> Leads
</span>
<span className="text-base font-bold text-zinc-200 mt-1 block font-mono">
{client.leads_count?.toLocaleString() || "1,420"}
</span>
</div>
</div>
</div>
<div className="mt-5 pt-3 border-t border-zinc-800/80 flex items-center justify-between">
<span className="text-[11px] text-zinc-500 font-mono">ID: {client.id}</span>
<div className="flex items-center gap-2">
<button
onClick={() => setSelectedForDelete(client.id)}
className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))}
{clients.length === 0 && (
<div className="col-span-full py-16 text-center text-xs text-zinc-500 bg-zinc-900 border border-zinc-800 rounded-xl">
No client portals created yet. Add your first agency sub-account above.
</div>
)}
</div>
{/* Delete Confirmation */}
<ConfirmModal
isOpen={Boolean(selectedForDelete)}
onClose={() => setSelectedForDelete(null)}
onConfirm={() => selectedForDelete && deleteMutation.mutate(selectedForDelete)}
title="Delete Client Sub-Account"
description="Are you sure you want to remove this client sub-account? All associated campaigns and isolated settings will be detached."
/>
{/* Add Client Drawer */}
<FormDrawer
isOpen={isAddOpen}
onClose={() => setIsAddOpen(false)}
title="Create Agency Client Sub-Account"
subtitle="Provision dedicated workspaces for client stakeholders."
>
<form
onSubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const data = {
name: (form.elements.namedItem("name") as HTMLInputElement).value,
email: (form.elements.namedItem("email") as HTMLInputElement).value,
website: (form.elements.namedItem("website") as HTMLInputElement).value,
};
try {
await apiClient.post("/clients", data);
toast.success("Client account created");
setIsAddOpen(false);
refetch();
} catch (err: any) {
toast.error("Failed to create client");
}
}}
className="space-y-4 text-xs"
>
<div>
<label className="block font-medium text-zinc-300 mb-1">Client Company Name *</label>
<input name="name" required placeholder="e.g. Acme Ventures" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Primary Contact Email *</label>
<input type="email" name="email" required placeholder="contact@acme.com" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Website URL</label>
<input name="website" placeholder="https://acme.com" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
<div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
<button
type="button"
onClick={() => setIsAddOpen(false)}
className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
>
Provision Account
</button>
</div>
</form>
</FormDrawer>
</div>
);
}
|