545065b527
Build & Auto-Repair / build (push) Has been cancelled
- Kanban board with drag-and-drop per stage, colour-coded cards, score bars - Project Management with milestones, Gantt timeline, inline add milestone/task - Proposal/Quote builder with line items, VAT, terms, e-signature capture - Time Tracking with stopwatch, manual entry, hours-by-project chart - Custom Fields system: admin-defined fields per entity (leads/customers/ projects/opportunities) with drag-and-drop reordering in settings - Reusable CustomFieldsSection component for entity detail pages - Customers API endpoint - All builds pass with zero errors
165 lines
5.9 KiB
TypeScript
165 lines
5.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server"
|
|
import { getSessionUser } from "@/lib/auth"
|
|
import { query } from "@/lib/db"
|
|
import { avatarSvgUrl } from "@/lib/avatar"
|
|
|
|
function stageToStatus(name: string): string {
|
|
switch (name) {
|
|
case "New": return "open"
|
|
case "Contacted": return "contacted"
|
|
case "Qualified":
|
|
case "Interested":
|
|
case "Demo Scheduled":
|
|
case "Negotiation": return "pending"
|
|
case "Closed Won": return "closed"
|
|
case "Closed Lost": return "ignored"
|
|
default: return "open"
|
|
}
|
|
}
|
|
|
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
const user = await getSessionUser()
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
|
|
const { id } = await params
|
|
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
|
|
|
const result = await query(
|
|
`SELECT l.id, l.company_name, l.contact_name, l.email, l.phone, l.score,
|
|
l.assigned_to, l.created_at, l.updated_at, l.notes, l.source_id,
|
|
l.custom_fields,
|
|
ls.name AS stage_name,
|
|
u.id AS user_id, u.first_name, u.last_name, u.email AS user_email, u.avatar_url
|
|
FROM leads l
|
|
JOIN lead_stages ls ON ls.id = l.stage_id
|
|
LEFT JOIN users u ON u.id = l.assigned_to
|
|
WHERE l.id = $1 AND l.deleted_at IS NULL
|
|
AND ($2 = true OR l.assigned_to = $3)`,
|
|
[id, isAdmin, user.id]
|
|
)
|
|
|
|
if (result.rows.length === 0) {
|
|
return NextResponse.json({ error: "Lead not found" }, { status: 404 })
|
|
}
|
|
|
|
const r = result.rows[0]
|
|
const lead = {
|
|
id: r.id,
|
|
companyName: r.company_name || "",
|
|
contactName: r.contact_name,
|
|
email: r.email || "",
|
|
phone: r.phone || "",
|
|
source: "",
|
|
description: r.notes || "",
|
|
status: stageToStatus(r.stage_name),
|
|
assignedUserId: r.assigned_to,
|
|
assignedUser: r.assigned_to ? {
|
|
id: r.user_id,
|
|
name: `${r.first_name} ${r.last_name}`,
|
|
email: r.user_email,
|
|
avatar: avatarSvgUrl(`${r.first_name} ${r.last_name}`),
|
|
} : null,
|
|
customFields: r.custom_fields || {},
|
|
createdAt: r.created_at,
|
|
updatedAt: r.updated_at,
|
|
}
|
|
|
|
return NextResponse.json(lead)
|
|
} catch (error) {
|
|
console.error("Lead detail API error:", error)
|
|
return NextResponse.json({ error: "Failed to load lead" }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
function statusToStageName(status: string): string {
|
|
switch (status) {
|
|
case "open": return "New"
|
|
case "contacted": return "Contacted"
|
|
case "pending": return "Qualified"
|
|
case "closed": return "Closed Won"
|
|
case "ignored": return "Closed Lost"
|
|
default: return "New"
|
|
}
|
|
}
|
|
|
|
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
const user = await getSessionUser()
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
|
|
const { id } = await params
|
|
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
|
|
|
// Verify access
|
|
const accessCheck = await query(
|
|
`SELECT id FROM leads WHERE id = $1 AND deleted_at IS NULL
|
|
AND ($2 = true OR assigned_to = $3)`,
|
|
[id, isAdmin, user.id]
|
|
)
|
|
if (accessCheck.rows.length === 0) {
|
|
return NextResponse.json({ error: "Lead not found" }, { status: 404 })
|
|
}
|
|
|
|
const body = await request.json()
|
|
const fields: string[] = []
|
|
const values: any[] = []
|
|
let idx = 1
|
|
|
|
if (body.companyName !== undefined) { fields.push(`company_name = $${idx++}`); values.push(body.companyName) }
|
|
if (body.contactName !== undefined) { fields.push(`contact_name = $${idx++}`); values.push(body.contactName) }
|
|
if (body.email !== undefined) { fields.push(`email = $${idx++}`); values.push(body.email) }
|
|
if (body.phone !== undefined) { fields.push(`phone = $${idx++}`); values.push(body.phone) }
|
|
if (body.description !== undefined) { fields.push(`notes = $${idx++}`); values.push(body.description) }
|
|
if (body.source !== undefined) { fields.push(`source_id = $${idx++}`); values.push(body.source) }
|
|
if (body.assignedUserId !== undefined) {
|
|
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
|
if (!isAdmin) {
|
|
// non-admin cannot reassign
|
|
return NextResponse.json({ error: "Only admins can reassign leads" }, { status: 403 })
|
|
}
|
|
fields.push(`assigned_to = $${idx++}`)
|
|
values.push(body.assignedUserId === "none" ? null : body.assignedUserId)
|
|
}
|
|
if (body.status !== undefined) {
|
|
const stageName = statusToStageName(body.status)
|
|
const stageResult = await query("SELECT id FROM lead_stages WHERE name = $1", [stageName])
|
|
if (stageResult.rows.length > 0) {
|
|
fields.push(`stage_id = $${idx++}`)
|
|
values.push(stageResult.rows[0].id)
|
|
}
|
|
}
|
|
if (body.score !== undefined) {
|
|
const score = Number(body.score)
|
|
if (!isFinite(score) || score < 0 || score > 100) {
|
|
return NextResponse.json({ error: "Score must be a number between 0 and 100" }, { status: 400 })
|
|
}
|
|
fields.push(`score = $${idx++}`)
|
|
values.push(score)
|
|
}
|
|
if (body.customFields !== undefined) {
|
|
fields.push(`custom_fields = $${idx++}`)
|
|
values.push(JSON.stringify(body.customFields))
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return NextResponse.json({ error: "No fields to update" }, { status: 400 })
|
|
}
|
|
|
|
fields.push(`updated_at = NOW()`)
|
|
values.push(id)
|
|
|
|
const sql = `UPDATE leads SET ${fields.join(", ")} WHERE id = $${idx} AND deleted_at IS NULL RETURNING id`
|
|
const result = await query(sql, values)
|
|
|
|
if (result.rows.length === 0) {
|
|
return NextResponse.json({ error: "Lead not found" }, { status: 404 })
|
|
}
|
|
|
|
return NextResponse.json({ success: true, id: result.rows[0].id })
|
|
} catch (error) {
|
|
console.error("Lead PATCH error:", error)
|
|
return NextResponse.json({ error: "Failed to update lead" }, { status: 500 })
|
|
}
|
|
}
|