186bb9ea19
- Pagamenti: totale ereditato dalla somma accepted_total delle offerte attive (con override) + selettore schema rate 1/2/3 step; nuova colonna payments.percent per rescalare gli importi preservando label/stato - Fasi & Task: recomputePhaseStatus auto-cascade (task -> fase) su updateTaskStatus/addTask, fix UI stale, etichette Da iniziare/In corso/Completata - Pagina pubblica: colori fasi (grigio/blu/#1A463C) + fasi collassabili - Transcript del cliente visibili in tab Documenti admin e dashboard pubblica - Timer: inserimento manuale (+30/+60, minuti liberi, data) + elimina entry - CLAUDE.md: procedura Deploy & DB Access; push su main automatico Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
232 lines
5.9 KiB
TypeScript
232 lines
5.9 KiB
TypeScript
import { eq, and, isNull, desc, gte, lte, ilike, or } from "drizzle-orm";
|
|
import { db } from "@/db";
|
|
import { leads, activities, reminders, quotes, clientTranscripts } from "@/db/schema";
|
|
|
|
// Get all leads
|
|
export async function getAllLeads(limit: number = 100, offset: number = 0) {
|
|
return await db
|
|
.select()
|
|
.from(leads)
|
|
.orderBy(desc(leads.updated_at))
|
|
.limit(limit)
|
|
.offset(offset);
|
|
}
|
|
|
|
// Get lead by ID
|
|
export async function getLeadById(id: string) {
|
|
const [lead] = await db
|
|
.select()
|
|
.from(leads)
|
|
.where(eq(leads.id, id));
|
|
return lead;
|
|
}
|
|
|
|
// Get leads by stage
|
|
export async function getLeadsByStage(stage: string) {
|
|
return await db
|
|
.select()
|
|
.from(leads)
|
|
.where(eq(leads.status, stage))
|
|
.orderBy(desc(leads.last_contact_date));
|
|
}
|
|
|
|
// Get leads needing follow-up
|
|
export async function getLeadsNeedingFollowUp(daysAgo: number = 7) {
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysAgo);
|
|
|
|
return await db
|
|
.select()
|
|
.from(leads)
|
|
.where(
|
|
or(
|
|
isNull(leads.last_contact_date),
|
|
lte(leads.last_contact_date, cutoffDate)
|
|
)
|
|
)
|
|
.orderBy(leads.created_at);
|
|
}
|
|
|
|
// Get activity log
|
|
export async function getActivityLog(leadId: string) {
|
|
return await db
|
|
.select()
|
|
.from(activities)
|
|
.where(eq(activities.lead_id, leadId))
|
|
.orderBy(desc(activities.activity_date));
|
|
}
|
|
|
|
// Get upcoming reminders
|
|
export async function getUpcomingReminders(leadId: string) {
|
|
return await db
|
|
.select()
|
|
.from(reminders)
|
|
.where(and(eq(reminders.lead_id, leadId), isNull(reminders.completed_at)))
|
|
.orderBy(reminders.due_date);
|
|
}
|
|
|
|
// Get overdue reminders
|
|
export async function getOverdueReminders() {
|
|
const now = new Date();
|
|
return await db
|
|
.select()
|
|
.from(reminders)
|
|
.where(and(lte(reminders.due_date, now), isNull(reminders.completed_at)))
|
|
.orderBy(reminders.due_date);
|
|
}
|
|
|
|
// Create activity and auto-update last_contact_date
|
|
export async function createActivity(data: {
|
|
lead_id: string;
|
|
type: string;
|
|
activity_date: Date | string;
|
|
duration_minutes?: number;
|
|
notes: string;
|
|
}) {
|
|
const activityDate =
|
|
typeof data.activity_date === "string"
|
|
? new Date(data.activity_date)
|
|
: data.activity_date;
|
|
|
|
const [activity] = await db
|
|
.insert(activities)
|
|
.values({
|
|
lead_id: data.lead_id,
|
|
type: data.type,
|
|
activity_date: activityDate,
|
|
duration_minutes: data.duration_minutes,
|
|
notes: data.notes,
|
|
})
|
|
.returning();
|
|
|
|
// Auto-update lead.last_contact_date
|
|
await db
|
|
.update(leads)
|
|
.set({ last_contact_date: activityDate, updated_at: new Date() })
|
|
.where(eq(leads.id, data.lead_id));
|
|
|
|
return activity;
|
|
}
|
|
|
|
// Update lead stage
|
|
export async function updateLeadStage(leadId: string, stage: string) {
|
|
const [updated] = await db
|
|
.update(leads)
|
|
.set({ status: stage, updated_at: new Date() })
|
|
.where(eq(leads.id, leadId))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
// Update lead
|
|
export async function updateLead(
|
|
leadId: string,
|
|
data: Partial<{
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
company: string;
|
|
notes: string;
|
|
next_action: string;
|
|
next_action_date: Date | string;
|
|
}>
|
|
) {
|
|
const updateData: Record<string, any> = { updated_at: new Date() };
|
|
|
|
if (data.name !== undefined) updateData.name = data.name;
|
|
if (data.email !== undefined) updateData.email = data.email;
|
|
if (data.phone !== undefined) updateData.phone = data.phone;
|
|
if (data.company !== undefined) updateData.company = data.company;
|
|
if (data.notes !== undefined) updateData.notes = data.notes;
|
|
if (data.next_action !== undefined) updateData.next_action = data.next_action;
|
|
if (data.next_action_date !== undefined) {
|
|
updateData.next_action_date =
|
|
typeof data.next_action_date === "string"
|
|
? new Date(data.next_action_date)
|
|
: data.next_action_date;
|
|
}
|
|
|
|
const [updated] = await db
|
|
.update(leads)
|
|
.set(updateData)
|
|
.where(eq(leads.id, leadId))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
// Search leads
|
|
export async function searchLeads(query: string) {
|
|
return await db
|
|
.select()
|
|
.from(leads)
|
|
.where(
|
|
or(
|
|
ilike(leads.name, `%${query}%`),
|
|
ilike(leads.email, `%${query}%`),
|
|
ilike(leads.company, `%${query}%`)
|
|
)
|
|
)
|
|
.orderBy(desc(leads.updated_at));
|
|
}
|
|
|
|
// Create reminder
|
|
export async function createReminder(data: {
|
|
lead_id: string;
|
|
title: string;
|
|
description?: string;
|
|
due_date: Date | string;
|
|
}) {
|
|
const dueDate =
|
|
typeof data.due_date === "string" ? new Date(data.due_date) : data.due_date;
|
|
|
|
const [reminder] = await db
|
|
.insert(reminders)
|
|
.values({
|
|
lead_id: data.lead_id,
|
|
title: data.title,
|
|
description: data.description,
|
|
due_date: dueDate,
|
|
})
|
|
.returning();
|
|
return reminder;
|
|
}
|
|
|
|
// Complete reminder
|
|
export async function completeReminder(reminderId: string) {
|
|
const [updated] = await db
|
|
.update(reminders)
|
|
.set({ completed_at: new Date() })
|
|
.where(eq(reminders.id, reminderId))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
// Get quotes by lead
|
|
export async function getQuotesByLeadId(leadId: string) {
|
|
return await db
|
|
.select()
|
|
.from(quotes)
|
|
.where(eq(quotes.lead_id, leadId))
|
|
.orderBy(desc(quotes.created_at));
|
|
}
|
|
|
|
// Get transcript log for a lead — call_date DESC (D-05)
|
|
// Returns all fields including full content (Phase 21 reads them in bulk).
|
|
export async function getTranscripts(leadId: string) {
|
|
return await db
|
|
.select()
|
|
.from(clientTranscripts)
|
|
.where(eq(clientTranscripts.lead_id, leadId))
|
|
.orderBy(desc(clientTranscripts.call_date));
|
|
}
|
|
|
|
// Get transcripts linked to a client (post lead→client conversion).
|
|
// Returns all fields — used by admin DocumentsTab and public TranscriptsSection.
|
|
export async function getTranscriptsByClientId(clientId: string) {
|
|
return await db
|
|
.select()
|
|
.from(clientTranscripts)
|
|
.where(eq(clientTranscripts.client_id, clientId))
|
|
.orderBy(desc(clientTranscripts.call_date));
|
|
}
|