diff --git a/app/(protected)/admin/page.tsx b/app/(protected)/admin/page.tsx
index cb5f75d..905db58 100644
--- a/app/(protected)/admin/page.tsx
+++ b/app/(protected)/admin/page.tsx
@@ -3,7 +3,7 @@ import { redirect } from "next/navigation";
import React from "react";
import { DataTable } from "./data-table";
import { columns } from "./columns";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
const AdminPage = async () => {
const currentUserRole = await getCurrentUserRole();
diff --git a/app/(protected)/inferred-deals/[uid]/edit/page.tsx b/app/(protected)/inferred-deals/[uid]/edit/page.tsx
index b19699b..801819e 100644
--- a/app/(protected)/inferred-deals/[uid]/edit/page.tsx
+++ b/app/(protected)/inferred-deals/[uid]/edit/page.tsx
@@ -2,7 +2,7 @@ import EditDealForm from "@/components/forms/edit-deal-form";
import PreviousPageButton from "@/components/PreviousPageButton";
import { Card, CardContent } from "@/components/ui/card";
import { fetchSpecificInferredDeal } from "@/lib/firebase/db";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Metadata } from "next";
import React from "react";
diff --git a/app/(protected)/inferred-deals/[uid]/page.tsx b/app/(protected)/inferred-deals/[uid]/page.tsx
index 1a14123..1a53c5b 100644
--- a/app/(protected)/inferred-deals/[uid]/page.tsx
+++ b/app/(protected)/inferred-deals/[uid]/page.tsx
@@ -39,7 +39,7 @@ import {
import { Badge } from "@/components/ui/badge";
import PreviousPageButton from "@/components/PreviousPageButton";
import ScreenDealDialog from "@/components/Dialogs/screen-deal-dialog";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { DealDetailItem } from "@/components/DealDetailItem";
import AIReasoning from "@/components/AiReasoning";
import SimUploadDialog from "@/components/Dialogs/sim-upload-dialog";
diff --git a/app/(protected)/inferred-deals/page.tsx b/app/(protected)/inferred-deals/page.tsx
index 9a7fc1f..bfdacce 100644
--- a/app/(protected)/inferred-deals/page.tsx
+++ b/app/(protected)/inferred-deals/page.tsx
@@ -1,7 +1,7 @@
import React, { Suspense } from "react";
import DealCardSkeleton from "@/components/skeletons/DealCardSkeleton";
import { Metadata } from "next";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import DealCard from "@/components/DealCard";
import getCurrentUserRole from "@/lib/data/current-user-role";
import GetDeals from "@/app/actions/get-deal";
diff --git a/app/(protected)/manual-deals/[uid]/edit/page.tsx b/app/(protected)/manual-deals/[uid]/edit/page.tsx
index fd9d076..6183785 100644
--- a/app/(protected)/manual-deals/[uid]/edit/page.tsx
+++ b/app/(protected)/manual-deals/[uid]/edit/page.tsx
@@ -2,7 +2,7 @@ import EditDealForm from "@/components/forms/edit-deal-form";
import PreviousPageButton from "@/components/PreviousPageButton";
import { Card, CardContent } from "@/components/ui/card";
import { fetchSpecificManualDeal } from "@/lib/firebase/db";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Metadata } from "next";
import React from "react";
diff --git a/app/(protected)/manual-deals/[uid]/page.tsx b/app/(protected)/manual-deals/[uid]/page.tsx
index bdf207b..3179185 100644
--- a/app/(protected)/manual-deals/[uid]/page.tsx
+++ b/app/(protected)/manual-deals/[uid]/page.tsx
@@ -28,7 +28,7 @@ import PreviousPageButton from "@/components/PreviousPageButton";
import { DealDetailItem } from "@/components/DealDetailItem";
import AIReasoning from "@/components/AiReasoning";
import SimItem from "@/components/SimItem";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import SimUploadDialog from "@/components/Dialogs/sim-upload-dialog";
import FetchDealSim from "@/components/FetchDealSim";
import SimItemSkeleton from "@/components/skeletons/SimItemSkeleton";
diff --git a/app/(protected)/manual-deals/page.tsx b/app/(protected)/manual-deals/page.tsx
index 6f73aef..5771bbb 100644
--- a/app/(protected)/manual-deals/page.tsx
+++ b/app/(protected)/manual-deals/page.tsx
@@ -2,7 +2,7 @@ import DealCardSkeleton from "@/components/skeletons/DealCardSkeleton";
import { Metadata } from "next";
import React, { Suspense } from "react";
import FetchingManualDeals from "./FetchingManualDeals";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import DealCard from "@/components/DealCard";
import GetDeals from "@/app/actions/get-deal";
import Pagination from "@/components/pagination";
diff --git a/app/(protected)/profile/[uid]/page.tsx b/app/(protected)/profile/[uid]/page.tsx
index 1f5a570..11fb71c 100644
--- a/app/(protected)/profile/[uid]/page.tsx
+++ b/app/(protected)/profile/[uid]/page.tsx
@@ -1,5 +1,5 @@
import ProfileForm from "@/components/forms/profile-form";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import React from "react";
diff --git a/app/(protected)/questionnaires/page.tsx b/app/(protected)/questionnaires/page.tsx
index 2f9b55b..3da5e89 100644
--- a/app/(protected)/questionnaires/page.tsx
+++ b/app/(protected)/questionnaires/page.tsx
@@ -3,7 +3,7 @@ import BaseLineUploadForm from "@/components/forms/baseline-upload-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { fetchQuestionnaires } from "@/lib/firebase/db";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Metadata } from "next";
import React from "react";
diff --git a/app/(protected)/raw-deals/[uid]/edit/page.tsx b/app/(protected)/raw-deals/[uid]/edit/page.tsx
index 788656f..30cd57f 100644
--- a/app/(protected)/raw-deals/[uid]/edit/page.tsx
+++ b/app/(protected)/raw-deals/[uid]/edit/page.tsx
@@ -2,7 +2,7 @@ import EditDealForm from "@/components/forms/edit-deal-form";
import PreviousPageButton from "@/components/PreviousPageButton";
import { Card, CardContent } from "@/components/ui/card";
import { fetchSpecificManualDeal } from "@/lib/firebase/db";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Metadata } from "next";
import React from "react";
diff --git a/app/(protected)/raw-deals/[uid]/page.tsx b/app/(protected)/raw-deals/[uid]/page.tsx
index 8258df3..2d99628 100644
--- a/app/(protected)/raw-deals/[uid]/page.tsx
+++ b/app/(protected)/raw-deals/[uid]/page.tsx
@@ -124,8 +124,8 @@ export default async function ManualDealSpecificPage(props: {
companyLocation,
industry,
ebitdaMargin,
- isReviewed,
- isPublished,
+ reviewed,
+ published,
tags,
status,
askingPrice,
@@ -179,23 +179,23 @@ export default async function ManualDealSpecificPage(props: {
Reviewed
-
- {isReviewed ? "Yes" : "No"}
-
+
+ {reviewed ? "Yes" : "No"}
+
Published
-
- {isPublished ? "Yes" : "No"}
-
+
+ {published ? "Yes" : "No"}
+
@@ -216,7 +216,7 @@ export default async function ManualDealSpecificPage(props: {
{tags && tags.length > 0 ? (
- {tags.map((tag) => (
+ {tags.map((tag: string) => (
{tag}
))}
@@ -232,8 +232,8 @@ export default async function ManualDealSpecificPage(props: {
diff --git a/app/(protected)/raw-deals/[uid]/screen/page.tsx b/app/(protected)/raw-deals/[uid]/screen/page.tsx
index 1035431..5ea67fd 100644
--- a/app/(protected)/raw-deals/[uid]/screen/page.tsx
+++ b/app/(protected)/raw-deals/[uid]/screen/page.tsx
@@ -1,6 +1,6 @@
import PreviousPageButton from "@/components/PreviousPageButton";
import ScreenDealComponent from "@/components/ScreenDealComponent";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Metadata } from "next";
import React, { Suspense } from "react";
diff --git a/app/actions/add-baseline.ts b/app/actions/add-baseline.ts
index 105d63e..b65932d 100644
--- a/app/actions/add-baseline.ts
+++ b/app/actions/add-baseline.ts
@@ -1,6 +1,6 @@
"use server";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { put } from "@vercel/blob";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/add-deal.ts b/app/actions/add-deal.ts
index 28bc288..519ef97 100644
--- a/app/actions/add-deal.ts
+++ b/app/actions/add-deal.ts
@@ -4,7 +4,7 @@ import {
NewDealFormSchema,
NewDealFormSchemaType,
} from "@/components/forms/new-deal-form";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { withAuthServerAction } from "@/lib/withAuth";
import { DealType, User } from "@prisma/client";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
diff --git a/app/actions/add-poc.ts b/app/actions/add-poc.ts
index c60ad89..7b70f50 100644
--- a/app/actions/add-poc.ts
+++ b/app/actions/add-poc.ts
@@ -4,7 +4,7 @@ import {
addPocFormSchema,
AddPocFormValues,
} from "@/components/forms/add-poc-form";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { User } from "@prisma/client";
import { withAuthServerAction } from "@/lib/withAuth";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/block-account.ts b/app/actions/block-account.ts
index 716e2f5..4d837ea 100644
--- a/app/actions/block-account.ts
+++ b/app/actions/block-account.ts
@@ -2,7 +2,7 @@
import { auth } from "@/auth";
import getCurrentUserRole from "@/lib/data/current-user-role";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { revalidatePath } from "next/cache";
const blockAccount = async (userId: string) => {
diff --git a/app/actions/bulk-delete-deals.ts b/app/actions/bulk-delete-deals.ts
index 760ff58..b3f745b 100644
--- a/app/actions/bulk-delete-deals.ts
+++ b/app/actions/bulk-delete-deals.ts
@@ -1,7 +1,7 @@
"use server";
import { auth } from "@/auth";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { revalidatePath } from "next/cache";
/**
diff --git a/app/actions/bulk-upload-deal.ts b/app/actions/bulk-upload-deal.ts
index 4454234..0e7815d 100644
--- a/app/actions/bulk-upload-deal.ts
+++ b/app/actions/bulk-upload-deal.ts
@@ -3,7 +3,7 @@
import { db } from "@/lib/firebase/init";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { TransformedDeal } from "../types";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { DealType, User } from "@prisma/client";
import { withAuthServerAction } from "@/lib/withAuth";
import { auth } from "@/auth";
diff --git a/app/actions/delete-ai-screening.ts b/app/actions/delete-ai-screening.ts
index 947e249..6772191 100644
--- a/app/actions/delete-ai-screening.ts
+++ b/app/actions/delete-ai-screening.ts
@@ -1,5 +1,5 @@
"use server";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { DealType } from "@prisma/client";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/delete-baseline.ts b/app/actions/delete-baseline.ts
index 83cf530..442f257 100644
--- a/app/actions/delete-baseline.ts
+++ b/app/actions/delete-baseline.ts
@@ -1,7 +1,7 @@
"use server";
import { db } from "@/lib/firebase/init";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { del } from "@vercel/blob";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/delete-deal.ts b/app/actions/delete-deal.ts
index 48111fc..ab8873f 100644
--- a/app/actions/delete-deal.ts
+++ b/app/actions/delete-deal.ts
@@ -5,7 +5,7 @@ import {
NewDealFormSchemaType,
} from "@/components/forms/new-deal-form";
import { db } from "@/lib/firebase/init";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { DealType } from "@prisma/client";
import {
addDoc,
diff --git a/app/actions/delete-poc.ts b/app/actions/delete-poc.ts
index f84a54c..baacc93 100644
--- a/app/actions/delete-poc.ts
+++ b/app/actions/delete-poc.ts
@@ -1,7 +1,7 @@
"use server";
import { withAuthServerAction } from "@/lib/withAuth";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { User } from "@prisma/client";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/delete-sim.ts b/app/actions/delete-sim.ts
index dd24204..dfb529a 100644
--- a/app/actions/delete-sim.ts
+++ b/app/actions/delete-sim.ts
@@ -1,6 +1,6 @@
"use server";
import { auth } from "@/auth";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { DealType } from "@prisma/client";
import { del } from "@vercel/blob";
diff --git a/app/actions/edit-deal.ts b/app/actions/edit-deal.ts
index 0a77a54..c9435c3 100644
--- a/app/actions/edit-deal.ts
+++ b/app/actions/edit-deal.ts
@@ -2,7 +2,7 @@
import { EditDealFormSchemaType } from "@/components/forms/edit-deal-form";
import { db } from "@/lib/firebase/init";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { DealType } from "@prisma/client";
import { doc, getDoc, serverTimestamp, updateDoc } from "firebase/firestore";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/edit-screen-deal-result.ts b/app/actions/edit-screen-deal-result.ts
index 4061fdf..8cc30fe 100644
--- a/app/actions/edit-screen-deal-result.ts
+++ b/app/actions/edit-screen-deal-result.ts
@@ -1,7 +1,7 @@
"use server";
import { auth } from "@/auth";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { screenDealSchemaType } from "@/lib/schemas";
import { DealType } from "@prisma/client";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/get-deal.ts b/app/actions/get-deal.ts
index 7488c8c..49ac4d4 100644
--- a/app/actions/get-deal.ts
+++ b/app/actions/get-deal.ts
@@ -129,8 +129,8 @@ export const GetAllDeals = async ({
? { ebitdaMargin: { gte: ebitdaMarginValue } }
: {}),
...(showSeen ? { seen: { equals: showSeen } } : {}),
- ...(showReviewed ? { isReviewed: { equals: showReviewed } } : {}),
- ...(showPublished ? { isPublished: { equals: showPublished } } : {}),
+ ...(showReviewed ? { reviewed: { equals: showReviewed } } : {}),
+ ...(showPublished ? { published: { equals: showPublished } } : {}),
...(status ? { status: { equals: status } } : {}),
...(tags && tags.length > 0 ? { tags: { hasSome: tags } } : {}),
};
diff --git a/app/actions/revert-deal-version.ts b/app/actions/revert-deal-version.ts
new file mode 100644
index 0000000..0787ead
--- /dev/null
+++ b/app/actions/revert-deal-version.ts
@@ -0,0 +1,68 @@
+"use server";
+
+import prismaDB from "@/lib/prisma.server";
+import { revalidatePath } from "next/cache";
+import { DealType } from "@prisma/client";
+
+/**
+ * Revert a Deal record to the snapshot stored in `dealHistory`.
+ * @param versionId – id of the dealHistory row we want to restore
+ * @param dealId – id of the Deal to update (redundant but explicit)
+ */
+const revertDealVersion = async (versionId: string, dealId: string) => {
+ try {
+ // 1. fetch snapshot to restore
+ const version = await prismaDB.dealHistory.findUnique({
+ where: { id: versionId },
+ });
+
+ if (!version) {
+ return { type: "error" as const, message: "Version not found" };
+ }
+
+ // 2. Extract snapshot & drop immutable columns
+ const { snapshot } = version as { snapshot: Record };
+ if (!snapshot) {
+ return {
+ type: "error" as const,
+ message: "Snapshot missing for the selected version.",
+ };
+ }
+
+ // Remove prisma-generated/immutable fields
+ const {
+ id: _discardId,
+ createdAt: _discardCreated,
+ updatedAt: _discardUpdated,
+ ...dataToRestore
+ } = snapshot as Record;
+
+ // 3. Perform update (this will itself write a new version via middleware)
+ const updated = await prismaDB.deal.update({
+ where: { id: dealId },
+ data: dataToRestore,
+ });
+
+ // 4. Revalidate correct page route so UI updates
+ switch (updated.dealType as DealType) {
+ case "MANUAL":
+ revalidatePath(`/manual-deals/${dealId}`);
+ break;
+ case "SCRAPED":
+ revalidatePath(`/raw-deals/${dealId}`);
+ break;
+ case "AI_INFERRED":
+ revalidatePath(`/inferred-deals/${dealId}`);
+ break;
+ default:
+ break;
+ }
+
+ return { type: "success" as const };
+ } catch (err) {
+ console.error("Error reverting deal to version", err);
+ return { type: "error" as const, message: "Failed to revert deal." };
+ }
+};
+
+export default revertDealVersion;
diff --git a/app/actions/save-infer-deal.ts b/app/actions/save-infer-deal.ts
index a3ae524..971fe44 100644
--- a/app/actions/save-infer-deal.ts
+++ b/app/actions/save-infer-deal.ts
@@ -2,7 +2,7 @@
import { auth } from "@/auth";
import { InferDealSchema } from "@/components/schemas/infer-deal-schema";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
export default async function SaveInferredDeal({
generation,
diff --git a/app/actions/save-screen-deal.ts b/app/actions/save-screen-deal.ts
index 2a0a1d5..f5d3c3c 100644
--- a/app/actions/save-screen-deal.ts
+++ b/app/actions/save-screen-deal.ts
@@ -5,7 +5,7 @@ import {
NewDealFormSchema,
NewDealFormSchemaType,
} from "@/components/forms/new-deal-form";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { screenDealSchemaType } from "@/lib/schemas";
import { DealType } from "@prisma/client";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
diff --git a/app/actions/screening-save-result.ts b/app/actions/screening-save-result.ts
index 7af27ea..f6806e6 100644
--- a/app/actions/screening-save-result.ts
+++ b/app/actions/screening-save-result.ts
@@ -2,7 +2,7 @@
import { auth } from "@/auth";
import { openai } from "@/lib/ai/available-models";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { Sentiment } from "@prisma/client";
import { generateObject } from "ai";
import { revalidatePath } from "next/cache";
diff --git a/app/actions/unblock-account.ts b/app/actions/unblock-account.ts
index 2b4c2ae..57b92e7 100644
--- a/app/actions/unblock-account.ts
+++ b/app/actions/unblock-account.ts
@@ -2,7 +2,7 @@
import { auth } from "@/auth";
import getCurrentUserRole from "@/lib/data/current-user-role";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { revalidatePath } from "next/cache";
const unblockAccount = async (userId: string) => {
diff --git a/app/actions/update-deal-specifications.ts b/app/actions/update-deal-specifications.ts
index 81dd619..33e1295 100644
--- a/app/actions/update-deal-specifications.ts
+++ b/app/actions/update-deal-specifications.ts
@@ -47,8 +47,8 @@ export async function updateDealSpecificationsAction(
data: {
seen: validatedFields.data.seen,
status: validatedFields.data.status,
- isReviewed: validatedFields.data.isReviewed,
- isPublished: validatedFields.data.isPublished,
+ reviewed: validatedFields.data.isReviewed,
+ published: validatedFields.data.isPublished,
},
});
diff --git a/app/actions/upload-bitrix.ts b/app/actions/upload-bitrix.ts
index fffcce9..010c880 100644
--- a/app/actions/upload-bitrix.ts
+++ b/app/actions/upload-bitrix.ts
@@ -4,7 +4,7 @@
import axios from "axios";
import { Deal, User } from "@prisma/client";
import { auth } from "@/auth";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { revalidatePath } from "next/cache";
import { withAuthServerAction } from "@/lib/withAuth";
diff --git a/app/actions/upload-cim.ts b/app/actions/upload-cim.ts
index 3ee364e..caf9325 100644
--- a/app/actions/upload-cim.ts
+++ b/app/actions/upload-cim.ts
@@ -4,7 +4,7 @@ import { put } from "@vercel/blob";
import { DealType, PrismaClient } from "@prisma/client";
import { revalidatePath } from "next/cache";
import { cimFormSchema } from "@/lib/schemas";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
export default async function UploadCim(
data: FormData,
diff --git a/app/api/deals/[id]/deal-documents/route.ts b/app/api/deals/[id]/deal-documents/route.ts
new file mode 100644
index 0000000..fc8135c
--- /dev/null
+++ b/app/api/deals/[id]/deal-documents/route.ts
@@ -0,0 +1,25 @@
+import { NextResponse } from "next/server";
+import prisma from "@/lib/prisma.server";
+
+// GET /api/deals/:id/deal-documents
+export async function GET(
+ _req: Request,
+ context: { params: { id: string } },
+) {
+ // Next.js 15 dynamic params are async proxies – await before access to silence warning
+ const id = await context.params.id;
+
+ try {
+ const data = await prisma.dealDocument.findMany({
+ where: { dealId: id },
+ orderBy: { createdAt: "desc" },
+ });
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error("Deal documents fetch error", error);
+ return NextResponse.json(
+ { error: "Failed to fetch deal documents" },
+ { status: 500 },
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/deals/[id]/pocs/route.ts b/app/api/deals/[id]/pocs/route.ts
new file mode 100644
index 0000000..5565bd6
--- /dev/null
+++ b/app/api/deals/[id]/pocs/route.ts
@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+import prisma from "@/lib/prisma.server";
+
+// GET /api/deals/:id/pocs
+export async function GET(
+ _req: Request,
+ context: { params: { id: string } },
+) {
+ const id = await context.params.id;
+
+ try {
+ const data = await prisma.pOC.findMany({
+ where: { dealId: id },
+ });
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error("POCs fetch error", error);
+ return NextResponse.json({ error: "Failed to fetch POCs" }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/deals/[id]/route.ts b/app/api/deals/[id]/route.ts
new file mode 100644
index 0000000..7343f94
--- /dev/null
+++ b/app/api/deals/[id]/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+import prisma from "@/lib/prisma.server";
+
+export async function PATCH(request: Request, context: { params: { id: string } }) {
+ const id = await context.params.id;
+ const data = await request.json();
+ // data can be { reviewed: boolean } or { seen: boolean } etc.
+ const updated = await prisma.deal.update({
+ where: { id },
+ data,
+ select: { id: true },
+ });
+ return NextResponse.json(updated);
+}
diff --git a/app/api/deals/[id]/versions/route.ts b/app/api/deals/[id]/versions/route.ts
new file mode 100644
index 0000000..69ec9f2
--- /dev/null
+++ b/app/api/deals/[id]/versions/route.ts
@@ -0,0 +1,41 @@
+// app/api/deals/[id]/versions/route.ts
+import { NextResponse } from "next/server";
+import prisma from "@/lib/prisma.server";
+
+export async function GET(
+ request: Request,
+ context: { params: { id: string } },
+) {
+ // Next.js 15 dynamic params are async proxies – await before access to silence warning
+ const id = await context.params.id;
+
+ const rawVersions = await prisma.dealHistory.findMany({
+ where: { dealId: id },
+ orderBy: { createdAt: "desc" },
+ });
+
+ // ── Deduplicate snapshots that are byte-for-byte identical ──
+ const versions: typeof rawVersions = [];
+ const stripTimestamps = (obj: Record) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { createdAt: _c, updatedAt: _u, ...rest } = obj;
+ return rest;
+ };
+
+ const seenSeconds = new Set();
+ rawVersions.forEach((v) => {
+ const tsSec = Math.floor(new Date(v.createdAt).getTime() / 1000);
+ if (seenSeconds.has(tsSec)) return; // skip other writes in same second window
+
+ if (
+ versions.length === 0 ||
+ JSON.stringify(stripTimestamps(versions[versions.length - 1].snapshot as Record)) !==
+ JSON.stringify(stripTimestamps(v.snapshot as Record))
+ ) {
+ versions.push(v);
+ seenSeconds.add(tsSec);
+ }
+ });
+
+ return NextResponse.json(versions);
+}
diff --git a/app/api/deals/pending/route.ts b/app/api/deals/pending/route.ts
index a68b81e..cf5e648 100644
--- a/app/api/deals/pending/route.ts
+++ b/app/api/deals/pending/route.ts
@@ -1,7 +1,14 @@
import { auth } from "@/auth";
-import { redisClient } from "@/lib/redis";
+import { getRedisClient } from "@/lib/redis";
import { NextResponse } from "next/server";
+interface RedisDealListing {
+ id: string;
+ title: string;
+ ebitda: number;
+ userId: string;
+}
+
export async function GET() {
const userSession = await auth();
@@ -10,10 +17,8 @@ export async function GET() {
}
try {
- // ensure connected
- if (!redisClient.isOpen) {
- await redisClient.connect();
- }
+ const redisClient = await getRedisClient();
+ if (!redisClient.isOpen) await redisClient.connect();
} catch (error) {
console.error("Error connecting to Redis:", error);
return NextResponse.json(
@@ -23,25 +28,26 @@ export async function GET() {
}
try {
- const raw = await redisClient.lRange("dealListings", 0, -1);
+ const raw = await (await getRedisClient()).lRange("dealListings", 0, -1);
- // parse and filter by this user
- const all = raw
- .map((s) => {
+ // parse and filter by this user with typing
+ const all: RedisDealListing[] = raw
+ .map((s: string) => {
try {
- return JSON.parse(s);
+ return JSON.parse(s) as RedisDealListing;
} catch {
return null;
}
})
- .filter((d) => d && d.userId === userSession.user.id);
-
- // map to shape: { id, productName, status: "Pending" }
- const pending = all.map(({ id, title, ebitda }) => ({
- id,
- title: title,
- ebitda: ebitda,
- status: "Pending",
+ .filter((d: RedisDealListing | null): d is RedisDealListing => !!d)
+ .filter((d: RedisDealListing) => d.userId === userSession.user.id);
+
+ // map to response shape
+ const pending = all.map((d: RedisDealListing) => ({
+ id: d.id,
+ title: d.title,
+ ebitda: d.ebitda,
+ status: "Pending" as const,
}));
console.log("pending", pending);
diff --git a/app/api/screen-all/route.ts b/app/api/screen-all/route.ts
index a2eb6f0..7062f09 100644
--- a/app/api/screen-all/route.ts
+++ b/app/api/screen-all/route.ts
@@ -1,5 +1,5 @@
import { auth } from "@/auth";
-import { redisClient } from "@/lib/redis";
+import { getRedisClient } from "@/lib/redis";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
@@ -22,6 +22,7 @@ export async function POST(request: Request) {
try {
console.log("connecting to redis");
+ const redisClient = await getRedisClient();
if (!redisClient.isOpen) {
await redisClient.connect();
}
@@ -37,6 +38,7 @@ export async function POST(request: Request) {
try {
console.log("pushing all the deals to bitrix");
+ const redisClient = await getRedisClient();
dealListings.forEach(async (dealListing: any) => {
const dealListingWithUserId = {
...dealListing,
@@ -50,7 +52,7 @@ export async function POST(request: Request) {
});
// publish the message that a new screening call request was made
- await redisClient.publish(
+ await (await getRedisClient()).publish(
"new_screen_call",
JSON.stringify({
userId: userSession.user.id,
diff --git a/app/api/upload-deal-document/route.ts b/app/api/upload-deal-document/route.ts
index 0c6cb98..8d1b98a 100644
--- a/app/api/upload-deal-document/route.ts
+++ b/app/api/upload-deal-document/route.ts
@@ -1,6 +1,6 @@
import { auth } from "@/auth";
import { dealDocumentFormSchema } from "@/lib/schemas";
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
import { put } from "@vercel/blob";
import { NextRequest, NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
diff --git a/auth.config.ts b/auth.config.ts
index cf0ddd2..50ebcc2 100644
--- a/auth.config.ts
+++ b/auth.config.ts
@@ -1,7 +1,7 @@
import GitHub from "next-auth/providers/github";
import type { DefaultSession, NextAuthConfig } from "next-auth";
import Google from "next-auth/providers/google";
-import prismaDB from "./lib/prisma";
+import prismaDB from "./lib/prisma.server";
import { UserRole } from "@prisma/client";
// Notice this is only an object, not a full Auth.js instance
diff --git a/auth.ts b/auth.ts
index fa3b30b..9c8ea6f 100644
--- a/auth.ts
+++ b/auth.ts
@@ -1,6 +1,6 @@
import NextAuth, { DefaultSession } from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
-import prismaDB from "./lib/prisma";
+import prismaDB from "./lib/prisma.server";
import { User, UserRole } from "@prisma/client";
import authConfig from "./auth.config";
import { getCurrentUserByEmail } from "./lib/data/current-user";
@@ -12,6 +12,7 @@ const adminEmails = [
"diligence@darkalphacapital.com",
"da@darkalphacapital.com",
"daigbe@gmail.com",
+ "kshah77@asu.edu"
];
declare module "next-auth" {
diff --git a/components/DealCard.tsx b/components/DealCard.tsx
index bb00171..801d1c8 100644
--- a/components/DealCard.tsx
+++ b/components/DealCard.tsx
@@ -165,27 +165,27 @@ const DealCard = ({
) : (
)
}
label="Published"
- value={deal.isPublished ? "Yes" : "No"}
+ value={deal.published ? "Yes" : "No"}
className="whitespace-nowrap"
/>
) : (
)
}
label="Reviewed"
- value={deal.isReviewed ? "Yes" : "No"}
+ value={deal.reviewed ? "Yes" : "No"}
className="whitespace-nowrap"
/>
diff --git a/components/DealHistoryView.tsx b/components/DealHistoryView.tsx
new file mode 100644
index 0000000..a170781
--- /dev/null
+++ b/components/DealHistoryView.tsx
@@ -0,0 +1,113 @@
+// components/DealHistoryView.tsx
+"use client";
+
+import React, { useEffect, useState, useTransition } from "react";
+import revertDealVersion from "@/app/actions/revert-deal-version";
+import { useRouter } from "next/navigation";
+import { useToast } from "@/hooks/use-toast";
+
+interface Version {
+ id: string;
+ snapshot: Record;
+ createdAt: string;
+}
+
+function diffSnapshots(a: Record, b: Record | null) {
+ if (!b) return [] as Array<{ field: string; oldVal: any; newVal: any }>;
+ const fields = new Set([...Object.keys(a), ...Object.keys(b)]);
+ const changes: Array<{ field: string; oldVal: any; newVal: any }> = [];
+ fields.forEach((f) => {
+ const prevVal = b[f];
+ const nextVal = a[f];
+ if (JSON.stringify(prevVal) !== JSON.stringify(nextVal)) {
+ changes.push({ field: f, oldVal: prevVal, newVal: nextVal });
+ }
+ });
+ return changes;
+}
+
+export default function DealHistoryView({ dealId }: { dealId: string }) {
+ const [versions, setVersions] = useState([]);
+ const [openId, setOpenId] = useState(null);
+ const [isPending, startTransition] = useTransition();
+ const router = useRouter();
+ const { toast } = useToast();
+
+ useEffect(() => {
+ fetch(`/api/deals/${dealId}/versions`)
+ .then((res) => res.json())
+ .then(setVersions);
+ }, [dealId]);
+
+ const getOlderSnapshot = (idx: number) =>
+ idx + 1 < versions.length ? versions[idx + 1].snapshot : null;
+
+ return (
+
+ {versions.map((v, idx) => {
+ const changes = diffSnapshots(v.snapshot, getOlderSnapshot(idx));
+ return (
+
+
+ {openId === v.id && (
+
+ {changes.length > 0 ? (
+
+ {changes.map((c) => (
+ -
+ {c.field}: {JSON.stringify(c.oldVal)} → {JSON.stringify(c.newVal)}
+
+ ))}
+
+ ) : (
+
No visible difference
+ )}
+
+
+
+ )}
+
+ );
+ })}
+ {versions.length === 0 && (
+
No version history.
+ )}
+
+ );
+}
diff --git a/components/DealListItem.tsx b/components/DealListItem.tsx
index a97571f..9adf9f5 100644
--- a/components/DealListItem.tsx
+++ b/components/DealListItem.tsx
@@ -35,8 +35,8 @@ export default function DealListItem({ deal, selected, onToggle }: Props) {
Email: {deal.email}
Status: {deal.status}
- Reviewed: {deal.isReviewed ? "Yes" : "No"}
- Published: {deal.isPublished ? "Yes" : "No"}
+ Reviewed: {deal.reviewed ? "Yes" : "No"}
+ Published: {deal.published ? "Yes" : "No"}
LinkedIn: {deal.linkedinUrl}
Phone: {deal.workPhone}
diff --git a/components/DealTabs.tsx b/components/DealTabs.tsx
new file mode 100644
index 0000000..da4a0af
--- /dev/null
+++ b/components/DealTabs.tsx
@@ -0,0 +1,284 @@
+"use client";
+// components/DealTabs.tsx
+
+
+import React, { useState, Suspense } from 'react';
+import type { Deal } from '@prisma/client';
+import Link from 'next/link';
+import {
+ Hash,
+ Tag,
+ CheckCircle,
+ Briefcase,
+ MapPin,
+ Phone,
+ Mail,
+ Building,
+ DollarSign,
+ Percent,
+ CreditCard,
+ Plus,
+} from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
+import { DealDetailItem } from '@/components/DealDetailItem';
+import { ScrollArea } from '@/components/ui/scroll-area';
+
+import DealHistoryView from '@/components/DealHistoryView';
+import dynamic from 'next/dynamic';
+import AIReasoningSkeleton from '@/components/skeletons/AIReasoningSkeleton';
+import SimUploadDialog from '@/components/Dialogs/sim-upload-dialog';
+import SimItemSkeleton from '@/components/skeletons/SimItemSkeleton';
+import DealDocumentUploadDialog from '@/components/Dialogs/deal-document-upload-dialog';
+
+const FetchDealAIScreenings = dynamic(() => import('@/components/FetchDealAIScreenings'), { ssr: true });
+const FetchDealSim = dynamic(() => import('@/components/FetchDealSim'), { ssr: true });
+const FetchDealDocuments = dynamic(() => import('@/components/fetch-deal-documents'), { ssr: true });
+const FetchDealPOC = dynamic(() => import('@/components/FetchDealPOC'), { ssr: true });
+
+interface Props {
+ deal: Deal;
+}
+
+export default function DealTabs({ deal }: Props) {
+ const [active, setActive] = useState<
+ 'basic' | 'financial' | 'ai' | 'docs' | 'sims' | 'pocs' | 'history'
+ >('basic');
+
+ return (
+
+ {/* ── Tab Headers ── */}
+
+ {[
+ { id: 'basic', label: 'Basic Information' },
+ { id: 'financial', label: 'Financial Details' },
+ { id: 'ai', label: 'AI Reasoning' },
+ { id: 'docs', label: 'Deal Documents' },
+ { id: 'sims', label: 'SIMs' },
+ { id: 'pocs', label: 'Point of Contacts' },
+ { id: 'history', label: 'Version History' },
+ ].map((tab) => (
+
+ ))}
+
+
+ {/* ── Tab Panels ── */}
+
+ {/* Basic Information */}
+ {active === 'basic' && (
+
+
+ Basic Information
+
+
+ } label="Deal ID" value={deal.id} />
+ }
+ label="Bitrix ID"
+ value={deal.bitrixId ?? '—'}
+ />
+ }
+ label="Status"
+ value={deal.status}
+ />
+ }
+ label="Tags"
+ value={deal.tags.length ? deal.tags.join(', ') : '—'}
+ />
+ }
+ label="Reviewed"
+ value={deal.reviewed ? 'Yes' : 'No'}
+ />
+ }
+ label="Seen"
+ value={deal.seen ? 'Yes' : 'No'}
+ />
+ }
+ label="Published"
+ value={deal.published ? 'Yes' : 'No'}
+ />
+ }
+ label="Brokerage"
+ value={deal.brokerage}
+ />
+ }
+ label="Location"
+ value={deal.companyLocation ?? '—'}
+ />
+ }
+ label="Industry"
+ value={deal.industry}
+ />
+ }
+ label="Phone"
+ value={deal.workPhone ?? '—'}
+ />
+ }
+ label="Email"
+ value={deal.email ?? '—'}
+ />
+
+
+ )}
+
+ {/* Financial Details */}
+ {active === 'financial' && (
+
+
+ Financial Details
+
+
+ }
+ label="Revenue"
+ value={`$${deal.revenue.toFixed(2)}`}
+ />
+ }
+ label="EBITDA"
+ value={`$${deal.ebitda.toFixed(2)}`}
+ />
+ }
+ label="EBITDA Margin"
+ value={`${((deal.ebitda / deal.revenue) * 100).toFixed(2)}%`}
+ />
+ }
+ label="Asking Price"
+ value={
+ deal.askingPrice != null
+ ? `$${deal.askingPrice.toFixed(2)}`
+ : '—'
+ }
+ />
+ }
+ label="Gross Revenue"
+ value={
+ deal.grossRevenue != null
+ ? `$${deal.grossRevenue.toFixed(2)}`
+ : '—'
+ }
+ />
+
+
+ )}
+
+ {/* AI Reasoning */}
+ {active === 'ai' && (
+
+
+ AI Reasoning
+
+
+
+
+ }>
+
+
+
+
+
+ )}
+
+ {/* Deal Documents */}
+ {active === 'docs' && (
+
+
+ Deal Documents
+
+
+
+
+
+
+
+
+ )}
+
+ {/* SIMs */}
+ {active === 'sims' && (
+
+
+ SIMs
+
+
+
+
+ }>
+
+
+
+
+
+ )}
+
+ {/* Point of Contacts */}
+ {active === 'pocs' && (
+
+
+ Point of Contacts
+
+
+
+
+
+
+
+ )}
+
+ {/* Version History */}
+ {active === 'history' && (
+
+
+ Version History
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/Dialogs/deal-document-upload-dialog.tsx b/components/Dialogs/deal-document-upload-dialog.tsx
index 4b05a7a..af2a306 100644
--- a/components/Dialogs/deal-document-upload-dialog.tsx
+++ b/components/Dialogs/deal-document-upload-dialog.tsx
@@ -40,11 +40,13 @@ import { toast } from "sonner";
interface DealDocumentUploadDialogProps {
dealId: string;
dealType: DealType;
+ onUploaded?: () => void;
}
const DealDocumentUploadDialog: React.FC = ({
dealId,
dealType,
+ onUploaded,
}) => {
const [isPending, startTransition] = useTransition();
const [isOpen, setIsOpen] = React.useState(false);
@@ -79,6 +81,7 @@ const DealDocumentUploadDialog: React.FC = ({
toast.success("Document uploaded successfully");
form.reset();
setIsOpen(false);
+ onUploaded?.();
} else {
throw new Error(result.statusText);
}
diff --git a/components/FetchDealAIScreenings.tsx b/components/FetchDealAIScreenings.tsx
index 8f254ae..57d7f13 100644
--- a/components/FetchDealAIScreenings.tsx
+++ b/components/FetchDealAIScreenings.tsx
@@ -1,54 +1,76 @@
-import prismaDB from "@/lib/prisma";
-import React from "react";
+"use client";
+
+import React, { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
import { AlertTriangle } from "lucide-react";
+import { DealType, Sentiment } from "@prisma/client";
+
+const AIReasoning = dynamic(() => import("./AiReasoning"), { ssr: false });
import { Button } from "./ui/button";
-import { DealType } from "@prisma/client";
-import AIReasoning from "./AiReasoning";
-const FetchDealAIScreenings = async ({
- dealId,
- dealType,
-}: {
+interface Props {
dealId: string;
dealType: DealType;
-}) => {
- const screenings = await prismaDB.aiScreening.findMany({
- where: {
- dealId: dealId,
- },
- });
+}
+
+interface Screening {
+ id: string;
+ title: string;
+ explanation: string;
+ sentiment: Sentiment;
+}
+
+export default function FetchDealAIScreenings({ dealId, dealType }: Props) {
+ const [screenings, setScreenings] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const res = await fetch(`/api/deals/${dealId}/ai-screenings`);
+ if (res.ok) {
+ const data = (await res.json()) as Screening[];
+ setScreenings(data);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [dealId]);
+
+ if (loading) {
+ return Loading...
;
+ }
return (
-
- {screenings.length > 0 ? (
- screenings.map((e, index) => (
-
- ))
- ) : (
-
-
-
No AI Reasoning Available
-
- AI analysis for this deal has not been generated yet. Check back
- later or request an analysis.
-
-
-
- )}
-
+ {screenings.length > 0 ? (
+ screenings.map((e) => (
+
+ ))
+ ) : (
+
+
+
No AI Reasoning Available
+
+ AI analysis for this deal has not been generated yet. Check back
+ later or request an analysis.
+
+
+
+ )}
);
-};
-
-export default FetchDealAIScreenings;
+}
\ No newline at end of file
diff --git a/components/FetchDealPOC.tsx b/components/FetchDealPOC.tsx
index f999f4f..3912ce6 100644
--- a/components/FetchDealPOC.tsx
+++ b/components/FetchDealPOC.tsx
@@ -1,39 +1,58 @@
-import { DealType } from "@prisma/client";
-import React from "react";
-import { getDealPOC } from "@/lib/queries";
-import { Plus, Trash } from "lucide-react";
-import { Button } from "./ui/button";
+"use client";
+
+import React, { useEffect, useState } from "react";
+import { AlertTriangle } from "lucide-react";
import AddPocDialog from "./Dialogs/add-poc-dialog";
import DeletePocButton from "./Buttons/delete-poc-button";
+import type { DealType } from "@prisma/client";
+
+interface POC {
+ id: string;
+ name: string;
+ email: string;
+ workPhone?: string | null;
+}
+
+const FetchDealPOC = ({ dealId, dealType }: { dealId: string; dealType: DealType }) => {
+ const [pocs, setPocs] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const res = await fetch(`/api/deals/${dealId}/pocs`);
+ if (res.ok) {
+ const data = (await res.json()) as POC[];
+ setPocs(data);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [dealId]);
+
+ if (loading) {
+ return Loading...
;
+ }
-const FetchDealPOC = async ({
- dealId,
- dealType,
-}: {
- dealId: string;
- dealType: DealType;
-}) => {
- const pocs = await getDealPOC(dealId);
return (
{pocs.length > 0 ? (
-
+
{pocs.map((poc) => (
-
-
- {poc.name}
-
+
{poc.name}
{poc.email}
{poc.workPhone && (
-
- {poc.workPhone}
-
+
{poc.workPhone}
)}
@@ -41,12 +60,13 @@ const FetchDealPOC = async ({
))}
) : (
-
- No points of contact found for this deal.
+
+
+
No points of contact found
)}
);
};
-export default FetchDealPOC;
+export default FetchDealPOC;
\ No newline at end of file
diff --git a/components/FetchDealSim.tsx b/components/FetchDealSim.tsx
index 3b89afd..b6fef3d 100644
--- a/components/FetchDealSim.tsx
+++ b/components/FetchDealSim.tsx
@@ -1,24 +1,48 @@
-import prismaDB from "@/lib/prisma";
-import React from "react";
-import SimItem from "@/components/SimItem";
+"use client";
+
+import React, { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
import { AlertTriangle } from "lucide-react";
-import { Button } from "./ui/button";
import { DealType } from "@prisma/client";
-// this component will be used to fetch and display all sims for a particular deal
+const SimItem = dynamic(() => import("@/components/SimItem"), { ssr: false });
-const FetchDealSim = async ({
- dealId,
- dealType,
-}: {
+interface Props {
dealId: string;
dealType: DealType;
-}) => {
- const sims = await prismaDB.sIM.findMany({
- where: {
- dealId: dealId,
- },
- });
+}
+
+interface Sim {
+ id: string;
+ title: string;
+ caption: string;
+ status: string;
+ fileUrl: string;
+}
+
+export default function FetchDealSim({ dealId, dealType }: Props) {
+ const [sims, setSims] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const res = await fetch(`/api/deals/${dealId}/sims`);
+ if (res.ok) {
+ const data = (await res.json()) as Sim[];
+ setSims(data);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [dealId]);
+
+ if (loading) {
+ return Loading...
;
+ }
return (
@@ -47,6 +71,4 @@ const FetchDealSim = async ({
)}
);
-};
-
-export default FetchDealSim;
+}
\ No newline at end of file
diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx
new file mode 100644
index 0000000..ff9a83c
--- /dev/null
+++ b/components/ThemeToggle.tsx
@@ -0,0 +1,21 @@
+// components/ThemeToggle.tsx
+"use client";
+
+import React from "react";
+import { useTheme } from "next-themes";
+import { Sun, Moon } from "lucide-react";
+
+export default function ThemeToggle() {
+ const { resolvedTheme, setTheme } = useTheme();
+ const isDark = resolvedTheme === "dark";
+
+ return (
+
+ );
+}
diff --git a/components/fetch-deal-documents.tsx b/components/fetch-deal-documents.tsx
index 89715c6..e81c59b 100644
--- a/components/fetch-deal-documents.tsx
+++ b/components/fetch-deal-documents.tsx
@@ -1,35 +1,63 @@
-import prismaDB from "@/lib/prisma";
-import React from "react";
+"use client";
+
+import React, { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
import { AlertTriangle } from "lucide-react";
import { DealType } from "@prisma/client";
-import DealDocumentItem from "./DealDocumentItem";
-const FetchDealDocuments = async ({
- dealId,
- dealType,
-}: {
+const DealDocumentItem = dynamic(() => import("./DealDocumentItem"), { ssr: false });
+
+interface Props {
dealId: string;
dealType: DealType;
-}) => {
- const dealDocuments = await prismaDB.dealDocument.findMany({
- where: {
- dealId: dealId,
- },
- });
+ refreshKey?: number;
+}
+
+interface DealDocument {
+ id: string;
+ title: string;
+ description: string | null;
+ category: import("@prisma/client").DealDocumentCategory;
+ documentUrl: string;
+}
+
+export default function FetchDealDocuments({ dealId, dealType, refreshKey }: Props) {
+ const [documents, setDocuments] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const res = await fetch(`/api/deals/${dealId}/deal-documents`);
+ if (res.ok) {
+ const data = (await res.json()) as DealDocument[];
+ setDocuments(data);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [dealId, refreshKey]);
+
+ if (loading) {
+ return Loading...
;
+ }
return (
- {dealDocuments.length > 0 ? (
- dealDocuments.map((dealDocument) => (
+ {documents.length > 0 ? (
+ documents.map((doc) => (
))
) : (
@@ -44,6 +72,4 @@ const FetchDealDocuments = async ({
)}
);
-};
-
-export default FetchDealDocuments;
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 64de4c2..00a20e0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,7 +6,7 @@ services:
restart: always
container_name: the-bitrix24-db
ports:
- - 5432:5432
+ - 5433:5432
environment:
POSTGRES_PASSWORD: example
PGDATA: /data/postgres
diff --git a/lib/data/current-user.ts b/lib/data/current-user.ts
index 1cb653b..22d46da 100644
--- a/lib/data/current-user.ts
+++ b/lib/data/current-user.ts
@@ -1,6 +1,6 @@
// helper function to get the current User
-import prismaDB from "../prisma";
+import prismaDB from "../prisma.server";
/**
* get the current user by using their id
diff --git a/lib/prisma.server.ts b/lib/prisma.server.ts
new file mode 100644
index 0000000..17c826c
--- /dev/null
+++ b/lib/prisma.server.ts
@@ -0,0 +1,140 @@
+// lib/prisma.server.ts
+import { Prisma, PrismaClient } from "@prisma/client";
+
+const prismaClientSingleton = () =>
+ new PrismaClient({
+ log: ["info"],
+ });
+
+declare const globalThis: {
+ prismaGlobal?: ReturnType;
+} & typeof global;
+
+const prismaDB = globalThis.prismaGlobal ?? prismaClientSingleton();
+if (process.env.NODE_ENV !== "production") {
+ globalThis.prismaGlobal = prismaDB;
+}
+
+/* -------------------------------------------------------------------------- */
+/* Deal Version-History Middleware */
+/* -------------------------------------------------------------------------- */
+
+/**
+ * Recursively remove Prisma-managed timestamp fields so they don’t trigger
+ * false-positive diffs in history snapshots.
+ */
+function stripTimestampsDeep(val: T): T {
+ if (Array.isArray(val)) {
+ // @ts-ignore – narrow to any[] for recursion then cast back to T
+ return val.map((v) => stripTimestampsDeep(v)) as unknown as T;
+ }
+ if (val && typeof val === "object") {
+ const obj: Record = {};
+ Object.entries(val as Record).forEach(([k, v]) => {
+ if (k === "createdAt" || k === "updatedAt") return;
+ obj[k] = stripTimestampsDeep(v);
+ });
+ return obj as T;
+ }
+ return val;
+}
+
+// Models whose mutations should create a history snapshot for the parent Deal
+const TRACKED_MODELS = [
+ "Deal",
+ "AiScreening",
+ "SIM",
+ "POC",
+ "DealDocument",
+] as const satisfies ReadonlyArray;
+
+const MUTATING_ACTIONS = [
+ "create",
+ "update",
+ "upsert",
+ "delete",
+] as const satisfies ReadonlyArray;
+
+prismaDB.$use(async (params, next) => {
+ const isTracked =
+ (TRACKED_MODELS as readonly string[]).includes(params.model ?? "") &&
+ (MUTATING_ACTIONS as readonly string[]).includes(params.action);
+
+ let dealId: string | null = null;
+
+ /* ---------------------- Determine affected dealId ---------------------- */
+ if (isTracked) {
+ if (params.model === "Deal") {
+ // For Deal itself we know the id either from where or result
+ if (params.action === "create") {
+ // new deal – will get id from result later
+ } else if (params.args?.where?.id) {
+ dealId = params.args.where.id as string;
+ }
+ } else {
+ // Child models contain dealId column
+ if (params.action === "delete") {
+ // Need to fetch existing row to read its dealId before deletion
+ const existing: any = await (
+ // @ts-ignore dynamic access to model delegate
+ prismaDB[params.model!.charAt(0).toLowerCase() + params.model!.slice(1)].findUnique({
+ where: { id: params.args.where.id as string },
+ })
+ );
+ dealId = existing?.dealId ?? null;
+ } else {
+ // create/update/upsert
+ dealId =
+ (params.args?.data?.dealId as string | undefined) ??
+ (params.args?.where?.dealId as string | undefined) ??
+ null;
+ }
+ }
+ }
+
+ // Perform the actual DB mutation first
+ const result = await next(params);
+
+ if (isTracked) {
+ // For Deal.create we now have the id in result
+ if (!dealId && params.model === "Deal" && typeof result?.id === "string") {
+ dealId = result.id;
+ }
+
+ if (dealId) {
+ // Fetch a fresh, full snapshot of the deal including children
+ const snapshot = await prismaDB.deal.findUnique({
+ where: { id: dealId },
+ include: {
+ AiScreening: true,
+ POC: true,
+ DealDocument: true,
+ SIM: true,
+ },
+ });
+
+ if (snapshot) {
+ const latest = await prismaDB.dealHistory.findFirst({
+ where: { dealId },
+ orderBy: { createdAt: "desc" },
+ });
+
+ const cleanedNew = stripTimestampsDeep(snapshot);
+ const cleanedPrev = latest ? stripTimestampsDeep(latest.snapshot) : null;
+
+ if (!cleanedPrev || JSON.stringify(cleanedPrev) !== JSON.stringify(cleanedNew)) {
+ await prismaDB.dealHistory.create({
+ data: {
+ dealId,
+ snapshot: cleanedNew,
+ },
+ });
+ }
+ }
+ }
+ }
+
+ return result;
+});
+
+export default prismaDB;
\ No newline at end of file
diff --git a/lib/prisma.ts b/lib/prisma.ts
index 7405b61..5111401 100644
--- a/lib/prisma.ts
+++ b/lib/prisma.ts
@@ -1,17 +1,6 @@
-import { PrismaClient } from "@prisma/client";
-
-const prismaClientSingleton = () => {
- return new PrismaClient({
- log: ["info"],
- });
-};
-
-declare const globalThis: {
- prismaGlobal: ReturnType;
-} & typeof global;
-
-const prismaDB = globalThis.prismaGlobal ?? prismaClientSingleton();
+// Re-export the Prisma client for backward compatibility with imports from "@/lib/prisma"
+import prismaDB from "./prisma.server";
export default prismaDB;
-if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prismaDB;
+
diff --git a/lib/queries.ts b/lib/queries.ts
index d53f52f..3a646ad 100644
--- a/lib/queries.ts
+++ b/lib/queries.ts
@@ -1,7 +1,7 @@
// all our database queries for the app
import { User } from "next-auth";
-import prismaDB from "./prisma";
+import prismaDB from "./prisma.server";
import { withAuthServerAction } from "./withAuth";
import axios from "axios";
import { BitrixDealGET } from "@/app/types";
diff --git a/lib/redis.ts b/lib/redis.ts
index ae13037..f74fd8a 100644
--- a/lib/redis.ts
+++ b/lib/redis.ts
@@ -1,5 +1,10 @@
-import { createClient } from "redis";
+let cachedClient: any;
-export const redisClient = createClient({
- url: "redis://localhost:6379",
-});
+export async function getRedisClient() {
+ if (cachedClient) return cachedClient;
+ const { createClient } = await import("redis");
+ const url = process.env.REDIS_URL || "redis://localhost:6379";
+ const client = createClient({ url });
+ cachedClient = client;
+ return client;
+}
diff --git a/lib/schemas.ts b/lib/schemas.ts
index db06535..63d4bdc 100644
--- a/lib/schemas.ts
+++ b/lib/schemas.ts
@@ -1,3 +1,4 @@
+// lib/schemas.ts
import * as z from "zod";
import { DealDocumentCategory, DealStatus } from "@prisma/client";
@@ -12,32 +13,49 @@ export type dealSpecificationsFormSchemaType = z.infer<
typeof dealSpecificationsFormSchema
>;
+// define a helper that is either "instanceof File + size check" in the browser,
+// or just a no-op z.any() on the server where File === undefined
+const fileSchema = typeof File !== "undefined"
+ ? z
+ .instanceof(File)
+ .refine((file) => file.size <= 20 * 1024 * 1024, {
+ message: "File size must be less than 20MB",
+ })
+ : z.any();
+
export const dealDocumentFormSchema = z.object({
- title: z.string().min(1, "Title is required"),
+ title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
- category: z.nativeEnum(DealDocumentCategory),
- file: z.instanceof(File).refine((file) => file.size <= 20 * 1024 * 1024, {
- message: "File size must be less than 20MB",
- }),
+ category: z.nativeEnum(DealDocumentCategory),
+ file: fileSchema,
});
-export type DealDocumentFormValues = z.infer;
+export type DealDocumentFormValues = z.infer<
+ typeof dealDocumentFormSchema
+>;
+
+// and do the same for your CIM upload
+const cimFileSchema = typeof File !== "undefined"
+ ? z
+ .instanceof(File)
+ .refine((file) => file.size <= 10 * 1024 * 1024, {
+ message: "File size must be less than 10MB",
+ })
+ : z.any();
export const cimFormSchema = z.object({
- title: z.string().min(1, "Title is required"),
+ title: z.string().min(1, "Title is required"),
caption: z.string().min(1, "Caption is required"),
- status: z.enum(["IN_PROGRESS", "COMPLETED"]),
- file: z.instanceof(File).refine((file) => file.size <= 10 * 1024 * 1024, {
- message: "File size must be less than 10MB",
- }),
+ status: z.enum(["IN_PROGRESS", "COMPLETED"]),
+ file: cimFileSchema,
});
export type CimFormValues = z.infer;
export const screenDealSchema = z.object({
- title: z.string(),
+ title: z.string(),
explanation: z.string(),
- sentiment: z.enum(["POSITIVE", "NEUTRAL", "NEGATIVE"]),
+ sentiment: z.enum(["POSITIVE", "NEUTRAL", "NEGATIVE"]),
});
export type screenDealSchemaType = z.infer;
diff --git a/lib/server/schemas.ts b/lib/server/schemas.ts
new file mode 100644
index 0000000..9e26f80
--- /dev/null
+++ b/lib/server/schemas.ts
@@ -0,0 +1,24 @@
+import { z } from 'zod';
+
+// Define or import the DealDocumentCategory enum
+export enum DealDocumentCategory {
+ SALES = 'SALES',
+ PURCHASE = 'PURCHASE',
+ OTHER = 'OTHER',
+}
+
+// lib/server/schemas.ts (no File references)
+export const dealDocumentServerSchema = z.object({
+ title: z.string().min(1),
+ description: z.string().min(1),
+ category: z.nativeEnum(DealDocumentCategory),
+})
+
+// lib/client/schemas.ts (file validation lives here)
+export const dealDocumentClientSchema = dealDocumentServerSchema.extend({
+ file: z
+ .instanceof(File)
+ .refine((f) => f.size <= 20 * 1024 * 1024, {
+ message: 'File size must be < 20 MB',
+ }),
+})
diff --git a/package-lock.json b/package-lock.json
index 0e942d3..e2ba9f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,8 @@
"@ai-sdk/react": "^1.2.9",
"@auth/prisma-adapter": "^2.7.4",
"@clerk/nextjs": "^6.5.1",
+ "@faker-js/faker": "^9.9.0",
+ "@fontsource-variable/geist-mono": "^5.2.6",
"@hookform/resolvers": "^3.9.1",
"@prisma/client": "^6.1.0",
"@radix-ui/react-accordion": "^1.2.1",
@@ -62,6 +64,7 @@
"next": "15.0.3",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.6",
+ "node-fetch": "^3.3.2",
"openai": "^4.97.0",
"pdf-parse": "^1.1.1",
"react": "19.0.0-rc-66855b96-20241106",
@@ -540,6 +543,22 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@faker-js/faker": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
+ "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0"
+ }
+ },
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
@@ -1144,6 +1163,15 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
},
+ "node_modules/@fontsource-variable/geist-mono": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/geist-mono/-/geist-mono-5.2.6.tgz",
+ "integrity": "sha512-vw6T9JGTrYJ980bn7W8iTPhe2jVK5ifunVs7xh9dfTVArjDSkJs03JjeZrH5LKEpGABLXSlSlNU57HRm4tmFMg==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
@@ -5190,6 +5218,26 @@
"node-fetch": "^2.7.0"
}
},
+ "node_modules/cross-fetch/node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
@@ -5343,6 +5391,15 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@@ -6423,6 +6480,38 @@
"node": ">=0.8.0"
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/fetch-blob/node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -6609,6 +6698,18 @@
"node": ">= 12.20"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
@@ -9102,23 +9203,21 @@
"license": "MIT"
},
"node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
- "whatwg-url": "^5.0.0"
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
},
"engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
}
},
"node_modules/normalize-path": {
@@ -9305,6 +9404,26 @@
"undici-types": "~5.26.4"
}
},
+ "node_modules/openai/node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/openai/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
diff --git a/package.json b/package.json
index 3343d23..afe589e 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,8 @@
"@ai-sdk/react": "^1.2.9",
"@auth/prisma-adapter": "^2.7.4",
"@clerk/nextjs": "^6.5.1",
+ "@faker-js/faker": "^9.9.0",
+ "@fontsource-variable/geist-mono": "^5.2.6",
"@hookform/resolvers": "^3.9.1",
"@prisma/client": "^6.1.0",
"@radix-ui/react-accordion": "^1.2.1",
@@ -70,6 +72,7 @@
"next": "15.0.3",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.6",
+ "node-fetch": "^3.3.2",
"openai": "^4.97.0",
"pdf-parse": "^1.1.1",
"react": "19.0.0-rc-66855b96-20241106",
@@ -106,5 +109,6 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
- }
+ },
+ "type": "module"
}
diff --git a/prisma/migrations/20250729061122_add_description/migration.sql b/prisma/migrations/20250729061122_add_description/migration.sql
new file mode 100644
index 0000000..bb95213
--- /dev/null
+++ b/prisma/migrations/20250729061122_add_description/migration.sql
@@ -0,0 +1,200 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- CreateEnum
+CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN');
+
+-- CreateEnum
+CREATE TYPE "DealDocumentCategory" AS ENUM ('LEGAL', 'DOCUMENTATION', 'MARKETING', 'INVESTOR_RELATIONSHIPS', 'TECHNICAL', 'TOOLS', 'LEGISLATION', 'RESEARCH', 'PROSPECTUS', 'FINANCIALS', 'OTHER');
+
+-- CreateEnum
+CREATE TYPE "DealType" AS ENUM ('SCRAPED', 'MANUAL', 'AI_INFERRED');
+
+-- CreateEnum
+CREATE TYPE "SIMStatus" AS ENUM ('IN_PROGRESS', 'COMPLETED');
+
+-- CreateEnum
+CREATE TYPE "Sentiment" AS ENUM ('POSITIVE', 'NEUTRAL', 'NEGATIVE');
+
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "email" TEXT NOT NULL,
+ADD COLUMN "emailVerified" TIMESTAMP(3),
+ADD COLUMN "image" TEXT,
+ADD COLUMN "isBlocked" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'USER',
+ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
+ALTER COLUMN "name" DROP NOT NULL;
+
+-- CreateTable
+CREATE TABLE "Account" (
+ "userId" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerAccountId" TEXT NOT NULL,
+ "refresh_token" TEXT,
+ "access_token" TEXT,
+ "expires_at" INTEGER,
+ "token_type" TEXT,
+ "scope" TEXT,
+ "id_token" TEXT,
+ "session_state" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId")
+);
+
+-- CreateTable
+CREATE TABLE "Deal" (
+ "id" TEXT NOT NULL,
+ "brokerage" TEXT NOT NULL,
+ "firstName" TEXT,
+ "lastName" TEXT,
+ "email" TEXT,
+ "linkedinUrl" TEXT,
+ "workPhone" TEXT,
+ "dealCaption" TEXT NOT NULL,
+ "revenue" DOUBLE PRECISION NOT NULL,
+ "ebitda" DOUBLE PRECISION NOT NULL,
+ "title" TEXT,
+ "description" TEXT,
+ "dealTeaser" TEXT,
+ "grossRevenue" DOUBLE PRECISION,
+ "askingPrice" DOUBLE PRECISION,
+ "ebitdaMargin" DOUBLE PRECISION NOT NULL,
+ "industry" TEXT NOT NULL,
+ "dealType" "DealType" NOT NULL DEFAULT 'MANUAL',
+ "sourceWebsite" TEXT NOT NULL,
+ "companyLocation" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "bitrixId" TEXT,
+ "bitrixCreatedAt" TIMESTAMP(3),
+ "userId" TEXT,
+
+ CONSTRAINT "Deal_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DealDocument" (
+ "id" TEXT NOT NULL,
+ "dealId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "category" "DealDocumentCategory" NOT NULL DEFAULT 'OTHER',
+ "documentUrl" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "DealDocument_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "POC" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "workPhone" TEXT,
+ "email" TEXT NOT NULL,
+ "dealId" TEXT,
+
+ CONSTRAINT "POC_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SIM" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "caption" TEXT NOT NULL,
+ "status" "SIMStatus" NOT NULL,
+ "fileName" TEXT NOT NULL,
+ "fileType" TEXT NOT NULL,
+ "fileUrl" TEXT NOT NULL,
+ "dealId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "SIM_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "questionnaires" (
+ "id" TEXT NOT NULL,
+ "fileUrl" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "purpose" TEXT NOT NULL,
+ "author" TEXT NOT NULL,
+ "version" TEXT NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "questionnaires_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AiScreening" (
+ "id" TEXT NOT NULL,
+ "dealId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "explanation" TEXT NOT NULL,
+ "score" INTEGER,
+ "content" TEXT,
+ "sentiment" "Sentiment" NOT NULL DEFAULT 'NEUTRAL',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "AiScreening_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserActionLog" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "UserActionLog_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Employee" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "dealId" TEXT,
+
+ CONSTRAINT "Employee_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- AddForeignKey
+ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Deal" ADD CONSTRAINT "Deal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DealDocument" ADD CONSTRAINT "DealDocument_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "POC" ADD CONSTRAINT "POC_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SIM" ADD CONSTRAINT "SIM_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AiScreening" ADD CONSTRAINT "AiScreening_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserActionLog" ADD CONSTRAINT "UserActionLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Employee" ADD CONSTRAINT "Employee_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250729061747_add_published_at/migration.sql b/prisma/migrations/20250729061747_add_published_at/migration.sql
new file mode 100644
index 0000000..df027bd
--- /dev/null
+++ b/prisma/migrations/20250729061747_add_published_at/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Deal" ADD COLUMN "publishedAt" TIMESTAMP(3);
diff --git a/prisma/migrations/20250730071405_add_raw_deal_fields/migration.sql b/prisma/migrations/20250730071405_add_raw_deal_fields/migration.sql
new file mode 100644
index 0000000..17867d4
--- /dev/null
+++ b/prisma/migrations/20250730071405_add_raw_deal_fields/migration.sql
@@ -0,0 +1,10 @@
+-- CreateEnum
+CREATE TYPE "DealStatus" AS ENUM ('AVAILABLE', 'SOLD', 'UNDER_CONTRACT');
+
+-- AlterTable
+ALTER TABLE "Deal" ADD COLUMN "bitrixLink" TEXT,
+ADD COLUMN "published" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "reviewed" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "seen" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "status" "DealStatus" NOT NULL DEFAULT 'AVAILABLE',
+ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
diff --git a/prisma/migrations/20250730075042_add_deal_history/migration.sql b/prisma/migrations/20250730075042_add_deal_history/migration.sql
new file mode 100644
index 0000000..b48c889
--- /dev/null
+++ b/prisma/migrations/20250730075042_add_deal_history/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "DealHistory" (
+ "id" TEXT NOT NULL,
+ "dealId" TEXT NOT NULL,
+ "snapshot" JSONB NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "DealHistory_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "DealHistory" ADD CONSTRAINT "DealHistory_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
index fbffa92..648c57f 100644
--- a/prisma/migrations/migration_lock.toml
+++ b/prisma/migrations/migration_lock.toml
@@ -1,3 +1,3 @@
# Please do not edit this file manually
-# It should be added in your version-control system (i.e. Git)
+# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
\ No newline at end of file
diff --git a/prisma/queries.ts b/prisma/queries.ts
index f489e04..75716c2 100644
--- a/prisma/queries.ts
+++ b/prisma/queries.ts
@@ -1,4 +1,4 @@
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
/**
* Get a user by their ID
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 8cee3fc..7714f6e 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -68,6 +68,7 @@ model Deal {
revenue Float
ebitda Float
title String?
+ description String?
dealTeaser String?
grossRevenue Float?
askingPrice Float?
@@ -78,8 +79,16 @@ model Deal {
companyLocation String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
+ publishedAt DateTime?
SIM SIM[]
AiScreening AiScreening[]
+ history DealHistory[]
+ status DealStatus @default(AVAILABLE)
+ tags String[] @default([])
+ reviewed Boolean @default(false)
+ seen Boolean @default(false)
+ bitrixLink String?
+ published Boolean @default(false)
bitrixLink String?
status DealStatus @default(NOT_SPECIFIED)
@@ -170,6 +179,12 @@ enum SIMStatus {
COMPLETED
}
+enum DealStatus {
+ AVAILABLE
+ SOLD
+ UNDER_CONTRACT
+}
+
model AiScreening {
id String @id @default(cuid())
dealId String
@@ -206,3 +221,11 @@ model Employee {
Deal Deal? @relation(fields: [dealId], references: [id])
dealId String?
}
+
+model DealHistory {
+ id String @id @default(cuid())
+ dealId String
+ deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
+ snapshot Json // JSON of the Deal at that moment
+ createdAt DateTime @default(now())
+}
diff --git a/prisma/scripts/deleteAllDeals.ts b/prisma/scripts/deleteAllDeals.ts
index 037b33c..924526f 100644
--- a/prisma/scripts/deleteAllDeals.ts
+++ b/prisma/scripts/deleteAllDeals.ts
@@ -1,4 +1,4 @@
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
async function main() {
console.log("deleting all deals");
diff --git a/prisma/scripts/updateEbitdaMargin.ts b/prisma/scripts/updateEbitdaMargin.ts
index 0079113..30cd1f0 100644
--- a/prisma/scripts/updateEbitdaMargin.ts
+++ b/prisma/scripts/updateEbitdaMargin.ts
@@ -1,4 +1,4 @@
-import prismaDB from "@/lib/prisma";
+import prismaDB from "@/lib/prisma.server";
async function main() {
const deals = await prismaDB.deal.findMany();
diff --git a/prisma/seed.ts b/prisma/seed.ts
index aeb6711..3eeca58 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,4 +1,4 @@
-import prismaDB from "../lib/prisma";
+import prismaDB from "../lib/prisma.server";
async function main() {
console.log("Starting seeding");
diff --git a/scripts/generateDummyDeals.cjs b/scripts/generateDummyDeals.cjs
new file mode 100644
index 0000000..47bd462
--- /dev/null
+++ b/scripts/generateDummyDeals.cjs
@@ -0,0 +1,34 @@
+// scripts/generateDummyDeals.cjs
+
+;(async () => {
+ const { PrismaClient } = require('@prisma/client');
+ const { faker } = require('@faker-js/faker');
+ const prisma = new PrismaClient();
+
+ try {
+ // generate 10 fake deals
+ for (let i = 0; i < 10; i++) {
+ await prisma.deal.create({
+ data: {
+ brokerage: faker.company.name(),
+ dealCaption: faker.lorem.sentence(),
+ revenue: parseFloat(faker.finance.amount(1e6, 1e8, 2)),
+ ebitda: parseFloat(faker.finance.amount(1e5, 1e7, 2)),
+ ebitdaMargin: parseFloat(faker.finance.amount(0, 50, 2)),
+ industry: faker.commerce.department(),
+ sourceWebsite: faker.internet.url(),
+ title: faker.lorem.words(3),
+ description: faker.lorem.paragraph(),
+ publishedAt: faker.date.recent(),
+ },
+ });
+ }
+
+ console.log('✔ Inserted 10 dummy deals');
+ } catch (err) {
+ console.error(err);
+ process.exit(1);
+ } finally {
+ await prisma.$disconnect();
+ }
+})();
diff --git a/scripts/seedFromProd.cjs b/scripts/seedFromProd.cjs
new file mode 100644
index 0000000..410b4e8
--- /dev/null
+++ b/scripts/seedFromProd.cjs
@@ -0,0 +1,48 @@
+// scripts/seedFromProd.cjs
+;(async () => {
+ // Node 18+ has fetch built‑in
+ const { PrismaClient } = require('@prisma/client')
+ const prisma = new PrismaClient()
+
+ try {
+ // 1. fetch live deals
+ const res = await fetch('http://localhost:3000/api/raw-deals')
+ if (!res.ok) {
+ throw new Error(`Fetch failed: ${res.status} ${res.statusText}`)
+ }
+ const deals = await res.json()
+
+ // 2. upsert
+ for (const d of deals) {
+ await prisma.deal.upsert({
+ where: { id: d.id },
+ create: {
+ id: d.id,
+ brokerage: d.brokerage,
+ dealCaption: d.dealCaption,
+ revenue: d.revenue,
+ ebitda: d.ebitda,
+ title: d.title ?? '',
+ description: d.description ?? '',
+ ebitdaMargin: d.ebitdaMargin,
+ industry: d.industry,
+ sourceWebsite: d.sourceWebsite,
+ publishedAt: new Date(d.publishedAt),
+ },
+ update: {
+ title: d.title,
+ description: d.description,
+ ebitdaMargin: d.ebitdaMargin,
+ publishedAt: new Date(d.publishedAt),
+ },
+ })
+ }
+
+ console.log(`✔ Upserted ${deals.length} deals`)
+ } catch (err) {
+ console.error(err)
+ process.exit(1)
+ } finally {
+ await prisma.$disconnect()
+ }
+})()
diff --git a/scripts/seedFromProd.ts b/scripts/seedFromProd.ts
new file mode 100644
index 0000000..fc96169
--- /dev/null
+++ b/scripts/seedFromProd.ts
@@ -0,0 +1,64 @@
+// scripts/seedFromProd.ts
+import fetch from 'node-fetch'
+import { PrismaClient } from '@prisma/client'
+
+type DealDTO = {
+ id: string
+ brokerage: string
+ dealCaption: string
+ revenue: number
+ ebitda: number
+ title?: string
+ description?: string
+ ebitdaMargin: number
+ industry: string
+ sourceWebsite: string
+ publishedAt: string
+}
+
+const prisma = new PrismaClient()
+
+async function main() {
+ // 1. Fetch the live deals JSON
+ const res = await fetch('https://bitrix24-three.vercel.app/api/raw-deals')
+ if (!res.ok) throw new Error(`Fetch failed: ${res.status} ${res.statusText}`)
+ const deals = (await res.json()) as DealDTO[]
+
+
+ // 2. Upsert each deal
+ for (const d of deals) {
+ await prisma.deal.upsert({
+ where: { id: d.id },
+ create: {
+ id: d.id,
+ brokerage: d.brokerage,
+ dealCaption: d.dealCaption,
+ revenue: d.revenue,
+ ebitda: d.ebitda,
+ title: d.title ?? '',
+ description: d.description ?? '',
+ ebitdaMargin: d.ebitdaMargin,
+ industry: d.industry,
+ sourceWebsite: d.sourceWebsite,
+ publishedAt: new Date(d.publishedAt),
+ },
+ update: {
+ title: d.title,
+ description: d.description,
+ ebitdaMargin: d.ebitdaMargin,
+ publishedAt: new Date(d.publishedAt),
+ },
+ })
+ }
+
+ console.log(`✔ Upserted ${deals.length} deals`)
+}
+
+main()
+ .catch((e) => {
+ console.error(e)
+ process.exit(1)
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
diff --git a/tsconfig.json b/tsconfig.json
index d8b9323..90b988a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,6 +22,6 @@
"@/*": ["./*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "types/**/*.d.ts"],
"exclude": ["node_modules"]
}
diff --git a/types/redis.d.ts b/types/redis.d.ts
new file mode 100644
index 0000000..7091c99
--- /dev/null
+++ b/types/redis.d.ts
@@ -0,0 +1,11 @@
+declare module "redis" {
+ export function createClient(options?: { url?: string }): {
+ isOpen: boolean;
+ connect: () => Promise;
+ lRange: (key: string, start: number, stop: number) => Promise;
+ lPush: (key: string, value: string) => Promise;
+ publish: (channel: string, message: string) => Promise;
+ };
+}
+
+