- Connector lists Instagram accounts assigned to the system user without a
Page; numbers, inbox, mentions and publishing work for them
- Clients link by Page or Instagram account ("Link an account"); Facebook
features explain the client has no Page instead of failing
- Drafts for Instagram-only clients drop Facebook automatically
- Images for Instagram-only clients are hosted via the agency's own Page
(META_MEDIA_PAGE_ID, unpublished); never falls back to a client's Page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
4.5 KiB
JavaScript
105 lines
4.5 KiB
JavaScript
import { useEffect, useState } from "react";
|
|
import { RefreshCw, Instagram, Facebook } from "lucide-react";
|
|
import { api } from "../api.js";
|
|
import { Button, cx, Dialog, Empty, ErrorNote, Field, fmt, Spinner } from "../ui.jsx";
|
|
|
|
/** Pick a Facebook Page the agency can manage → create the client's folder, brand files and link. */
|
|
export default function AddClientDialog({ open, onClose, onAdded }) {
|
|
const [accounts, setAccounts] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const [picked, setPicked] = useState(null);
|
|
const [name, setName] = useState("");
|
|
const [runAudit, setRunAudit] = useState(true);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const load = (refresh) => {
|
|
setAccounts(null);
|
|
setError(null);
|
|
api(`/api/accounts/unlinked${refresh ? "?refresh=1" : ""}`).then((r) => setAccounts(r.accounts)).catch(setError);
|
|
};
|
|
useEffect(() => {
|
|
if (open) { load(false); setPicked(null); setName(""); }
|
|
}, [open]);
|
|
|
|
const add = async () => {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const r = await api("/api/clients", { method: "POST", body: { accountId: picked.account_id, name, runAudit } });
|
|
onClose();
|
|
onAdded?.(r.client);
|
|
} catch (e) {
|
|
setError(e);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onClose={onClose}
|
|
title="Add a business"
|
|
footer={
|
|
<>
|
|
<Button onClick={onClose}>Cancel</Button>
|
|
<Button variant="primary" disabled={!picked} loading={busy} onClick={add}>
|
|
Create client
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<p className="text-muted text-[13px] mb-3">
|
|
Pages connected to the agency's Meta account that don't have a client folder yet. Connect new Pages in Meta Business Suite first.
|
|
</p>
|
|
<ErrorNote error={error} className="mb-3" />
|
|
<div className="flex justify-end mb-2">
|
|
<Button size="sm" variant="ghost" icon={RefreshCw} onClick={() => load(true)}>Check Meta again</Button>
|
|
</div>
|
|
{!accounts && !error ? (
|
|
<Spinner label="Asking Meta…" />
|
|
) : accounts?.length === 0 ? (
|
|
<Empty title="Every connected Page already has a client">Add the new business's Page to the agency's Meta Business account, then check again.</Empty>
|
|
) : (
|
|
<ul className="border border-line rounded-lg divide-y divide-line max-h-[260px] overflow-y-auto">
|
|
{accounts?.map((a) => (
|
|
<li key={a.account_id}>
|
|
<button
|
|
onClick={() => { setPicked(a); setName(a.page_name); }}
|
|
className={cx("w-full text-left px-3.5 py-2.5 flex items-center gap-3", picked?.account_id === a.account_id ? "bg-brand-soft" : "hover:bg-[#f8fafc]")}
|
|
>
|
|
<span className={cx("size-4 rounded-full border-2 shrink-0", picked?.account_id === a.account_id ? "border-brand bg-brand shadow-[inset_0_0_0_2px_white]" : "border-line-strong")} />
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block font-medium truncate">{a.page_name}</span>
|
|
<span className="flex gap-3 text-[12px] text-muted">
|
|
{a.page_id ? <span className="inline-flex items-center gap-1"><Facebook className="size-3" />{a.category || "Page"}</span> : <span>Instagram only · no Facebook Page</span>}
|
|
{a.instagram ? (
|
|
<span className="inline-flex items-center gap-1"><Instagram className="size-3" />@{a.instagram.username} · {fmt(a.instagram.followers_count)} followers</span>
|
|
) : (
|
|
<span>no Instagram linked</span>
|
|
)}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
{picked && (
|
|
<div className="mt-4 space-y-3">
|
|
<Field label="Client name" hint="Used for the folder, reports and AI runs.">
|
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} />
|
|
</Field>
|
|
<label className="flex items-start gap-2 text-[13px]">
|
|
<input type="checkbox" className="mt-0.5 accent-brand" checked={runAudit} onChange={(e) => setRunAudit(e.target.checked)} />
|
|
<span>
|
|
Run the first account audit now
|
|
<span className="block text-muted text-[12px]">Claude drafts brand.md, the internal and client audits and the branded PDFs. Takes a few minutes.</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
)}
|
|
</Dialog>
|
|
);
|
|
}
|