From d6d209cf9da9b85832da5e14bc6a30a735935324 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Thu, 9 Apr 2026 15:14:48 -0500 Subject: [PATCH] Add Monday.com sync-all with push+update support and batch toolbar button - Sync-all endpoint now supports mode param: "push" (new only), "update" (existing only), or "all" (both); also accepts cardIds for batch ops - Response includes separate created/updated/failed counts - Settings button renamed to "Sync All to Monday.com" and shows breakdown - Add "Monday.com" button to the floating selection toolbar for syncing selected cards in batch Made-with: Cursor --- .../api/integrations/monday/sync-all/route.ts | 43 +++++++++++++------ src/app/settings/page.tsx | 32 +++++++++----- src/components/cards/dashboard-content.tsx | 35 +++++++++++++++ src/components/cards/selection-toolbar.tsx | 14 ++++++ 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/app/api/integrations/monday/sync-all/route.ts b/src/app/api/integrations/monday/sync-all/route.ts index f19cc78..7ab5f6e 100644 --- a/src/app/api/integrations/monday/sync-all/route.ts +++ b/src/app/api/integrations/monday/sync-all/route.ts @@ -1,9 +1,9 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; import { pushCardToMonday } from "@/lib/integrations"; import { createNotification } from "@/lib/notifications"; -export async function POST() { +export async function POST(req: NextRequest) { try { const settings = await prisma.appSettings.findUnique({ where: { id: "singleton" }, @@ -15,19 +15,27 @@ export async function POST() { ); } + const body = await req.json().catch(() => ({})); + const mode: "push" | "update" | "all" = body.mode || "all"; + const cardIds: string[] | undefined = body.cardIds; + + const where: Record = { ocrStatus: "complete" }; + if (mode === "push") where.mondayItemId = null; + else if (mode === "update") where.mondayItemId = { not: null }; + if (cardIds?.length) where.id = { in: cardIds }; + const cards = await prisma.responseCard.findMany({ - where: { - ocrStatus: "complete", - mondayItemId: null, - }, - select: { id: true, name: true }, + where, + select: { id: true, name: true, mondayItemId: true }, }); if (cards.length === 0) { - return NextResponse.json({ ok: true, synced: 0, failed: 0, message: "All cards are already synced" }); + const label = mode === "push" ? "push" : mode === "update" ? "update" : "sync"; + return NextResponse.json({ ok: true, synced: 0, failed: 0, created: 0, updated: 0, message: `No cards to ${label}` }); } - let synced = 0; + let created = 0; + let updated = 0; let failed = 0; const errors: string[] = []; @@ -43,12 +51,13 @@ export async function POST() { for (let j = 0; j < results.length; j++) { const result = results[j]; if (result.status === "fulfilled") { - synced++; + if (result.value.action === "created") created++; + else updated++; } else { failed++; const msg = result.reason instanceof Error ? result.reason.message : "Unknown error"; errors.push(`${batch[j].name || batch[j].id}: ${msg}`); - console.error(`[sync-all] Failed to push card ${batch[j].id}:`, result.reason); + console.error(`[sync-all] Failed to sync card ${batch[j].id}:`, result.reason); } } @@ -57,13 +66,19 @@ export async function POST() { } } + const synced = created + updated; + const parts: string[] = []; + if (created > 0) parts.push(`${created} pushed`); + if (updated > 0) parts.push(`${updated} updated`); + if (failed > 0) parts.push(`${failed} failed`); + await createNotification({ type: failed > 0 ? "monday_error" : "monday_sync", - title: "Monday.com Bulk Sync Complete", - message: `${synced} card(s) pushed${failed > 0 ? `, ${failed} failed` : ""}`, + title: "Monday.com Sync Complete", + message: parts.join(", "), }); - return NextResponse.json({ ok: true, synced, failed, total: cards.length, errors: errors.slice(0, 10) }); + return NextResponse.json({ ok: true, synced, created, updated, failed, total: cards.length, errors: errors.slice(0, 10) }); } catch (error) { console.error("[monday/sync-all POST]", error); const message = error instanceof Error ? error.message : "Sync failed"; diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 3b02f2a..6400d97 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -415,21 +415,31 @@ export default function SettingsPage() { } }; - const pushAllToMonday = async () => { + const syncAllToMonday = async () => { setPushingAll(true); try { - const res = await fetch("/api/integrations/monday/sync-all", { method: "POST" }); + const res = await fetch("/api/integrations/monday/sync-all", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mode: "all" }), + }); const data = await res.json(); if (res.ok) { if (data.synced === 0 && data.failed === 0) { - toast.success("All cards are already synced to Monday.com"); - } else if (data.failed > 0 && data.synced === 0) { - const errDetail = data.errors?.[0] ? `\n${data.errors[0]}` : ""; - toast.error(`All ${data.failed} card(s) failed to push to Monday.com${errDetail}`); - } else if (data.failed > 0) { - toast.warning(`Pushed ${data.synced} card(s), but ${data.failed} failed`); + toast.success(data.message || "All cards are already synced"); } else { - toast.success(`Pushed ${data.synced} card(s) to Monday.com`); + const parts: string[] = []; + if (data.created > 0) parts.push(`${data.created} pushed`); + if (data.updated > 0) parts.push(`${data.updated} updated`); + if (data.failed > 0) parts.push(`${data.failed} failed`); + const msg = parts.join(", "); + if (data.failed > 0 && data.synced === 0) { + toast.error(`Monday.com: ${msg}`, { description: data.errors?.[0] }); + } else if (data.failed > 0) { + toast.warning(`Monday.com: ${msg}`); + } else { + toast.success(`Monday.com: ${msg}`); + } } } else { toast.error(data.error || "Sync failed"); @@ -1050,11 +1060,11 @@ export default function SettingsPage() { variant="outline" size="sm" className="rounded-xl" - onClick={pushAllToMonday} + onClick={syncAllToMonday} disabled={pushingAll || !settings.mondayEnabled || !settings.mondayApiToken || !settings.mondayBoardId} > {pushingAll ? : } - Push All to Monday.com + Sync All to Monday.com diff --git a/src/components/cards/dashboard-content.tsx b/src/components/cards/dashboard-content.tsx index fa4f9ad..5c7fb4d 100644 --- a/src/components/cards/dashboard-content.tsx +++ b/src/components/cards/dashboard-content.tsx @@ -167,6 +167,40 @@ export function DashboardContent() { } }; + const handleBulkSyncMonday = async (ids: string[]) => { + try { + toast.info(`Syncing ${ids.length} card(s) to Monday.com…`); + const res = await fetch("/api/integrations/monday/sync-all", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ cardIds: ids, mode: "all" }), + }); + const result = await res.json(); + if (!res.ok) { + toast.error(result.error || "Monday.com sync failed"); + return; + } + const parts: string[] = []; + if (result.created > 0) parts.push(`${result.created} pushed`); + if (result.updated > 0) parts.push(`${result.updated} updated`); + if (result.failed > 0) parts.push(`${result.failed} failed`); + if (result.failed > 0 && result.synced === 0) { + toast.error(`Monday.com: ${parts.join(", ")}`, { + description: result.errors?.[0], + }); + } else if (result.failed > 0) { + toast.warning(`Monday.com: ${parts.join(", ")}`); + } else if (result.synced === 0) { + toast.info("No eligible cards to sync"); + } else { + toast.success(`Monday.com: ${parts.join(", ")}`); + } + fetchCards(); + } catch { + toast.error("Failed to sync to Monday.com"); + } + }; + const handleExportCsv = () => { if (data.length === 0) { toast.error("No data to export"); @@ -419,6 +453,7 @@ export function DashboardContent() { onMarkReviewed={(ids) => handleBulkAction(ids, "reviewed")} onMarkExported={(ids) => handleBulkAction(ids, "exported")} onReprocess={handleBulkReprocess} + onSyncMonday={handleBulkSyncMonday} onDelete={(ids) => handleBulkAction(ids, "delete")} onClear={() => setSelectedIds([])} /> diff --git a/src/components/cards/selection-toolbar.tsx b/src/components/cards/selection-toolbar.tsx index 654ca9e..ec93394 100644 --- a/src/components/cards/selection-toolbar.tsx +++ b/src/components/cards/selection-toolbar.tsx @@ -8,6 +8,7 @@ import { RefreshCw, Trash2, X, + LayoutGrid, } from "lucide-react"; import { toast } from "sonner"; @@ -27,6 +28,7 @@ interface SelectionToolbarProps { onMarkReviewed?: (ids: string[]) => void; onMarkExported?: (ids: string[]) => void; onReprocess?: (ids: string[]) => void; + onSyncMonday?: (ids: string[]) => void; onDelete?: (ids: string[]) => void; onClear: () => void; } @@ -54,6 +56,7 @@ export function SelectionToolbar({ onMarkReviewed, onMarkExported, onReprocess, + onSyncMonday, onDelete, onClear, }: SelectionToolbarProps) { @@ -154,6 +157,17 @@ export function SelectionToolbar({ Reprocess )} + {onSyncMonday && ( + + )}