Kanban board, Project Management, Proposals/Quotes, Time Tracking, Custom Fields
Build & Auto-Repair / build (push) Has been cancelled
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
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const search = searchParams.get("search") || ""
|
||||
const statusId = searchParams.get("status") || ""
|
||||
|
||||
let sql = `SELECT p.id, p.title, p.proposal_number, p.subtotal, p.total_amount, p.valid_until, p.created_at,
|
||||
ps.name AS status_name, ps.color AS status_color,
|
||||
u.first_name || ' ' || u.last_name AS created_by_name
|
||||
FROM proposals p
|
||||
JOIN proposal_statuses ps ON ps.id = p.status_id
|
||||
JOIN users u ON u.id = p.created_by
|
||||
WHERE p.deleted_at IS NULL`
|
||||
|
||||
const params: any[] = []
|
||||
let idx = 1
|
||||
|
||||
if (search) {
|
||||
sql += ` AND (p.title ILIKE $${idx} OR p.proposal_number ILIKE $${idx})`
|
||||
params.push(`%${search}%`)
|
||||
idx++
|
||||
}
|
||||
if (statusId) {
|
||||
sql += ` AND p.status_id = $${idx}`
|
||||
params.push(statusId)
|
||||
idx++
|
||||
}
|
||||
|
||||
sql += ` ORDER BY p.created_at DESC LIMIT 50`
|
||||
|
||||
const result = await query(sql, params)
|
||||
return NextResponse.json(result.rows)
|
||||
} catch (error) {
|
||||
console.error("Proposals API error:", error)
|
||||
return NextResponse.json({ error: "Failed to load proposals" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const body = await request.json()
|
||||
const draftStatus = await query("SELECT id FROM proposal_statuses WHERE name = 'Draft'")
|
||||
const statusId = draftStatus.rows[0]?.id
|
||||
|
||||
const numResult = await query("SELECT COALESCE(MAX(SUBSTRING(proposal_number FROM 'Q-(\\d+)')::INT), 0) + 1 AS next_num FROM proposals WHERE deleted_at IS NULL")
|
||||
const nextNum = numResult.rows[0]?.next_num || 1
|
||||
const proposalNumber = `Q-${String(nextNum).padStart(4, "0")}`
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO proposals (lead_id, customer_id, title, proposal_number, status_id, content, subtotal, tax_percent, tax_amount, total_amount, valid_until, notes, terms, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id`,
|
||||
[
|
||||
body.leadId || null,
|
||||
body.customerId || null,
|
||||
body.title,
|
||||
proposalNumber,
|
||||
statusId,
|
||||
body.content ? JSON.stringify(body.content) : null,
|
||||
body.subtotal || 0,
|
||||
body.taxPercent !== undefined ? body.taxPercent : 15,
|
||||
body.taxAmount || 0,
|
||||
body.totalAmount || 0,
|
||||
body.validUntil || null,
|
||||
body.notes || null,
|
||||
body.terms || null,
|
||||
user.id,
|
||||
]
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, id: result.rows[0].id, proposalNumber }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("Proposals POST error:", error)
|
||||
return NextResponse.json({ error: "Failed to create proposal" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user