Add manual push to Monday.com (single card + bulk sync)
- Extract reusable pushCardToMonday() that creates or updates Monday items - New /api/integrations/monday/push/[id] endpoint for single-card push - New /api/integrations/monday/sync-all endpoint for bulk push of all unsynced cards (ocrStatus=complete, no mondayItemId) - Add "Push to Monday.com" button on card detail page header (shows "Update Monday" when card already has a Monday item) - Add "Push All to Monday.com" button in Monday.com settings card Made-with: Cursor
This commit is contained in:
parent
2fcdd687f7
commit
7aefe7c4a1
5 changed files with 207 additions and 34 deletions
21
src/app/api/integrations/monday/push/[id]/route.ts
Normal file
21
src/app/api/integrations/monday/push/[id]/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pushCardToMonday } from "@/lib/integrations";
|
||||
|
||||
export async function POST(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const result = await pushCardToMonday(id);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
action: result.action,
|
||||
mondayItemId: result.mondayItemId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[monday/push POST]", error);
|
||||
const message = error instanceof Error ? error.message : "Push failed";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
58
src/app/api/integrations/monday/sync-all/route.ts
Normal file
58
src/app/api/integrations/monday/sync-all/route.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { pushCardToMonday } from "@/lib/integrations";
|
||||
import { createNotification } from "@/lib/notifications";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const settings = await prisma.appSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
});
|
||||
if (!settings?.mondayEnabled || !settings?.mondayApiToken || !settings?.mondayBoardId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Monday.com integration is not configured or enabled" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const cards = await prisma.responseCard.findMany({
|
||||
where: {
|
||||
ocrStatus: "complete",
|
||||
mondayItemId: null,
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
if (cards.length === 0) {
|
||||
return NextResponse.json({ ok: true, synced: 0, failed: 0, message: "All cards are already synced" });
|
||||
}
|
||||
|
||||
let synced = 0;
|
||||
let failed = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const card of cards) {
|
||||
try {
|
||||
await pushCardToMonday(card.id, settings);
|
||||
synced++;
|
||||
} catch (err) {
|
||||
failed++;
|
||||
const msg = err instanceof Error ? err.message : "Unknown error";
|
||||
errors.push(`${card.name || card.id}: ${msg}`);
|
||||
console.error(`[sync-all] Failed to push card ${card.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
await createNotification({
|
||||
type: failed > 0 ? "monday_error" : "monday_sync",
|
||||
title: "Monday.com Bulk Sync Complete",
|
||||
message: `${synced} card(s) pushed${failed > 0 ? `, ${failed} failed` : ""}`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, synced, 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";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ type CardData = {
|
|||
ocrConfidence: number | null;
|
||||
ocrError: string | null;
|
||||
rawOcrResponse: Record<string, unknown> | null;
|
||||
mondayItemId: string | null;
|
||||
frontImageUrl: string | null;
|
||||
backImageUrl: string | null;
|
||||
};
|
||||
|
|
@ -111,6 +112,7 @@ export default function CardDetailPage() {
|
|||
const [activityLog, setActivityLog] = React.useState<ActivityEntry[]>([]);
|
||||
const [activityLoading, setActivityLoading] = React.useState(false);
|
||||
const [expandedEntry, setExpandedEntry] = React.useState<string | null>(null);
|
||||
const [pushingToMonday, setPushingToMonday] = React.useState(false);
|
||||
|
||||
const fetchCard = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -211,6 +213,24 @@ export default function CardDetailPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handlePushToMonday = async () => {
|
||||
setPushingToMonday(true);
|
||||
try {
|
||||
const res = await fetch(`/api/integrations/monday/push/${id}`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
toast.success(data.action === "created" ? "Pushed to Monday.com" : "Updated in Monday.com");
|
||||
fetchCard();
|
||||
} else {
|
||||
toast.error(data.error || "Push to Monday.com failed");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to push to Monday.com");
|
||||
} finally {
|
||||
setPushingToMonday(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchActivity = React.useCallback(async () => {
|
||||
setActivityLoading(true);
|
||||
try {
|
||||
|
|
@ -304,6 +324,19 @@ export default function CardDetailPage() {
|
|||
<Loader2 className="size-3.5 animate-spin" /> Processing...
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-xl"
|
||||
onClick={handlePushToMonday}
|
||||
disabled={pushingToMonday || ocrStatus !== "complete"}
|
||||
>
|
||||
{pushingToMonday ? (
|
||||
<><Loader2 className="mr-1 size-4 animate-spin" /> Pushing...</>
|
||||
) : (
|
||||
<><LayoutGrid className="mr-1 size-4" /> {card.mondayItemId ? "Update Monday" : "Push to Monday"}</>
|
||||
)}
|
||||
</Button>
|
||||
{reviewStatus !== "reviewed" && (
|
||||
<Button variant="outline" size="sm" className="rounded-xl" onClick={handleMarkReviewed}>
|
||||
<Check className="mr-1 size-4" /> Mark Reviewed
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ export default function SettingsPage() {
|
|||
const [mondayColumns, setMondayColumns] = React.useState<MondayColumn[]>([]);
|
||||
const [fetchingColumns, setFetchingColumns] = React.useState(false);
|
||||
const [subscribing, setSubscribing] = React.useState(false);
|
||||
const [pushingAll, setPushingAll] = React.useState(false);
|
||||
const [webhookTestStatus, setWebhookTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle");
|
||||
const [notifPrefs, setNotifPrefs] = React.useState<NotificationPrefs>(DEFAULT_NOTIFICATION_PREFS);
|
||||
|
||||
|
|
@ -399,6 +400,27 @@ export default function SettingsPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const pushAllToMonday = async () => {
|
||||
setPushingAll(true);
|
||||
try {
|
||||
const res = await fetch("/api/integrations/monday/sync-all", { method: "POST" });
|
||||
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 {
|
||||
toast.success(`Pushed ${data.synced} card(s) to Monday.com${data.failed > 0 ? ` (${data.failed} failed)` : ""}`);
|
||||
}
|
||||
} else {
|
||||
toast.error(data.error || "Sync failed");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to sync cards to Monday.com");
|
||||
} finally {
|
||||
setPushingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testWebhook = async () => {
|
||||
setWebhookTestStatus("testing");
|
||||
try {
|
||||
|
|
@ -954,12 +976,24 @@ export default function SettingsPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-xs font-medium text-muted-foreground">Enable Integration</Label>
|
||||
<Switch
|
||||
checked={settings.mondayEnabled}
|
||||
onCheckedChange={(val: boolean) => setSettings((s) => ({ ...s, mondayEnabled: val }))}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-xs font-medium text-muted-foreground">Enable Integration</Label>
|
||||
<Switch
|
||||
checked={settings.mondayEnabled}
|
||||
onCheckedChange={(val: boolean) => setSettings((s) => ({ ...s, mondayEnabled: val }))}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-xl"
|
||||
onClick={pushAllToMonday}
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -169,36 +169,12 @@ async function handleMonday(
|
|||
card: Record<string, unknown>,
|
||||
cardId: string
|
||||
) {
|
||||
const token = settings.mondayApiToken;
|
||||
const boardId = settings.mondayBoardId;
|
||||
const columnMap = (settings.mondayColumnMap as Record<string, string>) ?? {};
|
||||
|
||||
if (event === "ocr_complete") {
|
||||
const columnValues = mapCardToColumnValues(card, columnMap);
|
||||
const itemName = (card.name as string) || "Unnamed Card";
|
||||
|
||||
const itemId = await createItem(token, boardId, itemName, columnValues);
|
||||
await prisma.responseCard.update({
|
||||
where: { id: cardId },
|
||||
data: { mondayItemId: itemId },
|
||||
});
|
||||
|
||||
// Upload image attachments if a files column is mapped
|
||||
const filesColId = columnMap._files;
|
||||
if (filesColId) {
|
||||
for (const imgPath of [card.backImagePath, card.frontImagePath]) {
|
||||
if (typeof imgPath === "string" && imgPath) {
|
||||
try {
|
||||
const buffer = await getBuffer(imgPath);
|
||||
const fileName = imgPath.split("/").pop() || "scan.jpg";
|
||||
await uploadFileToItem(token, itemId, filesColId, buffer, fileName);
|
||||
} catch (err) {
|
||||
console.error("[integrations] File upload to Monday failed:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await pushCardToMonday(cardId, settings);
|
||||
} else if (event === "card_reviewed" || event === "card_exported") {
|
||||
const token = settings.mondayApiToken;
|
||||
const boardId = settings.mondayBoardId;
|
||||
const columnMap = (settings.mondayColumnMap as Record<string, string>) ?? {};
|
||||
const mondayItemId = card.mondayItemId as string | null;
|
||||
if (mondayItemId) {
|
||||
const columnValues = mapCardToColumnValues(card, columnMap);
|
||||
|
|
@ -206,3 +182,54 @@ async function handleMonday(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function pushCardToMonday(
|
||||
cardId: string,
|
||||
settingsOverride?: { mondayApiToken: string; mondayBoardId: string; mondayColumnMap: unknown } | null
|
||||
) {
|
||||
const settings = settingsOverride ?? await prisma.appSettings.findUnique({ where: { id: "singleton" } });
|
||||
if (!settings?.mondayApiToken || !settings?.mondayBoardId) {
|
||||
throw new Error("Monday.com is not configured");
|
||||
}
|
||||
|
||||
const card = await prisma.responseCard.findUnique({ where: { id: cardId } });
|
||||
if (!card) throw new Error("Card not found");
|
||||
|
||||
const cardData = card as unknown as Record<string, unknown>;
|
||||
const token = settings.mondayApiToken;
|
||||
const boardId = settings.mondayBoardId;
|
||||
const columnMap = (settings.mondayColumnMap as Record<string, string>) ?? {};
|
||||
|
||||
const columnValues = mapCardToColumnValues(cardData, columnMap);
|
||||
const itemName = (card.name as string) || "Unnamed Card";
|
||||
|
||||
if (card.mondayItemId) {
|
||||
await updateItem(token, boardId, card.mondayItemId, columnValues);
|
||||
return { action: "updated" as const, mondayItemId: card.mondayItemId };
|
||||
}
|
||||
|
||||
const itemId = await createItem(token, boardId, itemName, columnValues);
|
||||
await prisma.responseCard.update({
|
||||
where: { id: cardId },
|
||||
data: { mondayItemId: itemId },
|
||||
});
|
||||
|
||||
const filesColId = columnMap._files;
|
||||
if (filesColId) {
|
||||
for (const imgPath of [cardData.backImagePath, cardData.frontImagePath]) {
|
||||
if (typeof imgPath === "string" && imgPath) {
|
||||
try {
|
||||
const buffer = await getBuffer(imgPath);
|
||||
const fileName = imgPath.split("/").pop() || "scan.jpg";
|
||||
await uploadFileToItem(token, itemId, filesColId, buffer, fileName);
|
||||
} catch (err) {
|
||||
console.error("[integrations] File upload to Monday failed:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await logActivity(cardId, "monday_sync", "system", "Card pushed to Monday.com");
|
||||
|
||||
return { action: "created" as const, mondayItemId: itemId };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue