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 { prisma } from "@/lib/db";
|
||||||
import { pushCardToMonday } from "@/lib/integrations";
|
import { pushCardToMonday } from "@/lib/integrations";
|
||||||
import { createNotification } from "@/lib/notifications";
|
import { createNotification } from "@/lib/notifications";
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const settings = await prisma.appSettings.findUnique({
|
const settings = await prisma.appSettings.findUnique({
|
||||||
where: { id: "singleton" },
|
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({
|
const cards = await prisma.responseCard.findMany({
|
||||||
where: {
|
where,
|
||||||
ocrStatus: "complete",
|
select: { id: true, name: true, mondayItemId: true },
|
||||||
mondayItemId: null,
|
|
||||||
},
|
|
||||||
select: { id: true, name: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cards.length === 0) {
|
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;
|
let failed = 0;
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
|
@ -43,12 +51,13 @@ export async function POST() {
|
||||||
for (let j = 0; j < results.length; j++) {
|
for (let j = 0; j < results.length; j++) {
|
||||||
const result = results[j];
|
const result = results[j];
|
||||||
if (result.status === "fulfilled") {
|
if (result.status === "fulfilled") {
|
||||||
synced++;
|
if (result.value.action === "created") created++;
|
||||||
|
else updated++;
|
||||||
} else {
|
} else {
|
||||||
failed++;
|
failed++;
|
||||||
const msg = result.reason instanceof Error ? result.reason.message : "Unknown error";
|
const msg = result.reason instanceof Error ? result.reason.message : "Unknown error";
|
||||||
errors.push(`${batch[j].name || batch[j].id}: ${msg}`);
|
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({
|
await createNotification({
|
||||||
type: failed > 0 ? "monday_error" : "monday_sync",
|
type: failed > 0 ? "monday_error" : "monday_sync",
|
||||||
title: "Monday.com Bulk Sync Complete",
|
title: "Monday.com Sync Complete",
|
||||||
message: `${synced} card(s) pushed${failed > 0 ? `, ${failed} failed` : ""}`,
|
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) {
|
} catch (error) {
|
||||||
console.error("[monday/sync-all POST]", error);
|
console.error("[monday/sync-all POST]", error);
|
||||||
const message = error instanceof Error ? error.message : "Sync failed";
|
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);
|
setPushingAll(true);
|
||||||
try {
|
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();
|
const data = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
if (data.synced === 0 && data.failed === 0) {
|
if (data.synced === 0 && data.failed === 0) {
|
||||||
toast.success("All cards are already synced to Monday.com");
|
toast.success(data.message || "All cards are already synced");
|
||||||
} 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`);
|
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
toast.error(data.error || "Sync failed");
|
toast.error(data.error || "Sync failed");
|
||||||
|
|
@ -1050,11 +1060,11 @@ export default function SettingsPage() {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-xl"
|
className="rounded-xl"
|
||||||
onClick={pushAllToMonday}
|
onClick={syncAllToMonday}
|
||||||
disabled={pushingAll || !settings.mondayEnabled || !settings.mondayApiToken || !settings.mondayBoardId}
|
disabled={pushingAll || !settings.mondayEnabled || !settings.mondayApiToken || !settings.mondayBoardId}
|
||||||
>
|
>
|
||||||
{pushingAll ? <Loader2 className="mr-2 size-3 animate-spin" /> : <LayoutGrid className="mr-2 size-3" />}
|
{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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</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 = () => {
|
const handleExportCsv = () => {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
toast.error("No data to export");
|
toast.error("No data to export");
|
||||||
|
|
@ -419,6 +453,7 @@ export function DashboardContent() {
|
||||||
onMarkReviewed={(ids) => handleBulkAction(ids, "reviewed")}
|
onMarkReviewed={(ids) => handleBulkAction(ids, "reviewed")}
|
||||||
onMarkExported={(ids) => handleBulkAction(ids, "exported")}
|
onMarkExported={(ids) => handleBulkAction(ids, "exported")}
|
||||||
onReprocess={handleBulkReprocess}
|
onReprocess={handleBulkReprocess}
|
||||||
|
onSyncMonday={handleBulkSyncMonday}
|
||||||
onDelete={(ids) => handleBulkAction(ids, "delete")}
|
onDelete={(ids) => handleBulkAction(ids, "delete")}
|
||||||
onClear={() => setSelectedIds([])}
|
onClear={() => setSelectedIds([])}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
|
LayoutGrid,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ interface SelectionToolbarProps {
|
||||||
onMarkReviewed?: (ids: string[]) => void;
|
onMarkReviewed?: (ids: string[]) => void;
|
||||||
onMarkExported?: (ids: string[]) => void;
|
onMarkExported?: (ids: string[]) => void;
|
||||||
onReprocess?: (ids: string[]) => void;
|
onReprocess?: (ids: string[]) => void;
|
||||||
|
onSyncMonday?: (ids: string[]) => void;
|
||||||
onDelete?: (ids: string[]) => void;
|
onDelete?: (ids: string[]) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +56,7 @@ export function SelectionToolbar({
|
||||||
onMarkReviewed,
|
onMarkReviewed,
|
||||||
onMarkExported,
|
onMarkExported,
|
||||||
onReprocess,
|
onReprocess,
|
||||||
|
onSyncMonday,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClear,
|
onClear,
|
||||||
}: SelectionToolbarProps) {
|
}: SelectionToolbarProps) {
|
||||||
|
|
@ -154,6 +157,17 @@ export function SelectionToolbar({
|
||||||
<span className="hidden sm:inline ml-1">Reprocess</span>
|
<span className="hidden sm:inline ml-1">Reprocess</span>
|
||||||
</Button>
|
</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}>
|
<Popover open={copyOpen} onOpenChange={setCopyOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue