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,125 @@
|
||||
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 entityType = searchParams.get("entityType")
|
||||
|
||||
let sql = `SELECT id, entity_type, field_key, field_label, field_type, options, placeholder, default_value, section, is_required, sort_order, is_active, created_at
|
||||
FROM custom_field_definitions WHERE is_active = true`
|
||||
const params: any[] = []
|
||||
|
||||
if (entityType) {
|
||||
sql += ` AND entity_type = $1`
|
||||
params.push(entityType)
|
||||
}
|
||||
|
||||
sql += ` ORDER BY entity_type, sort_order`
|
||||
|
||||
const result = await query(sql, params)
|
||||
return NextResponse.json(result.rows)
|
||||
} catch (error) {
|
||||
console.error("Custom fields API error:", error)
|
||||
return NextResponse.json({ error: "Failed to load custom fields" }, { 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()
|
||||
if (!body.entityType || !body.fieldKey || !body.fieldLabel) {
|
||||
return NextResponse.json({ error: "entityType, fieldKey, fieldLabel are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO custom_field_definitions (entity_type, field_key, field_label, field_type, options, placeholder, default_value, section, is_required, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
|
||||
[
|
||||
body.entityType,
|
||||
body.fieldKey,
|
||||
body.fieldLabel,
|
||||
body.fieldType || "text",
|
||||
body.options ? JSON.stringify(body.options) : null,
|
||||
body.placeholder || null,
|
||||
body.defaultValue || null,
|
||||
body.section || null,
|
||||
body.isRequired || false,
|
||||
body.sortOrder || 0,
|
||||
]
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, id: result.rows[0].id }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
if (error?.constraint === "uq_custom_field_entity_key") {
|
||||
return NextResponse.json({ error: "A field with this key already exists for this entity" }, { status: 409 })
|
||||
}
|
||||
console.error("Custom fields POST error:", error)
|
||||
return NextResponse.json({ error: "Failed to create custom field" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const body = await request.json()
|
||||
if (!body.id) return NextResponse.json({ error: "id required" }, { status: 400 })
|
||||
|
||||
const fields: string[] = []
|
||||
const values: any[] = []
|
||||
let idx = 1
|
||||
|
||||
const fieldMap: Record<string, string> = {
|
||||
fieldLabel: "field_label",
|
||||
fieldType: "field_type",
|
||||
options: "options",
|
||||
placeholder: "placeholder",
|
||||
defaultValue: "default_value",
|
||||
section: "section",
|
||||
isRequired: "is_required",
|
||||
sortOrder: "sort_order",
|
||||
isActive: "is_active",
|
||||
}
|
||||
|
||||
for (const [key, col] of Object.entries(fieldMap)) {
|
||||
if (body[key] !== undefined) {
|
||||
fields.push(`${col} = $${idx++}`)
|
||||
values.push(key === "options" && body[key] ? JSON.stringify(body[key]) : body[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.length === 0) return NextResponse.json({ error: "No fields to update" }, { status: 400 })
|
||||
|
||||
fields.push(`updated_at = NOW()`)
|
||||
values.push(body.id)
|
||||
|
||||
await query(`UPDATE custom_field_definitions SET ${fields.join(", ")} WHERE id = $${idx}`, values)
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Custom fields PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to update custom field" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
await getSessionUser()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const id = searchParams.get("id")
|
||||
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 })
|
||||
await query("DELETE FROM custom_field_definitions WHERE id = $1", [id])
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Custom fields DELETE error:", error)
|
||||
return NextResponse.json({ error: "Failed to delete custom field" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user