import { useCallback, useEffect, useMemo, useState } from "react";
import { CalendarClock, Check, Image as ImageIcon, Plus, Upload, X } from "lucide-react";
import { api } from "../api.js";
import { useAuth } from "../App.jsx";
import { Badge, Button, cx, Dialog, Empty, ErrorNote, Field, Panel, Spinner } from "../ui.jsx";
import PlatformPicker, { defaultPlatforms, selectionFor, useClientPlatforms } from "../PlatformPicker.jsx";
const STATUS = { draft: "neutral", approved: "brand", publishing: "brand", published: "ok", partial: "warn", failed: "bad", cancelled: "neutral" };
const TYPES = [["IMAGE", "Single image"], ["REELS", "Reel / video"], ["CAROUSEL", "Carousel / multi-image"], ["STORY", "Story (Instagram, Facebook)"], ["TEXT", "Text only (no Instagram)"]];
export default function Publishing() {
const { isAdmin } = useAuth();
const [data, setData] = useState(null);
const [clients, setClients] = useState([]);
const [error, setError] = useState(null);
const [filter, setFilter] = useState("");
const [show, setShow] = useState("upcoming");
const [composing, setComposing] = useState(false);
const [busy, setBusy] = useState(null);
const load = useCallback(() => api("/api/schedule").then(setData).catch(setError), []);
useEffect(() => { load(); api("/api/overview").then((r) => setClients(r.clients)).catch(setError); }, [load]);
useEffect(() => { const t = setInterval(load, 60000); return () => clearInterval(t); }, [load]);
const posts = useMemo(() => (data?.posts || []).filter((p) =>
(!filter || p.client === filter) &&
(show === "all" || (show === "upcoming" ? ["draft", "approved", "publishing", "failed", "partial"].includes(p.status) : p.status === "published"))
), [data, filter, show]);
const act = async (p, what) => {
setBusy(p.id + what); setError(null);
try { await api(`/api/schedule/${p.id}/${what}`, { method: "POST" }); await load(); } catch (e) { setError(e); }
setBusy(null);
};
if (!data) return error ? : ;
const drafts = data.posts.filter((p) => p.status === "draft").length;
const approved = data.posts.filter((p) => p.status === "approved").length;
return (
{!data.publishing &&
Publishing is switched off. Drafts can be prepared, but nothing will be approved or posted until ALLOW_PUBLISH=true is set in .env.
}
{["upcoming", "published", "all"].map((k) => (
))}
{!posts.length ? (
Create a post, or ask the agent to schedule the week's plan. Every post needs an admin's approval before it publishes.
) : (
{posts.map((p) => (
-
{p.clientName}
{p.status}
{p.type.toLowerCase()} · {p.platforms.join(" + ")}
{p.localTime} ({p.timezone.split("/").pop().replace("_", " ")} time)
{p.caption || No caption}
{p.collaborators?.length > 0 &&
Collab: {p.collaborators.map((c) => "@" + c).join(", ")}
}
{Object.entries(p.errors || {}).filter(([k]) => !p.waiting?.[k]).map(([k, v]) =>
{k}: {v}
)}
{Object.entries(p.waiting || {}).map(([k, until]) =>
{k}: daily limit reached. Goes out automatically after {new Date(until).toLocaleString("en-CA", { weekday: "short", hour: "numeric", minute: "2-digit" })}.
)}
{p.status === "draft" && (p.warnings || []).map((w) =>
{w}
)}
{Object.entries(p.results || {}).filter(([, v]) => v?.permalink).map(([k, v]) =>
View on {k})}
{p.results?.facebook?.scheduledOnFacebook && !p.results.facebook.cancelled &&
Facebook part is scheduled in Meta Planner.
}
by {p.createdBy}{p.approvedBy ? ` · approved by ${p.approvedBy}` : ""}
{p.status === "draft" && isAdmin && }
{!["published", "cancelled", "publishing"].includes(p.status) && }
))}
)}
{composing &&
setComposing(false)} onSaved={() => { setComposing(false); load(); }} />}
);
}
function Thumb({ post }) {
const m = post.media?.[0];
const isVid = /\.(mp4|mov|m4v)$/i.test(m || "");
const src = !m ? null : /^https?:/.test(m) ? m : `/api/schedule/media?path=${encodeURIComponent(m)}`;
return (
{!src ?
: isVid ?
:

}
);
}
function Compose({ clients, onClose, onSaved }) {
const [f, setF] = useState({ client: clients[0]?.slug || "", type: "IMAGE", media: [], caption: "", fbCaption: "", collaborators: "", date: "", time: "" });
const available = useClientPlatforms(f.client);
const [sel, setSel] = useState({ platforms: [], options: {} });
useEffect(() => { if (!available.loading) setSel({ platforms: defaultPlatforms(available.list), options: {} }); }, [available]);
const [tz, setTz] = useState(null);
const [uploading, setUploading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
useEffect(() => { if (f.client) api(`/api/schedule/suggest?client=${f.client}`).then(setTz).catch(() => setTz(null)); }, [f.client]);
const upload = async (files) => {
setUploading(true); setError(null);
try {
for (const file of files) {
const res = await fetch("/api/schedule/upload", { method: "POST", credentials: "same-origin", headers: { "X-Filename": file.name, "Content-Type": "application/octet-stream" }, body: file });
const j = await res.json();
if (!res.ok) throw new Error(j.error || "Upload failed");
setF((s) => ({ ...s, media: [...s.media, j.path] }));
}
} catch (e) { setError(e); }
setUploading(false);
};
const save = async () => {
setSaving(true); setError(null);
try {
const { platforms, options } = selectionFor(sel, available, f.type);
await api("/api/schedule", { method: "POST", body: {
client: f.client, platforms, options, type: f.type, media: f.media, caption: f.caption, fbCaption: f.fbCaption || undefined,
collaborators: f.collaborators.split(/[\s,]+/).map((x) => x.replace(/^@/, "")).filter(Boolean), localTime: `${f.date} ${f.time}`,
} });
onSaved();
} catch (e) { setError(e); }
setSaving(false);
};
return (
);
}