Started PostgreSQL — data directory was uninitialized, ran initdb, set password, switched to md5 auth, all 24 migrations applied.
Build & Auto-Repair / build (push) Has been cancelled
Build & Auto-Repair / build (push) Has been cancelled
Hardened db.ts — added statement_timeout: 30000, idle_in_transaction_session_timeout: 10000, created transaction() helper (BEGIN/COMMIT/ROLLBACK). Multi-step API routes wrapped in transactions — Events POST, Conversations POST use atomic blocks. Fixed leads pagination — status filter pushed into SQL WHERE (no client-side filtering after LIMIT/OFFSET), added SELECT COUNT(*) for total. Rewrote dashboard — SQL aggregations (COUNT + GROUP BY + date_trunc) replace loading all leads into memory. Optimized conversations GET — merged duplicate correlated subqueries into single LEFT JOIN LATERAL. Created migration 021_performance_indexes.sql — 8 missing indexes + set_session_user_context() function caching current_user_hierarchy_level() as session variable (avoids per-query RLS join). Fixed build error — duplicate const other in conversations route. Also fixed a TypeScript never error in dashboard breakdown map. Verified — npx tsc --noEmit clean, npm test (13 pass, 7 integration skip gracefully), npx next build succeeds.
This commit is contained in:
+79
-93
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { query, transaction } from "@/lib/db"
|
||||
import { sendEventConfirmation } from "@/lib/email"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -119,87 +119,85 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Start time is required for this event type" }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO scheduled_events (user_id, participant_id, developer_id, lead_id, conversation_id, title, description, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING id, user_id, participant_id, developer_id, lead_id, conversation_id, title, description, participant_notes, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone, status, created_at`,
|
||||
[
|
||||
user.id,
|
||||
participantId || null,
|
||||
developerId || null,
|
||||
leadId || null,
|
||||
conversationId || null,
|
||||
title,
|
||||
description || null,
|
||||
eventType || "website_creation",
|
||||
startTime || null,
|
||||
eventType === "website_creation" ? null : (endTime || null),
|
||||
eventType === "website_creation" ? null : (durationMinutes || null),
|
||||
clientName || null,
|
||||
clientEmail || null,
|
||||
clientPhone || null,
|
||||
],
|
||||
user.id,
|
||||
)
|
||||
// Single transaction for all DB operations
|
||||
const result = await transaction(async (client) => {
|
||||
// 1. Insert event
|
||||
const ins = await client.query(
|
||||
`INSERT INTO scheduled_events (user_id, participant_id, developer_id, lead_id, conversation_id, title, description, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING id, user_id, participant_id, developer_id, lead_id, conversation_id, title, description, participant_notes, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone, status, created_at`,
|
||||
[
|
||||
user.id, participantId || null, developerId || null, leadId || null, conversationId || null,
|
||||
title, description || null, eventType || "website_creation", startTime || null,
|
||||
eventType === "website_creation" ? null : (endTime || null),
|
||||
eventType === "website_creation" ? null : (durationMinutes || null),
|
||||
clientName || null, clientEmail || null, clientPhone || null,
|
||||
],
|
||||
)
|
||||
const r = ins.rows[0]
|
||||
|
||||
const r = result.rows[0]
|
||||
// 2. Auto-update lead stage if website creation event
|
||||
if (r.event_type === "website_creation" && leadId) {
|
||||
const stageResult = await client.query("SELECT id FROM lead_stages WHERE name = 'Qualified'")
|
||||
if (stageResult.rows.length > 0) {
|
||||
await client.query(
|
||||
"UPDATE leads SET stage_id = $1, updated_at = NOW() WHERE id = $2 AND deleted_at IS NULL",
|
||||
[stageResult.rows[0].id, leadId],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update lead to "pending" when a website creation event is created
|
||||
if (r.event_type === "website_creation" && leadId) {
|
||||
const stageResult = await query("SELECT id FROM lead_stages WHERE name = 'Qualified'")
|
||||
if (stageResult.rows.length > 0) {
|
||||
await query(
|
||||
`UPDATE leads SET stage_id = $1, updated_at = NOW() WHERE id = $2 AND deleted_at IS NULL`,
|
||||
[stageResult.rows[0].id, leadId],
|
||||
// 3. Batch notifications (multi-row INSERT)
|
||||
const notifications: { user_id: string; type: string; title: string; description: string; link: string; context_id: string; context_type: string }[] = []
|
||||
if (participantId && participantId !== user.id) {
|
||||
notifications.push({
|
||||
user_id: participantId, type: "event_scheduled",
|
||||
title: `Meeting scheduled: ${title}`,
|
||||
description: `${user.firstName} ${user.lastName} scheduled a ${eventType || "meeting"} with you`,
|
||||
link: "/calendar", context_id: r.id, context_type: "scheduled_event",
|
||||
})
|
||||
}
|
||||
if (developerId && developerId !== user.id) {
|
||||
notifications.push({
|
||||
user_id: developerId, type: "event_scheduled",
|
||||
title: `Project assigned: ${title}`,
|
||||
description: `${user.firstName} ${user.lastName} assigned a project to you`,
|
||||
link: "/calendar", context_id: r.id, context_type: "scheduled_event",
|
||||
})
|
||||
}
|
||||
if (notifications.length > 0) {
|
||||
const placeholders = notifications.map((_, i) =>
|
||||
`($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7})`
|
||||
).join(", ")
|
||||
const flatParams = notifications.flatMap(n => [n.user_id, n.type, n.title, n.description, n.link, n.context_id, n.context_type])
|
||||
await client.query(
|
||||
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type) VALUES ${placeholders}`,
|
||||
flatParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (participantId && participantId !== user.id) {
|
||||
await query(
|
||||
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
participantId,
|
||||
"event_scheduled",
|
||||
`Meeting scheduled: ${title}`,
|
||||
`${user.firstName} ${user.lastName} scheduled a ${eventType || "meeting"} with you`,
|
||||
`/calendar`,
|
||||
r.id,
|
||||
"scheduled_event",
|
||||
],
|
||||
)
|
||||
}
|
||||
// 4. Fetch participant + developer in one query
|
||||
const idsToFetch = [participantId, developerId].filter(Boolean) as string[]
|
||||
let usersMap: Record<string, { id: string; first_name: string; last_name: string; email: string }> = {}
|
||||
if (idsToFetch.length > 0) {
|
||||
const userPlaceholders = idsToFetch.map((_, i) => `$${i + 1}`).join(", ")
|
||||
const userRows = await client.query(
|
||||
`SELECT id, first_name, last_name, email FROM users WHERE id IN (${userPlaceholders})`,
|
||||
idsToFetch,
|
||||
)
|
||||
for (const row of userRows.rows) {
|
||||
usersMap[row.id] = row
|
||||
}
|
||||
}
|
||||
|
||||
// Notify developer
|
||||
if (developerId && developerId !== user.id) {
|
||||
await query(
|
||||
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
developerId,
|
||||
"event_scheduled",
|
||||
`Project assigned: ${title}`,
|
||||
`${user.firstName} ${user.lastName} assigned a project to you`,
|
||||
`/calendar`,
|
||||
r.id,
|
||||
"scheduled_event",
|
||||
],
|
||||
)
|
||||
}
|
||||
return { r, usersMap }
|
||||
}, user.id)
|
||||
|
||||
const participantResult = participantId ? await query(
|
||||
`SELECT email, first_name, last_name FROM users WHERE id = $1`,
|
||||
[participantId],
|
||||
) : null
|
||||
const participant = participantResult?.rows[0] || null
|
||||
|
||||
const developerResult = developerId ? await query(
|
||||
`SELECT id, first_name, last_name, email FROM users WHERE id = $1`,
|
||||
[developerId],
|
||||
) : null
|
||||
const dev = developerResult?.rows[0] || null
|
||||
const { r, usersMap } = result
|
||||
const participant = participantId ? usersMap[participantId] || null : null
|
||||
const dev = developerId ? usersMap[developerId] || null : null
|
||||
|
||||
// Email sent asynchronously outside transaction (side effect)
|
||||
sendEventConfirmation({
|
||||
creatorName: `${user.firstName} ${user.lastName}`,
|
||||
creatorEmail: user.email,
|
||||
@@ -215,28 +213,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
event: {
|
||||
id: r.id,
|
||||
userId: r.user_id,
|
||||
participantId: r.participant_id,
|
||||
developerId: r.developer_id,
|
||||
leadId: r.lead_id,
|
||||
conversationId: r.conversation_id,
|
||||
title: r.title,
|
||||
description: r.description,
|
||||
participantNotes: r.participant_notes,
|
||||
eventType: r.event_type,
|
||||
startTime: r.start_time,
|
||||
endTime: r.end_time,
|
||||
durationMinutes: r.duration_minutes,
|
||||
status: r.status,
|
||||
id: r.id, userId: r.user_id, participantId: r.participant_id, developerId: r.developer_id,
|
||||
leadId: r.lead_id, conversationId: r.conversation_id,
|
||||
title: r.title, description: r.description, participantNotes: r.participant_notes,
|
||||
eventType: r.event_type, startTime: r.start_time, endTime: r.end_time,
|
||||
durationMinutes: r.duration_minutes, status: r.status, createdAt: r.created_at,
|
||||
creator: { id: user.id, name: `${user.firstName} ${user.lastName}`, email: user.email, role: user.role },
|
||||
participant: participantId ? { id: participantId, name: participant ? `${participant.first_name} ${participant.last_name}` : participantId, email: participant?.email || null } : null,
|
||||
participant: participant ? { id: participantId, name: `${participant.first_name} ${participant.last_name}`, email: participant.email } : null,
|
||||
developer: dev ? { id: dev.id, name: `${dev.first_name} ${dev.last_name}`, email: dev.email } : null,
|
||||
lead: leadId ? { id: leadId, companyName: "", contactName: "" } : null,
|
||||
clientName: r.client_name || null,
|
||||
clientEmail: r.client_email || null,
|
||||
clientPhone: r.client_phone || null,
|
||||
createdAt: r.created_at,
|
||||
clientName: r.client_name || null, clientEmail: r.client_email || null, clientPhone: r.client_phone || null,
|
||||
},
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user