diff --git a/src/app/api/integrations/monday/push/[id]/route.ts b/src/app/api/integrations/monday/push/[id]/route.ts new file mode 100644 index 0000000..bade849 --- /dev/null +++ b/src/app/api/integrations/monday/push/[id]/route.ts @@ -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 }); + } +} diff --git a/src/app/api/integrations/monday/sync-all/route.ts b/src/app/api/integrations/monday/sync-all/route.ts new file mode 100644 index 0000000..d0c7fb3 --- /dev/null +++ b/src/app/api/integrations/monday/sync-all/route.ts @@ -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 }); + } +} diff --git a/src/app/cards/[id]/page.tsx b/src/app/cards/[id]/page.tsx index f900b94..be635b9 100644 --- a/src/app/cards/[id]/page.tsx +++ b/src/app/cards/[id]/page.tsx @@ -83,6 +83,7 @@ type CardData = { ocrConfidence: number | null; ocrError: string | null; rawOcrResponse: Record | null; + mondayItemId: string | null; frontImageUrl: string | null; backImageUrl: string | null; }; @@ -111,6 +112,7 @@ export default function CardDetailPage() { const [activityLog, setActivityLog] = React.useState([]); const [activityLoading, setActivityLoading] = React.useState(false); const [expandedEntry, setExpandedEntry] = React.useState(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() { Processing... )} + {reviewStatus !== "reviewed" && ( diff --git a/src/lib/integrations.ts b/src/lib/integrations.ts index e2fb54c..d1a6cd9 100644 --- a/src/lib/integrations.ts +++ b/src/lib/integrations.ts @@ -169,36 +169,12 @@ async function handleMonday( card: Record, cardId: string ) { - const token = settings.mondayApiToken; - const boardId = settings.mondayBoardId; - const columnMap = (settings.mondayColumnMap as Record) ?? {}; - 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) ?? {}; 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; + const token = settings.mondayApiToken; + const boardId = settings.mondayBoardId; + const columnMap = (settings.mondayColumnMap as Record) ?? {}; + + 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 }; +}