diff --git a/src/app/api/supplier/vendors/[connectionId]/route.ts b/src/app/api/supplier/vendors/[connectionId]/route.ts index 88148c7..77cce53 100644 --- a/src/app/api/supplier/vendors/[connectionId]/route.ts +++ b/src/app/api/supplier/vendors/[connectionId]/route.ts @@ -99,7 +99,7 @@ export async function PUT( return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) } - // Verify connection belongs to supplier + // First try normal connection update flow const connection = await prisma.connection.findFirst({ where: { id: connectionId, @@ -107,18 +107,80 @@ export async function PUT( } }) - if (!connection) { - return NextResponse.json({ message: "Connection not found" }, { status: 404 }) + if (connection) { + const updatedConnection = await prisma.connection.update({ + where: { id: connectionId }, + data: { status: status as "CONNECTED" | "REJECTED" } + }) + + return NextResponse.json({ + message: `Connection ${status.toLowerCase()} successfully`, + connection: updatedConnection + }) } - const updatedConnection = await prisma.connection.update({ - where: { id: connectionId }, - data: { status: status as "CONNECTED" | "REJECTED" } + // If not a connection, try treating `connectionId` as a pending invitation id. + const invitation = await prisma.invitation.findFirst({ + where: { + id: connectionId, + supplierId: supplierProfile.id, + used: false, + expiresAt: { + gt: new Date() + } + } + }) + + if (!invitation) { + return NextResponse.json({ message: "Connection or invitation not found" }, { status: 404 }) + } + + if (status === "CONNECTED") { + // Accept invitation: mark used and create connection in one transaction. + const createdConnection = await prisma.$transaction(async (tx) => { + await tx.invitation.update({ + where: { id: invitation.id }, + data: { + used: true, + usedAt: new Date() + } + }) + + return tx.connection.upsert({ + where: { + storeId_supplierId: { + storeId: invitation.storeId, + supplierId: invitation.supplierId + } + }, + update: { + status: "CONNECTED" + }, + create: { + storeId: invitation.storeId, + supplierId: invitation.supplierId, + status: "CONNECTED" + } + }) + }) + + return NextResponse.json({ + message: "Invitation accepted successfully", + connection: createdConnection + }) + } + + // Reject invitation: consume it so it disappears from pending list. + await prisma.invitation.update({ + where: { id: invitation.id }, + data: { + used: true, + usedAt: new Date() + } }) return NextResponse.json({ - message: `Connection ${status.toLowerCase()} successfully`, - connection: updatedConnection + message: "Invitation rejected successfully" }) } catch (error) { console.error("Error updating connection:", error) diff --git a/src/app/api/supplier/vendors/route.ts b/src/app/api/supplier/vendors/route.ts index 3c6c520..b8d6092 100644 --- a/src/app/api/supplier/vendors/route.ts +++ b/src/app/api/supplier/vendors/route.ts @@ -48,7 +48,60 @@ export async function GET() { } }) - return NextResponse.json({ connections }) + // Include pending invitations so supplier sees new invites immediately + const pendingInvitations = await prisma.invitation.findMany({ + where: { + supplierId: supplierProfile.id, + used: false, + expiresAt: { + gt: new Date() + } + }, + include: { + store: { + select: { + id: true, + businessName: true, + shopifyDomain: true, + shopifyStoreName: true, + user: { + select: { + name: true, + email: true + } + } + } + } + }, + orderBy: { + createdAt: "desc" + } + }) + + type ConnectionRow = (typeof connections)[number] + type InvitationRow = (typeof pendingInvitations)[number] + + const connectionStoreIds = new Set( + connections.map((c: ConnectionRow) => c.store.id) + ) + + // If a real connection already exists for a store, hide pending invitation for that same store. + const pendingAsConnections = pendingInvitations + .filter((inv: InvitationRow) => !connectionStoreIds.has(inv.store.id)) + .map((inv: InvitationRow) => ({ + id: inv.id, + status: "PENDING" as const, + createdAt: inv.createdAt, + store: inv.store, + invitationToken: inv.token, + invitationExpiresAt: inv.expiresAt, + })) + + const allConnections = [...connections, ...pendingAsConnections].sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) + + return NextResponse.json({ connections: allConnections }) } catch (error) { console.error("Error fetching vendor connections:", error) return NextResponse.json( diff --git a/src/app/supplier/vendors/page.tsx b/src/app/supplier/vendors/page.tsx index 4747b16..5725dcc 100644 --- a/src/app/supplier/vendors/page.tsx +++ b/src/app/supplier/vendors/page.tsx @@ -65,10 +65,13 @@ function SupplierVendorsContent() { const [invitationToken, setInvitationToken] = useState("") const [acceptingToken, setAcceptingToken] = useState(false) const [showProductSyncs, setShowProductSyncs] = useState(false) + const [isInitialLoad, setIsInitialLoad] = useState(true) useEffect(() => { - fetchConnections() - fetchProductSyncs() + void (async () => { + await Promise.all([fetchConnections({ silent: false }), fetchProductSyncs()]) + setIsInitialLoad(false) + })() // Check if token is in URL const token = searchParams.get("token") @@ -78,19 +81,15 @@ function SupplierVendorsContent() { } }, [searchParams]) - // Poll for updates every 5 seconds if there are pending connections or product syncs + // Poll for updates every 5 seconds so new invitations appear without manual refresh useEffect(() => { - const hasPending = connections.some(c => c.status === "PENDING") - const hasPendingSyncs = productSyncs.some(s => s.status === "PENDING") - if (!hasPending && !hasPendingSyncs) return // Don't poll if no pending items - const interval = setInterval(() => { - fetchConnections() - fetchProductSyncs() + void fetchConnections({ silent: true }) + void fetchProductSyncs() }, 5000) // Poll every 5 seconds return () => clearInterval(interval) - }, [connections, productSyncs]) // Re-run when connections or syncs change + }, []) useEffect(() => { if (success) { @@ -106,9 +105,9 @@ function SupplierVendorsContent() { } }, [error]) - const fetchConnections = async () => { + const fetchConnections = async ({ silent = false }: { silent?: boolean } = {}) => { try { - setLoading(true) + if (!silent) setLoading(true) const res = await fetch("/api/supplier/vendors") if (!res.ok) throw new Error("Failed to fetch connections") const data = await res.json() @@ -117,7 +116,7 @@ function SupplierVendorsContent() { } catch { setError("Failed to load vendor connections") } finally { - setLoading(false) + if (!silent) setLoading(false) } } @@ -142,7 +141,7 @@ function SupplierVendorsContent() { }) if (!res.ok) throw new Error("Failed to update connection") setSuccess(`Connection ${status.toLowerCase()} successfully`) - fetchConnections() + void fetchConnections({ silent: true }) } catch { setError("Failed to update connection") } @@ -157,7 +156,7 @@ function SupplierVendorsContent() { }) if (!res.ok) throw new Error("Failed to delete connection") setSuccess("Connection removed successfully") - fetchConnections() + void fetchConnections({ silent: true }) } catch { setError("Failed to remove connection") } @@ -175,8 +174,8 @@ function SupplierVendorsContent() { throw new Error(data.message || "Failed to update sync request") } setSuccess(`Product sync ${action === "accept" ? "accepted" : "rejected"} successfully`) - fetchProductSyncs() - fetchConnections() + void fetchProductSyncs() + void fetchConnections({ silent: true }) } catch (err) { setError(err instanceof Error ? err.message : "Failed to update sync request") } @@ -201,7 +200,7 @@ function SupplierVendorsContent() { setSuccess(data.message || "Invitation accepted successfully! You are now connected.") setInvitationToken("") setShowTokenInput(false) - fetchConnections() + void fetchConnections({ silent: true }) // Send supplier to JUNO CHAT so encryption keys auto-setup on first open setTimeout(() => router.push("/supplier/chat"), 1500) } catch (err) { @@ -423,7 +422,7 @@ function SupplierVendorsContent() { ))} - {loading ? ( + {loading && isInitialLoad ? (