import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; const opportunityInput = v.object({ url: v.string(), platform: v.string(), title: v.string(), snippet: v.string(), relevanceScore: v.number(), intent: v.string(), suggestedApproach: v.string(), matchedKeywords: v.array(v.string()), matchedProblems: v.array(v.string()), softPitch: v.boolean(), }); export const listByProject = query({ args: { projectId: v.id("projects"), status: v.optional(v.string()), intent: v.optional(v.string()), minScore: v.optional(v.number()), limit: v.optional(v.number()), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) return []; const project = await ctx.db.get(args.projectId); if (!project || project.userId !== userId) return []; const limit = args.limit ?? 50; let queryBuilder = args.status ? ctx.db .query("opportunities") .withIndex("by_project_status", (q) => q.eq("projectId", args.projectId).eq("status", args.status!) ) : ctx.db .query("opportunities") .withIndex("by_project_createdAt", (q) => q.eq("projectId", args.projectId) ); if (args.intent) { queryBuilder = queryBuilder.filter((q) => q.eq(q.field("intent"), args.intent) ); } if (args.minScore !== undefined) { queryBuilder = queryBuilder.filter((q) => q.gte(q.field("relevanceScore"), args.minScore) ); } const results = await queryBuilder.order("desc").collect(); return results.slice(0, limit); }, }); export const upsertBatch = mutation({ args: { projectId: v.id("projects"), analysisId: v.optional(v.id("analyses")), opportunities: v.array(opportunityInput), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Unauthorized"); const project = await ctx.db.get(args.projectId); if (!project || project.userId !== userId) { throw new Error("Project not found or unauthorized"); } let created = 0; let updated = 0; const now = Date.now(); for (const opp of args.opportunities) { const existing = await ctx.db .query("opportunities") .withIndex("by_project_url", (q) => q.eq("projectId", args.projectId).eq("url", opp.url) ) .first(); if (existing) { await ctx.db.patch(existing._id, { analysisId: args.analysisId, platform: opp.platform, title: opp.title, snippet: opp.snippet, relevanceScore: opp.relevanceScore, intent: opp.intent, suggestedApproach: opp.suggestedApproach, matchedKeywords: opp.matchedKeywords, matchedProblems: opp.matchedProblems, softPitch: opp.softPitch, updatedAt: now, }); updated += 1; } else { await ctx.db.insert("opportunities", { projectId: args.projectId, analysisId: args.analysisId, url: opp.url, platform: opp.platform, title: opp.title, snippet: opp.snippet, relevanceScore: opp.relevanceScore, intent: opp.intent, status: "new", suggestedApproach: opp.suggestedApproach, matchedKeywords: opp.matchedKeywords, matchedProblems: opp.matchedProblems, softPitch: opp.softPitch, createdAt: now, updatedAt: now, }); created += 1; } const seenExisting = await ctx.db .query("seenUrls") .withIndex("by_project_url", (q) => q.eq("projectId", args.projectId).eq("url", opp.url) ) .first(); if (seenExisting) { await ctx.db.patch(seenExisting._id, { lastSeenAt: now, source: "opportunities", }); } else { await ctx.db.insert("seenUrls", { projectId: args.projectId, url: opp.url, firstSeenAt: now, lastSeenAt: now, source: "opportunities", }); } } return { created, updated }; }, }); export const updateStatus = mutation({ args: { id: v.id("opportunities"), status: v.string(), notes: v.optional(v.string()), tags: v.optional(v.array(v.string())), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Unauthorized"); const opportunity = await ctx.db.get(args.id); if (!opportunity) throw new Error("Opportunity not found"); const project = await ctx.db.get(opportunity.projectId); if (!project || project.userId !== userId) { throw new Error("Project not found or unauthorized"); } await ctx.db.patch(args.id, { status: args.status, notes: args.notes, tags: args.tags, updatedAt: Date.now(), }); }, });