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
This commit is contained in:
parent
05a40cca51
commit
d6d209cf9d
4 changed files with 99 additions and 25 deletions
|
|
@ -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<string, unknown> = { 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";
|
||||
|
|
|
|||
|
|
@ -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 ? <Loader2 className="mr-2 size-3 animate-spin" /> : <LayoutGrid className="mr-2 size-3" />}
|
||||
Push All to Monday.com
|
||||
Sync All to Monday.com
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -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([])}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<span className="hidden sm:inline ml-1">Reprocess</span>
|
||||
</Button>
|
||||
)}
|
||||
{onSyncMonday && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="rounded-xl"
|
||||
onClick={() => onSyncMonday(selectedIds)}
|
||||
>
|
||||
<LayoutGrid className="size-4" />
|
||||
<span className="hidden sm:inline ml-1">Monday.com</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Popover open={copyOpen} onOpenChange={setCopyOpen}>
|
||||
<PopoverTrigger
|
||||
|
|
|
|||
Loading…
Reference in a new issue