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.email} +
+
+ + +
+ + View in CRM + +
+ + +
+ + {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} +
+
+ CRM URL: + + View in CRM + + +
+
+
+ +
+

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: