diff --git a/app/migrations/0012_productive_wallop.sql b/app/migrations/0012_productive_wallop.sql
new file mode 100644
index 0000000..8ce9852
--- /dev/null
+++ b/app/migrations/0012_productive_wallop.sql
@@ -0,0 +1,34 @@
+CREATE TABLE IF NOT EXISTS "crm_leads" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "lead_id" text NOT NULL,
+ "crm_url" text NOT NULL,
+ "file_name" text NOT NULL,
+ "document_url" text NOT NULL,
+ "transcription_id" uuid,
+ "extracted_data" jsonb NOT NULL,
+ "translation" text NOT NULL,
+ "user_id" text,
+ "is_default" boolean DEFAULT false NOT NULL,
+ "createdAt" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "password_reset_tokens" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "username" text NOT NULL,
+ "token" text NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "createdAt" timestamp DEFAULT now(),
+ CONSTRAINT "password_reset_tokens_username_unique" UNIQUE("username")
+);
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "crm_leads" ADD CONSTRAINT "crm_leads_transcription_id_transcriptions_id_fk" FOREIGN KEY ("transcription_id") REFERENCES "public"."transcriptions"("id") ON DELETE no action ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "crm_leads" ADD CONSTRAINT "crm_leads_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
diff --git a/app/migrations/meta/0012_snapshot.json b/app/migrations/meta/0012_snapshot.json
new file mode 100644
index 0000000..d376601
--- /dev/null
+++ b/app/migrations/meta/0012_snapshot.json
@@ -0,0 +1,578 @@
+{
+ "id": "31665b6b-8fda-4ba7-9342-a12b1c367588",
+ "prevId": "51c1645a-26bf-4446-97a1-c2049cca0522",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.bot": {
+ "name": "bot",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "botName": {
+ "name": "botName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "botEmail": {
+ "name": "botEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "botHd": {
+ "name": "botHd",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "botPicture": {
+ "name": "botPicture",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accessToken": {
+ "name": "accessToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refreshToken": {
+ "name": "refreshToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "bot_user_id_user_id_fk": {
+ "name": "bot_user_id_user_id_fk",
+ "tableFrom": "bot",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.crm_leads": {
+ "name": "crm_leads",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "lead_id": {
+ "name": "lead_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "crm_url": {
+ "name": "crm_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_url": {
+ "name": "document_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "transcription_id": {
+ "name": "transcription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extracted_data": {
+ "name": "extracted_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "translation": {
+ "name": "translation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "crm_leads_transcription_id_transcriptions_id_fk": {
+ "name": "crm_leads_transcription_id_transcriptions_id_fk",
+ "tableFrom": "crm_leads",
+ "tableTo": "transcriptions",
+ "columnsFrom": [
+ "transcription_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "crm_leads_user_id_user_id_fk": {
+ "name": "crm_leads_user_id_user_id_fk",
+ "tableFrom": "crm_leads",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.password_reset_tokens": {
+ "name": "password_reset_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "password_reset_tokens_username_unique": {
+ "name": "password_reset_tokens_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ }
+ }
+ },
+ "public.registrations": {
+ "name": "registrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userName": {
+ "name": "userName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userEmail": {
+ "name": "userEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.subscriptions": {
+ "name": "subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recordingCount": {
+ "name": "recordingCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fileSizeLimitMB": {
+ "name": "fileSizeLimitMB",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "durationDays": {
+ "name": "durationDays",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "subscriptions_name_unique": {
+ "name": "subscriptions_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ }
+ },
+ "public.transcriptions": {
+ "name": "transcriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "translation": {
+ "name": "translation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "segments": {
+ "name": "segments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "detected_language": {
+ "name": "detected_language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "documentUrl": {
+ "name": "documentUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "documentName": {
+ "name": "documentName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "isDefault": {
+ "name": "isDefault",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "audioDuration": {
+ "name": "audioDuration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_name": {
+ "name": "user_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "transcriptions_user_id_user_id_fk": {
+ "name": "transcriptions_user_id_user_id_fk",
+ "tableFrom": "transcriptions",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contactNumber": {
+ "name": "contactNumber",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subscriptionId": {
+ "name": "subscriptionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_subscriptionId_subscriptions_id_fk": {
+ "name": "user_subscriptionId_subscriptions_id_fk",
+ "tableFrom": "user",
+ "tableTo": "subscriptions",
+ "columnsFrom": [
+ "subscriptionId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ }
+ }
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/app/migrations/meta/_journal.json b/app/migrations/meta/_journal.json
index 54d72fe..4502f5e 100644
--- a/app/migrations/meta/_journal.json
+++ b/app/migrations/meta/_journal.json
@@ -85,6 +85,13 @@
"when": 1754562354517,
"tag": "0011_lucky_genesis",
"breakpoints": true
+ },
+ {
+ "idx": 12,
+ "version": "7",
+ "when": 1757093957998,
+ "tag": "0012_productive_wallop",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/app/src/app/api/crm-leads/default/route.ts b/app/src/app/api/crm-leads/default/route.ts
new file mode 100644
index 0000000..668d5c3
--- /dev/null
+++ b/app/src/app/api/crm-leads/default/route.ts
@@ -0,0 +1,25 @@
+import { NextResponse } from "next/server";
+import { db } from "@/db";
+import { crmLeadsTable } from "@/db/schema";
+import { eq } from "drizzle-orm";
+
+export async function GET(req: Request) {
+ try {
+ // Get the default CRM leads
+ const defaultLeads = await db
+ .select()
+ .from(crmLeadsTable)
+ .where(eq(crmLeadsTable.isDefault, true));
+
+ // Return the results
+ return NextResponse.json({
+ success: true,
+ data: defaultLeads
+ });
+ } catch (error) {
+ return NextResponse.json(
+ { success: false, error: "Failed to fetch default CRM leads" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/src/app/api/crm-leads/route.ts b/app/src/app/api/crm-leads/route.ts
new file mode 100644
index 0000000..aabec96
--- /dev/null
+++ b/app/src/app/api/crm-leads/route.ts
@@ -0,0 +1,30 @@
+import { NextResponse } from "next/server";
+import { db } from "@/db";
+import { crmLeadsTable } from "@/db/schema";
+
+export async function GET() {
+ try {
+
+
+
+ const allLeads = await db
+ .select()
+ .from(crmLeadsTable);
+
+
+
+ return NextResponse.json({
+ success: true,
+ data: allLeads,
+ count: allLeads.length
+ });
+ } catch (error) {
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/src/app/api/crm-leads/simple/[id]/route.ts b/app/src/app/api/crm-leads/simple/[id]/route.ts
new file mode 100644
index 0000000..d18fbac
--- /dev/null
+++ b/app/src/app/api/crm-leads/simple/[id]/route.ts
@@ -0,0 +1,51 @@
+import { NextResponse } from "next/server";
+import { db } from "@/db";
+import { crmLeadsTable } from "@/db/schema";
+import { eq, or } from "drizzle-orm";
+
+export async function GET(
+ req: Request,
+ { params }: { params: { id: string } }
+) {
+ try {
+ const id = params.id;
+
+
+ if (!id) {
+ return NextResponse.json({ error: "ID is required" }, { status: 400 });
+ }
+
+ // Try to match by any ID field
+ const lead = await db
+ .select()
+ .from(crmLeadsTable)
+ .where(
+ or(
+ eq(crmLeadsTable.id, id),
+ eq(crmLeadsTable.leadId, id)
+ )
+ )
+ .limit(1);
+
+ // If no leads are found, check if the table has any data at all
+ if (!lead || lead.length === 0) {
+ const count = await db
+ .select({ count: db.fn.count() })
+ .from(crmLeadsTable);
+
+ const totalCount = Number(count[0]?.count || 0);
+
+ return NextResponse.json({
+ error: "CRM lead not found",
+ id,
+ tableHasData: totalCount > 0,
+ totalLeads: totalCount
+ }, { status: 404 });
+ }
+
+ // Return the found lead
+ return NextResponse.json(lead[0]);
+ } catch (error) {
+ return NextResponse.json({ error: String(error) }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/src/app/api/transcribe/route.ts b/app/src/app/api/transcribe/route.ts
index 7a097c7..cfe6685 100644
--- a/app/src/app/api/transcribe/route.ts
+++ b/app/src/app/api/transcribe/route.ts
@@ -26,7 +26,6 @@ export async function POST(req: Request) {
return new Response(JSON.stringify(data), { status: 200 });
} catch (error) {
- console.log(error);
if (error instanceof z.ZodError) {
return new Response(error.message, { status: 422 });
}
diff --git a/app/src/app/api/transcribe/save/route.ts b/app/src/app/api/transcribe/save/route.ts
index bbecb05..51d1b6c 100644
--- a/app/src/app/api/transcribe/save/route.ts
+++ b/app/src/app/api/transcribe/save/route.ts
@@ -1,6 +1,7 @@
import { db } from "@/db";
import { transcriptions, TranscriptionsPayload, userTable } from "@/db/schema";
import { eq } from "drizzle-orm";
+import { crmLeadsTable } from "@/db/schema"; // Import crmLeadsTable explicitly
export async function POST(req: Request) {
try {
@@ -13,8 +14,13 @@ export async function POST(req: Request) {
translation,
audioDuration,
segments,
- detectedLanguage
- }: TranscriptionsPayload = body;
+ detectedLanguage,
+ isDefault,
+ // CRM data
+ leadId,
+ crmUrl,
+ extractedData
+ } = body;
@@ -42,10 +48,30 @@ export async function POST(req: Request) {
segments,
detectedLanguage
}).returning();
+
+ // If lead ID exists, save CRM data
+ if (leadId) {
+ try {
+
+ await db.insert(crmLeadsTable).values({
+ leadId: leadId,
+ crmUrl: crmUrl || "",
+ fileName: documentName,
+ documentUrl: documentUrl, // Make sure this field exists in your schema
+ transcriptionId: response[0].id,
+ extractedData: extractedData || {},
+ translation: translation,
+ userId: userID,
+ isDefault: isDefault === true
+ });
+
+ } catch (crmError) {
+ // Continue with the response even if CRM save fails
+ }
+ }
return new Response(JSON.stringify(response), { status: 200 });
} catch (error) {
- console.log(error);
return new Response(JSON.stringify(error), { status: 500 });
}
-}
+}
\ No newline at end of file
diff --git a/app/src/app/api/transcriptions/route.ts b/app/src/app/api/transcriptions/route.ts
index 40a8a8e..889504e 100644
--- a/app/src/app/api/transcriptions/route.ts
+++ b/app/src/app/api/transcriptions/route.ts
@@ -61,7 +61,6 @@ export async function GET(req: NextRequest) {
{ status: 200 }
);
} catch (error) {
- console.log(error);
return new Response(JSON.stringify(error), { status: 500 });
}
}
diff --git a/app/src/app/crm/[id]/page.tsx b/app/src/app/crm/[id]/page.tsx
new file mode 100644
index 0000000..6f9fefb
--- /dev/null
+++ b/app/src/app/crm/[id]/page.tsx
@@ -0,0 +1,49 @@
+import { Metadata } from "next";
+import DetailedCRM from "../../../components/DetailedCRM";
+import { getCrmLeadById } from "@/lib/crm-api";
+import { notFound } from "next/navigation";
+import { CRM_CONSTANTS } from "@/constants/crm";
+import { ExtractedData } from "@/types/crm";
+
+export const metadata: Metadata = {
+ title: "Lingo.ai | CRM Details",
+};
+
+interface PageProps {
+ params: Promise<{
+ id: string;
+ }>;
+}
+
+const page = async (props: PageProps) => {
+ const { id } = await props.params;
+
+ try {
+ const lead = await getCrmLeadById(id);
+ const extractedData = (lead.extractedData as ExtractedData) || {};
+ const crmRecord = {
+ id: lead.id,
+ leadId: lead.leadId,
+ crmUrl: lead.crmUrl,
+ fileName: lead.fileName,
+ contact: extractedData.contact || CRM_CONSTANTS.FALLBACK_DATA.CONTACT,
+ email: extractedData.email || '',
+ company: extractedData.company || CRM_CONSTANTS.FALLBACK_DATA.COMPANY,
+ lastContact: lead.createdAt ? new Date(lead.createdAt).toLocaleDateString() : '',
+ documentUrl: lead.documentUrl,
+ translation: lead.translation || CRM_CONSTANTS.FALLBACK_DATA.TRANSLATION
+ };
+
+ return (
+
+ );
+ } catch (error) {
+ notFound();
+ }
+};
+
+export default page;
diff --git a/app/src/app/crm/page.tsx b/app/src/app/crm/page.tsx
new file mode 100644
index 0000000..f4257ac
--- /dev/null
+++ b/app/src/app/crm/page.tsx
@@ -0,0 +1,16 @@
+import { Metadata } from "next";
+import CRMItem from "@/components/CRMItem";
+
+export const metadata: Metadata = {
+ title: "Lingo.ai | CRM",
+};
+
+const CRMPage = () => {
+ return (
+
+
+
+ );
+};
+
+export default CRMPage;
diff --git a/app/src/components/CRMItem.tsx b/app/src/components/CRMItem.tsx
new file mode 100644
index 0000000..1c76f05
--- /dev/null
+++ b/app/src/components/CRMItem.tsx
@@ -0,0 +1,291 @@
+"use client";
+import { useState, useEffect, useRef, useCallback, useMemo, memo } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
+import { Database, User, Mail, Phone, Calendar, MapPin, Play, Pause, FileAudio } from "lucide-react";
+import { Table, TableBody, TableHead, TableHeader, TableRow } from "./ui/table";
+import { Badge } from "./ui/badge";
+import { Button } from "./ui/button";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./ui/select";
+import Link from "next/link";
+import { getAllCrmLeads, transformCrmLead } from "@/lib/crm-api";
+import { CRM_CONSTANTS } from "@/constants/crm";
+import { ExtractedData, CRMDisplayData } from "@/types/crm";
+import { CrmLeadsType } from "@/db/schema";
+
+
+
+const CRMItem = () => {
+ const [crmData, setCrmData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const transformLead = useCallback((lead: CrmLeadsType): CRMDisplayData => {
+ const extractedData = (lead.extractedData as ExtractedData) || {};
+ return {
+ id: lead.id,
+ leadId: lead.leadId,
+ crmUrl: lead.crmUrl,
+ fileName: lead.fileName,
+ email: extractedData.email || '',
+ company: extractedData.company || CRM_CONSTANTS.FALLBACK_DATA.COMPANY,
+ contact: extractedData.contact || CRM_CONSTANTS.FALLBACK_DATA.CONTACT,
+ lastContact: lead.createdAt ? new Date(lead.createdAt).toLocaleDateString() : '',
+ documentUrl: lead.documentUrl,
+ translation: lead.translation
+ };
+ }, []);
+
+ useEffect(() => {
+ const fetchCrmData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const leads = await getAllCrmLeads();
+ const transformedLeads = leads.map(transformLead);
+ setCrmData(transformedLeads);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : CRM_CONSTANTS.MESSAGES.FAILED_TO_FETCH);
+ setCrmData([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchCrmData();
+ }, [transformLead]);
+
+ const filteredData = useMemo(() => crmData, [crmData]);
+
+ if (loading) {
+ return (
+
+
+
+
+
{CRM_CONSTANTS.MESSAGES.LOADING}
+
+
+
+ );
+ }
+
+ if (crmData.length === 0 && !error) {
+ return (
+
+
+
{CRM_CONSTANTS.UI.TITLE}
+
+ {CRM_CONSTANTS.UI.SUBTITLE}
+
+
+
+
+
+
+ {CRM_CONSTANTS.UI.EMPTY_STATE_TITLE}
+
+ {CRM_CONSTANTS.UI.EMPTY_STATE_DESCRIPTION}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {error && (
+
+
+ Warning: {error}. Showing fallback data.
+
+
+ )}
+
+
+
+
{CRM_CONSTANTS.UI.TITLE}
+
+ {CRM_CONSTANTS.UI.SUBTITLE}
+ {crmData.length > 0 && ` (${crmData.length} records)`}
+
+
+
+
+
+
+
+ All CRM Records ({filteredData.length})
+
+
+
+
+
+
+
+ Lead ID
+ File Name
+ Contact
+ Company
+ Email
+ CRM URL
+ Last Contact
+
+
+
+ {filteredData.map((record) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+interface CRMRecordRowProps {
+ record: CRMDisplayData;
+}
+
+const CRMRecordRow = memo(({ record }: CRMRecordRowProps) => {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const audioRef = useRef(null);
+
+ const getStatusBadgeVariant = (status: string) => {
+ switch (status.toLowerCase()) {
+ case "active":
+ return "default";
+ case "prospect":
+ return "secondary";
+ case "qualified":
+ return "outline";
+ case "closed":
+ return "destructive";
+ default:
+ return "secondary";
+ }
+ };
+
+ const handlePlayPause = () => {
+ if (isPlaying) {
+ audioRef.current?.pause();
+ setIsPlaying(false);
+ } else {
+ audioRef.current?.play();
+ setIsPlaying(true);
+ }
+ };
+
+ const handleAudioEnd = () => {
+ setIsPlaying(false);
+ };
+
+ useEffect(() => {
+ if (record.documentUrl) {
+ const audio = new Audio(record.documentUrl);
+ audioRef.current = audio;
+
+ audio.addEventListener("ended", handleAudioEnd);
+
+ return () => {
+ audio.removeEventListener("ended", handleAudioEnd);
+ audio.pause();
+ };
+ }
+ }, [record.documentUrl]);
+
+ useEffect(() => {
+ if (audioRef.current) {
+ if (isPlaying) {
+ audioRef.current.play();
+ } else {
+ audioRef.current.pause();
+ }
+ }
+ }, [isPlaying]);
+
+ return (
+
+ |
+
+ |
+
+
+
+
+ {record.leadId}
+
+
+ |
+
+
+
+
+ {record.fileName}
+
+
+ |
+
+
+
+ {record.contact}
+
+ |
+
+
+ {record.company}
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+ {record.lastContact}
+
+ |
+
+ );
+});
+
+CRMRecordRow.displayName = 'CRMRecordRow';
+
+export default CRMItem;
diff --git a/app/src/components/DetailedCRM.tsx b/app/src/components/DetailedCRM.tsx
new file mode 100644
index 0000000..7f28429
--- /dev/null
+++ b/app/src/components/DetailedCRM.tsx
@@ -0,0 +1,211 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ FileAudio,
+ FileText,
+ Database,
+ PlayCircleIcon,
+ PauseCircleIcon,
+ User,
+ Mail,
+ Calendar,
+ Building,
+ ExternalLink,
+} from "lucide-react";
+import { useCallback, useEffect, useRef, useState, memo } from "react";
+import Markdown from "react-markdown";
+import { CRM_CONSTANTS } from "@/constants/crm";
+import { CRMRecord } from "@/types/crm";
+
+const DetailedCRM = memo(({ crmRecord }: { crmRecord: CRMRecord }) => {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [audioDuration, setAudioDuration] = useState(null);
+ const audioRef = useRef(null);
+ const [currentTime, setCurrentTime] = useState(0);
+
+ const getProxyUrl = (audioUrl: string): string => {
+ if (audioUrl.includes('.s3.') || audioUrl.includes('s3.amazonaws.com')) {
+ return `/api/proxy?url=${encodeURIComponent(audioUrl)}`;
+ }
+ return audioUrl;
+ };
+
+ const handlePlayPause = useCallback(() => {
+ if (audioRef.current) {
+ if (isPlaying) {
+ audioRef.current.pause();
+ setIsPlaying(false);
+ } else {
+ audioRef.current.play();
+ setIsPlaying(true);
+ }
+ }
+ }, [isPlaying]);
+
+ useEffect(() => {
+ if (crmRecord.documentUrl) {
+ const proxyUrl = getProxyUrl(crmRecord.documentUrl);
+ const audio = new Audio(proxyUrl);
+ audioRef.current = audio;
+
+ const handleLoadedMetadata = () => {
+ setAudioDuration(formatDuration(audio.duration));
+ };
+
+ const handleTimeUpdate = () => {
+ setCurrentTime(audio.currentTime);
+ };
+
+ const handleEnded = () => {
+ setIsPlaying(false);
+ setCurrentTime(0);
+ };
+
+ audio.addEventListener("loadedmetadata", handleLoadedMetadata);
+ audio.addEventListener("timeupdate", handleTimeUpdate);
+ audio.addEventListener("ended", handleEnded);
+
+ return () => {
+ audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
+ audio.removeEventListener("timeupdate", handleTimeUpdate);
+ audio.removeEventListener("ended", handleEnded);
+ audio.pause();
+ };
+ }
+ }, [crmRecord.documentUrl]);
+
+ const formatDuration = (seconds: number): string => {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ const formatTime = (time: number): string => {
+ const mins = Math.floor(time / 60);
+ const secs = Math.floor(time % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
{CRM_CONSTANTS.UI.DETAILS_TITLE}
+
+ {CRM_CONSTANTS.UI.DETAILS_SUBTITLE}
+
+
+
+
+ {/* CRM Info Section */}
+
+
+
+
+
+ CRM Information
+
+
+
+
+ {/* Left Column - Contact Info */}
+
+
Contact Information
+
+
+ {crmRecord.contact}
+
+
+
+ {crmRecord.company}
+
+
+
+ {crmRecord.email}
+
+
+
+ {crmRecord.lastContact}
+
+
+
+ {/* Right Column - Lead Info & Audio */}
+
+
+
Lead Information
+
+
+ Lead ID:
+ {crmRecord.leadId}
+
+
+ File Name:
+ {crmRecord.fileName}
+
+
+
+
+
+
+
Recording
+
+
+ {crmRecord.fileName}
+
+
+
+
+
+
+
+
+
+
+ {/* Translation Section */}
+
+
+
+
+
+ Call Translation
+
+
+
+
+ {crmRecord.translation}
+
+
+
+
+
+
+ );
+});
+
+DetailedCRM.displayName = 'DetailedCRM';
+
+export default DetailedCRM;
\ No newline at end of file
diff --git a/app/src/components/NavigateBack.tsx b/app/src/components/NavigateBack.tsx
index 5d75099..9eb3d3e 100644
--- a/app/src/components/NavigateBack.tsx
+++ b/app/src/components/NavigateBack.tsx
@@ -15,6 +15,8 @@ const NavigateBack = (props: NavigateBackProps) => {
const handleBack = () => {
if (pathname.startsWith("/transcriptions/")) {
router.push("/transcriptions");
+ } else if (pathname.startsWith("/crm/")) {
+ router.push("/crm");
} else {
href ? router.push(href) : router.back();
}
diff --git a/app/src/components/Navigation.tsx b/app/src/components/Navigation.tsx
index 7addf1b..ddab2e4 100644
--- a/app/src/components/Navigation.tsx
+++ b/app/src/components/Navigation.tsx
@@ -14,6 +14,9 @@ import {
Files,
User2,
Layers,
+ ChevronDown,
+ FileText,
+ Database,
} from "lucide-react";
import {
DropdownMenu,
@@ -239,11 +242,32 @@ const Navigation = ({ isSignedIn }: NavigationProps) => {
)}
- {pathname !== "/transcriptions" && (
-
+ {pathname !== "/transcriptions" && pathname !== "/crm" && (
+
+
+
+
+
+ router.push("/transcriptions")}
+ className="cursor-pointer hover:!text-white hover:font-bold hover:!bg-[#668D7E]"
+ >
+
+ Summarization
+
+ router.push("/crm")}
+ className="cursor-pointer hover:!text-white hover:font-bold hover:!bg-[#668D7E]"
+ >
+
+ View CRM
+
+
+
)}
{isSignedIn && pathname !== "/" && (
diff --git a/app/src/components/RecorderCard.tsx b/app/src/components/RecorderCard.tsx
index c5c69ab..63d212d 100644
--- a/app/src/components/RecorderCard.tsx
+++ b/app/src/components/RecorderCard.tsx
@@ -117,6 +117,10 @@ const RecorderCard = (props: RecorderCardProps) => {
translation: res.translation,
segments: res.segments,
detectedLanguage: res.detected_language,
+ leadId: res.leadId,
+ crmUrl: res.crmUrl,
+ extractedData: res.extractedData,
+ isDefault: res.isDefault,
});
},
onError: (error) => {
@@ -181,7 +185,7 @@ const RecorderCard = (props: RecorderCardProps) => {
setStatus(`Saving transcription for ${file?.name}`);
toast.info(`Saving transcription for ${file?.name}`);
},
- mutationFn: async (data: TranscriptionsPayload) => {
+ mutationFn: async (data: any) => {
if (recordingTime > 0) {
// recorded
data.audioDuration = recordingTime;
@@ -437,4 +441,4 @@ const RecorderCard = (props: RecorderCardProps) => {
);
};
-export default RecorderCard;
+export default RecorderCard;
\ No newline at end of file
diff --git a/app/src/constants/crm.ts b/app/src/constants/crm.ts
new file mode 100644
index 0000000..8190a09
--- /dev/null
+++ b/app/src/constants/crm.ts
@@ -0,0 +1,22 @@
+export const CRM_CONSTANTS = {
+ MESSAGES: {
+ LOADING: "Loading CRM records...",
+ FAILED_TO_FETCH: "Failed to fetch CRM data",
+ UNKNOWN_COMPANY: "Unknown Company",
+ UNKNOWN_CONTACT: "Unknown",
+ NO_TRANSLATION: "No translation available",
+ },
+ UI: {
+ TITLE: "CRM Records",
+ SUBTITLE: "Manage and view your customer relationship management data",
+ DETAILS_TITLE: "CRM Record Details",
+ DETAILS_SUBTITLE: "View and analyze customer relationship data and call recordings",
+ EMPTY_STATE_TITLE: "No CRM Records Found",
+ EMPTY_STATE_DESCRIPTION: "You don't have any CRM records yet. Start by uploading audio files or creating new leads.",
+ },
+ FALLBACK_DATA: {
+ COMPANY: "Unknown Company",
+ CONTACT: "Unknown",
+ TRANSLATION: "No translation available",
+ }
+} as const;
diff --git a/app/src/db/schema.ts b/app/src/db/schema.ts
index 0a0e1d6..132849f 100644
--- a/app/src/db/schema.ts
+++ b/app/src/db/schema.ts
@@ -97,3 +97,25 @@ export const passwordResetTokens = pgTable("password_reset_tokens", {
export type TranscriptionsPayload = typeof transcriptions.$inferInsert;
export type TranscriptionsType = typeof transcriptions.$inferSelect;
+
+
+
+// Add this to your schema.ts file if it doesn't exist
+
+export const crmLeadsTable = pgTable("crm_leads", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ leadId: text("lead_id").notNull(),
+ crmUrl: text("crm_url").notNull(),
+ fileName: text("file_name").notNull(),
+ documentUrl: text("document_url").notNull(), // Add this new field
+ transcriptionId: uuid("transcription_id").references(() => transcriptions.id),
+ extractedData: jsonb("extracted_data").notNull(),
+ translation: text("translation").notNull(),
+ userId: text("user_id").references(() => userTable.id),
+ isDefault: boolean("is_default").notNull().default(false),
+ createdAt: timestamp("createdAt", { mode: "date" }).defaultNow()
+});
+
+// Make sure to export the type as well
+export type CrmLeadsPayload = typeof crmLeadsTable.$inferInsert;
+export type CrmLeadsType = typeof crmLeadsTable.$inferSelect;
\ No newline at end of file
diff --git a/app/src/lib/crm-api.ts b/app/src/lib/crm-api.ts
new file mode 100644
index 0000000..247d0f2
--- /dev/null
+++ b/app/src/lib/crm-api.ts
@@ -0,0 +1,167 @@
+import { CrmLeadsType } from "@/db/schema";
+import { ExtractedData } from "@/types/crm";
+
+interface ApiResponse {
+ success: boolean;
+ data?: T;
+ count?: number;
+ error?: string;
+}
+
+interface CrmLeadWithDetails extends CrmLeadsType {
+
+ name?: string;
+ email?: string;
+ phone?: string;
+ company?: string;
+ status?: string;
+ lastContact?: string;
+ location?: string;
+ leadSource?: string;
+ recordingUrl?: string;
+ recordingName?: string;
+ extraction?: {
+ entities: Array<{
+ type: string;
+ value: string;
+ confidence: number;
+ }>;
+ keyPoints: string[];
+ actionItems: string[];
+ };
+}
+
+
+export class CrmApiClient {
+ private baseUrl: string;
+
+ constructor() {
+ this.baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
+ }
+
+ private getApiUrl(endpoint: string): string {
+ if (this.baseUrl) {
+ return `${this.baseUrl}${endpoint}`;
+ }
+ // In server-side context, we need an absolute URL
+ if (typeof window === 'undefined') {
+ // Server-side: construct absolute URL
+ const baseUrl = process.env.NEXTAUTH_URL || process.env.VERCEL_URL || 'http://localhost:3000';
+ return `${baseUrl}${endpoint}`;
+ }
+
+ return endpoint;
+ }
+
+ async getAllLeads(): Promise {
+ try {
+ const response = await fetch(this.getApiUrl('/api/crm-leads'), {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch CRM leads: ${response.statusText}`);
+ }
+
+ const result: ApiResponse = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch CRM leads');
+ }
+
+ return result.data || [];
+ } catch (error) {
+ throw error;
+ }
+ }
+
+
+ async getLeadById(id: string): Promise {
+ try {
+ const url = this.getApiUrl(`/api/crm-leads/simple/${id}`);
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+
+ if (response.status === 404) {
+ throw new Error('CRM lead not found');
+ }
+ throw new Error(`Failed to fetch CRM lead: ${response.statusText} - ${errorText}`);
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+
+ async getDefaultLeads(): Promise {
+ try {
+ const response = await fetch(this.getApiUrl('/api/crm-leads/default'), {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch default CRM leads: ${response.statusText}`);
+ }
+
+ const result: ApiResponse = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch default CRM leads');
+ }
+
+ return result.data || [];
+ } catch (error) {
+ throw error;
+ }
+ }
+
+
+ transformLeadToDisplay(lead: CrmLeadsType): CrmLeadWithDetails {
+ const extractedData = (lead.extractedData as ExtractedData) || {};
+
+ return {
+ ...lead,
+ name: extractedData.name || extractedData.contactName || 'Unknown',
+ email: extractedData.email || extractedData.contactEmail || '',
+ phone: extractedData.phone || extractedData.contactPhone || '',
+ company: extractedData.company || extractedData.organization || 'Unknown Company',
+ status: extractedData.status || 'Active',
+ lastContact: lead.createdAt ? new Date(lead.createdAt).toISOString().split('T')[0] : '',
+ location: extractedData.location || extractedData.address || '',
+ leadSource: extractedData.leadSource || extractedData.source || 'Unknown',
+ recordingUrl: lead.documentUrl,
+ recordingName: lead.fileName,
+ extraction: {
+ entities: extractedData.entities || [],
+ keyPoints: extractedData.keyPoints || [],
+ actionItems: extractedData.actionItems || []
+ }
+ };
+ }
+}
+
+
+export const crmApiClient = new CrmApiClient();
+
+
+export const getAllCrmLeads = () => crmApiClient.getAllLeads();
+export const getCrmLeadById = (id: string) => crmApiClient.getLeadById(id);
+export const getDefaultCrmLeads = () => crmApiClient.getDefaultLeads();
+export const transformCrmLead = (lead: CrmLeadsType) => crmApiClient.transformLeadToDisplay(lead);
diff --git a/app/src/types/TranscriptionResponse.ts b/app/src/types/TranscriptionResponse.ts
index fedc66a..c84c646 100644
--- a/app/src/types/TranscriptionResponse.ts
+++ b/app/src/types/TranscriptionResponse.ts
@@ -6,4 +6,9 @@ export type TranscriptionResponse = {
summary: string;
segments: segment[];
detected_language: string;
+ leadId?: string;
+ crmUrl?: string;
+ extractedData?: any;
+ isDefault?: boolean;
+ transcriptionId?: string | null;
};
diff --git a/app/src/types/crm.ts b/app/src/types/crm.ts
new file mode 100644
index 0000000..8fa80ed
--- /dev/null
+++ b/app/src/types/crm.ts
@@ -0,0 +1,58 @@
+
+export interface ExtractedData {
+
+ name?: string;
+ contactName?: string;
+ email?: string;
+ contactEmail?: string;
+ phone?: string;
+ contactPhone?: string;
+ contact?: string;
+
+ company?: string;
+ organization?: string;
+
+ status?: string;
+ leadSource?: string;
+ source?: string;
+ location?: string;
+ address?: string;
+
+
+ entities?: Array<{
+ type: string;
+ value: string;
+ confidence: number;
+ }>;
+ keyPoints?: string[];
+ actionItems?: string[];
+
+
+ [key: string]: any;
+}
+
+export interface CRMDisplayData {
+ id: string;
+ leadId: string;
+ crmUrl: string;
+ fileName: string;
+ email: string;
+ company: string;
+ contact: string;
+ lastContact: string;
+ documentUrl: string;
+ translation: string;
+}
+
+export interface CRMRecord {
+ id: string;
+ leadId: string;
+ crmUrl: string;
+ fileName: string;
+ contact: string;
+ email: string;
+ company: string;
+ lastContact: string;
+ documentUrl: string;
+ translation: string;
+}
diff --git a/service/config.py b/service/config.py
index fd5463c..fb8b7cb 100644
--- a/service/config.py
+++ b/service/config.py
@@ -11,7 +11,6 @@
ollama_model_name = os.getenv("OLLAMA_MODEL_NAME", "llama3.2")
open_ai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4")
open_ai_temperature = os.getenv("OPENAI_TEMPERATURE", 0.2)
-
odoo_url = os.getenv("ODOO_URL")
odoo_db = os.getenv("ODOO_DB")
odoo_username = os.getenv("ODOO_USERNAME")
diff --git a/service/crm_client.py b/service/crm_client.py
index c7a59a9..b4df929 100644
--- a/service/crm_client.py
+++ b/service/crm_client.py
@@ -9,7 +9,6 @@ def __init__(self, url: str, db: str, username: str, password: str):
self.username = username
self.password = password
-
self.common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common", allow_none=True)
self.uid = self.common.authenticate(db, username, password, {})
if not self.uid:
diff --git a/service/main.py b/service/main.py
index 7bf7216..a68fa0c 100644
--- a/service/main.py
+++ b/service/main.py
@@ -30,6 +30,8 @@
allow_headers=["*"], # Allows all headers
)
+app.include_router(core_banking_mock_router)
+
@app.get("/")
def root_route():
return 'Hello, this is the root route for lingo ai server'
@@ -47,10 +49,12 @@ def generate_timestamp_json(translation, summary, detected_language=None):
"detected_language": detected_language or translation.get("detected_language", "unknown")
}
+# First API endpoint (v1)
@api_version(1)
@app.post("/upload-audio")
async def upload_audio(body: Body):
try:
+
if body.audio_file_link == "":
return JSONResponse(status_code=400, content={"message":"Invalid file link"})
@@ -68,11 +72,66 @@ async def upload_audio(body: Body):
result = generate_timestamp_json(translation, summary, detected_language)
+ contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None}
+
+ logger.info(result)
+
+ # Fire-and-forget CRM sync (do not block response)
+ lead_id = None
+ try:
+ if odoo_url and odoo_db and odoo_username and odoo_password:
+ client = OdooCRMClient(odoo_url, odoo_db, odoo_username, odoo_password)
+ lead_id = client.create_lead(
+ name=contact_info.get("name") or "Unknown",
+ email=None,
+ phone=contact_info.get("phone"),
+ )
+ logger.info(f"CRM: Lead created lead_id={lead_id}")
+ partner_id = client.add_contact_details(lead_id, contact_info.get("name"), None, contact_info.get("phone"))
+ logger.info(f"CRM: Partner created/linked partner_id={partner_id} to lead_id={lead_id}")
+
+ # Update: Use the complete street information from LLM extraction
+ street = contact_info.get("street")
+ logger.info(f"{street}= streetstreetstreetstreet")
+ street2 = None
+ city = contact_info.get("city")
+ state = contact_info.get("state")
+ zip_code = contact_info.get("zip")
+ country = contact_info.get("country")
+
+ logger.info(f"CRM: Address components street={street}, city={city}")
+
+ updated = client.update_contact_address(
+ partner_id,
+ street=street,
+ street2=street2,
+ city=city,
+ state_id=None,
+ zip_code=zip_code,
+ country_id=None
+ )
+ logger.info(f"CRM: Address update result={updated} for partner_id={partner_id}")
+
+ # Add CRM data to the result
+ result["leadId"] = str(lead_id)
+ result["crmUrl"] = odoo_url
+ result["extractedData"] = contact_info
+ result["transcriptionId"] = None # This will be determined when transcription is saved
+ result["translation"] = translation["text"]
+ result["userId"] = None # This will be set by frontend
+ result["isDefault"] = False
+ else:
+ logger.info("CRM: Odoo credentials not configured; skipping CRM sync")
+ except Exception as e:
+ logger.info(f"CRM: Exception during sync: {e}")
+
return JSONResponse(content=result, status_code=200)
except Exception as e:
logger.info(traceback.format_exc())
return JSONResponse(content={"message": str(e)}, status_code=500)
+
+# Second API endpoint (v2)
@api_version(2)
@app.post("/upload-audio")
async def upload_audio(body: Body):
@@ -80,14 +139,76 @@ async def upload_audio(body: Body):
if body.audio_file_link == "":
return JSONResponse(status_code=400, content={"message":"Invalid file link"})
- # Remove file extension check since frontend handles this
translation = translate_with_whisper_timestamped(body.audio_file_link)
- detected_language = translation.get('detected_language', 'unknown')
+
+ # Extract detected language
+ detected_language = translation.get("detected_language", "unknown")
+
logger.info("translation done")
summary = summarize_using_ollama(translation["text"])
logger.info("summary done")
- result = generate_timestamp_json(translation,summary,detected_language)
+
+ # Pass the translation object and detected_language to generate_timestamp_json
+ result = generate_timestamp_json(translation, summary, detected_language)
+
+
+ contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None}
+
+ logger.info(result)
+
+ # Fire-and-forget CRM sync (do not block response)
+ lead_id = None
+ try:
+ if odoo_url and odoo_db and odoo_username and odoo_password:
+ client = OdooCRMClient(odoo_url, odoo_db, odoo_username, odoo_password)
+ lead_id = client.create_lead(
+ name=contact_info.get("name") or "Unknown",
+ email=None,
+ phone=contact_info.get("phone"),
+ )
+ logger.info(f"CRM: Lead created lead_id={lead_id}")
+ partner_id = client.add_contact_details(lead_id, contact_info.get("name"), None, contact_info.get("phone"))
+ logger.info(f"CRM: Partner created/linked partner_id={partner_id} to lead_id={lead_id}")
+
+ # Update: Use the complete street information from LLM extraction
+ street = contact_info.get("street")
+ logger.info(f"CRM: - Street: '{street}'")
+
+
+ street2 = None
+ city = contact_info.get("city")
+ state = contact_info.get("state")
+ zip_code = contact_info.get("zip")
+ country = contact_info.get("country")
+
+ logger.info(f"CRM: Address components street={street}, city={city}")
+
+ updated = client.update_contact_address(
+ partner_id,
+ street=street,
+ street2=street2,
+ city=city,
+ state_id=None,
+ zip_code=zip_code,
+ country_id=None
+ )
+ logger.info(f"CRM: Address update result={updated} for partner_id={partner_id}")
+
+ # Add CRM data to the result
+ result["leadId"] = str(lead_id)
+ result["crmUrl"] = odoo_url
+ result["extractedData"] = contact_info
+ result["transcriptionId"] = None # This will be determined when transcription is saved
+ result["translation"] = translation["text"]
+ result["userId"] = None # This will be set by frontend
+ result["isDefault"] = False
+
+
+ else:
+ logger.info("CRM: Odoo credentials not configured; skipping CRM sync")
+ except Exception as e:
+ logger.info(f"CRM: Exception during sync: {e}")
return JSONResponse(content=result, status_code=200)
@@ -138,11 +259,65 @@ async def transcribe_intent(audio: UploadFile = File(...), session_id: str = For
"intent_data": formatted_intent_data
}
}
- return JSONResponse(content=result, status_code=200)
+
+ # Make the API call
+ response = requests.post(
+ f"{api_base_url}/api/crm-leads",
+ json=crm_lead_data,
+ headers={"Content-Type": "application/json"}
+ )
+
+ if response.status_code == 200:
+ logger.info(f"CRM lead data saved successfully for lead_id={lead_id}")
+ return response.json()
+ else:
+ logger.error(f"Failed to save CRM lead data: {response.status_code} - {response.text}")
+ return None
+
+ except Exception as e:
+ logger.error(f"Exception saving CRM lead data: {str(e)}")
+ return None
+
+
+
+# Add this function to retrieve default CRM leads
+async def get_default_crm_leads():
+ """
+ Fetch CRM leads that are marked as default.
+ """
+ try:
+ api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000")
+
+ response = requests.get(
+ f"{api_base_url}/api/crm-leads/default",
+ headers={"Content-Type": "application/json"}
+ )
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ logger.error(f"Failed to get default CRM leads: {response.status_code}")
+ return None
+ except Exception as e:
+ logger.error(f"Error fetching default CRM leads: {str(e)}")
+ return None
+# Add a route to expose this functionality
+@app.get("/crm-leads/default")
+async def fetch_default_crm_leads():
+ try:
+ result = await get_default_crm_leads()
+ if result and result.get("success"):
+ return JSONResponse(content=result, status_code=200)
+ else:
+ return JSONResponse(
+ content={"message": "Failed to retrieve default CRM leads"},
+ status_code=500
+ )
except Exception as e:
- logger.info(traceback.format_exc())
+ logger.error(f"Error in fetch_default_crm_leads: {str(e)}")
return JSONResponse(content={"message": str(e)}, status_code=500)
+
async def save_crm_lead_data(lead_id, file_path, translation, extracted_data, summary, user_id=None, transcription_id=None):
"""
diff --git a/service/summarizer.py b/service/summarizer.py
index 91fd78b..d1fd95b 100644
--- a/service/summarizer.py
+++ b/service/summarizer.py
@@ -48,7 +48,6 @@ def summarize_using_ollama(text):
summary = response["response"]
return summary
-
def _extract_first_json_block(text: str) -> dict | None:
"""Helper to find the first JSON block in a string."""
try: