diff --git a/dashboard/server/index.js b/dashboard/server/index.js index ed0436b..371062c 100644 --- a/dashboard/server/index.js +++ b/dashboard/server/index.js @@ -445,6 +445,31 @@ app.post("/api/library/:id/review", requireAdmin, h(async (req) => { await odoo.taskActivity(item.taskRef, { note: ok ? `
Approved by ${esc(who(req))}: ${esc(item.title)}.
` : `Changes requested by ${esc(who(req))}: ${esc(item.reviewNote)}
`, stageMatch: ok ? "done" : "progress" }); return { item }; })); +// Schedule several approved items at once: one per slot (start date, every N days, cycling through the times). +// Items with their own planned date keep it. Creates drafts only; each still needs approval. +app.post("/api/library/bulk-schedule", h(async (req) => { + const { client, ids = [], startDate, everyDays = 1, times = [], platforms = [], options = {} } = req.body || {}; + clients.safeSlug(client); + if (!/^\d{4}-\d{2}-\d{2}$/.test(String(startDate))) throw Object.assign(new Error("Pick a start date."), { status: 400 }); + const slots = (times.length ? times : ["10:00"]).filter((t) => /^\d{2}:\d{2}$/.test(t)); + if (!slots.length) throw Object.assign(new Error("Times must be like 10:00."), { status: 400 }); + if (!platforms.length) throw Object.assign(new Error("Choose at least one platform."), { status: 400 }); + const step = Math.min(Math.max(Number(everyDays) || 1, 1), 14); + const items = ids.map((id) => lib.getItem(id)).filter((i) => i.client === client && i.status === "approved") + .sort((a, b) => (a.forDate || "9999").localeCompare(b.forDate || "9999") || a.createdAt.localeCompare(b.createdAt)); + const day = (iso, n) => { const d = new Date(`${iso}T12:00:00Z`); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); }; + const created = [], errors = []; + let k = 0; + for (const i of items) { + const own = i.forDate && i.forDate >= startDate; + const date = own ? i.forDate : day(startDate, k * step); + const time = slots[(own ? created.length : k) % slots.length]; + if (!own) k += 1; + try { const post = lib.scheduleItem(i.id, { platforms, localTime: `${date} ${time}`, options }, who(req)); created.push({ id: i.id, title: i.title, localTime: post.localTime, warnings: post.warnings }); } + catch (e) { errors.push({ id: i.id, title: i.title, error: e.message }); } + } + return { created, errors }; +})); app.post("/api/library/:id/schedule", h(async (req) => ({ post: lib.scheduleItem(req.params.id, req.body || {}, who(req)), item: lib.getItem(req.params.id) }))); app.post("/api/library/:id/archive", h(async (req) => ({ item: lib.archiveItem(req.params.id, who(req)) }))); app.get("/api/library/file", h(async (req, res) => { diff --git a/dashboard/web/src/pages/Library.jsx b/dashboard/web/src/pages/Library.jsx index 91d58cc..bb065e9 100644 --- a/dashboard/web/src/pages/Library.jsx +++ b/dashboard/web/src/pages/Library.jsx @@ -32,7 +32,10 @@ export default function Library() { const [tab, setTab] = useState(isAdmin ? "submitted" : "changes"); const [data, setData] = useState(null); const [error, setError] = useState(null); - const [dialog, setDialog] = useState(null); // {kind:'upload'} | {kind:'review'|'schedule'|'version', item} + const [dialog, setDialog] = useState(null); // {kind:'upload'} | {kind:'review'|'schedule'|'version', item} | {kind:'bulk'} + const [picked, setPicked] = useState([]); + const canBulk = tab === "approved" && Boolean(client); + useEffect(() => { setPicked([]); }, [client, tab]); useEffect(() => { api("/api/overview").then((r) => setClients(r.clients)).catch(setError); }, []); const load = useCallback(() => api(`/api/library${client ? `?client=${client}` : ""}`).then(setData).catch(setError), [client]); @@ -63,6 +66,8 @@ export default function Library() { ))} + {canBulk && } + {tab === "approved" && !client && Pick a client to schedule several at once.}