Current state
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { chatWithAI } from "@/lib/ai"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
if (!["sales", "admin", "super_admin"].includes(user.role)) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { message } = await request.json()
|
||||
if (!message || typeof message !== "string") {
|
||||
return NextResponse.json({ error: "Message is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Forward the JWT from the session cookie to the Rust backend
|
||||
const sessionCookie = request.cookies.get("session")?.value
|
||||
if (!sessionCookie) return NextResponse.json({ error: "No session" }, { status: 401 })
|
||||
|
||||
const response = await chatWithAI(message, sessionCookie)
|
||||
|
||||
return NextResponse.json({ response })
|
||||
} catch (error) {
|
||||
console.error("AI chat error:", error)
|
||||
return NextResponse.json({ error: "AI service unavailable" }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { fetchJobs } from "@/lib/ai"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
if (!["sales", "admin", "super_admin"].includes(user.role)) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const jobs = await fetchJobs()
|
||||
return NextResponse.json({ jobs })
|
||||
} catch {
|
||||
console.warn("Failed to fetch AI jobs in API route")
|
||||
return NextResponse.json({ jobs: [] })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
export async function GET() {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get("session")?.value
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "No session" }, { status: 401 })
|
||||
}
|
||||
return NextResponse.json({ token })
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import {
|
||||
comparePassword,
|
||||
getUserByEmail,
|
||||
getUserByUsername,
|
||||
mapDbUserToSessionUser,
|
||||
recordLoginAttempt,
|
||||
incrementFailedAttempts,
|
||||
resetFailedAttempts,
|
||||
isAccountLocked,
|
||||
createSession,
|
||||
} from "@/lib/auth"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, username, password } = await request.json()
|
||||
|
||||
const credential = email || username
|
||||
|
||||
if (!credential || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: "Email/Username and password are required." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (credential.trim().length === 0 || password.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Credentials cannot be empty." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"127.0.0.1"
|
||||
|
||||
const userAgent = request.headers.get("user-agent") || null
|
||||
|
||||
// Try to find user by email first, then by username
|
||||
let dbUser =
|
||||
email || credential.includes("@")
|
||||
? await getUserByEmail(credential)
|
||||
: null
|
||||
|
||||
if (!dbUser) {
|
||||
dbUser = await getUserByUsername(credential)
|
||||
}
|
||||
|
||||
if (!dbUser) {
|
||||
await recordLoginAttempt(
|
||||
null,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
false,
|
||||
"User not found"
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email/username or password." },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const lockStatus = await isAccountLocked(dbUser)
|
||||
if (lockStatus.locked) {
|
||||
await recordLoginAttempt(
|
||||
dbUser.id,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
false,
|
||||
lockStatus.reason
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: lockStatus.reason },
|
||||
{ status: 423 }
|
||||
)
|
||||
}
|
||||
|
||||
const valid = await comparePassword(password, dbUser.password_hash)
|
||||
if (!valid) {
|
||||
await incrementFailedAttempts(dbUser.id)
|
||||
await recordLoginAttempt(
|
||||
dbUser.id,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
false,
|
||||
"Invalid password"
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email/username or password." },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
await resetFailedAttempts(dbUser.id)
|
||||
await recordLoginAttempt(
|
||||
dbUser.id,
|
||||
credential,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
true
|
||||
)
|
||||
|
||||
await createSession(dbUser.id, dbUser.role_name)
|
||||
|
||||
const user = mapDbUserToSessionUser(dbUser)
|
||||
|
||||
return NextResponse.json({ user }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication service unavailable." },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { destroySession } from "@/lib/auth"
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
await destroySession()
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Logout failed." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
|
||||
}
|
||||
return NextResponse.json({ user }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Auth me error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication service unavailable." },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import {
|
||||
getSessionUser,
|
||||
decryptPassword,
|
||||
setSessionContext,
|
||||
} from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) {
|
||||
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
|
||||
}
|
||||
if (sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Only SUPER_ADMIN can recover passwords." }, { status: 403 })
|
||||
}
|
||||
|
||||
const { userId } = await request.json()
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "userId is required." }, { status: 400 })
|
||||
}
|
||||
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"127.0.0.1"
|
||||
|
||||
await setSessionContext(sessionUser.id, ipAddress)
|
||||
|
||||
const result = await query(
|
||||
`SELECT id, username, email, first_name, last_name, password_encrypted
|
||||
FROM users WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[userId]
|
||||
)
|
||||
|
||||
const user = result.rows[0]
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found." }, { status: 404 })
|
||||
}
|
||||
|
||||
if (!user.password_encrypted) {
|
||||
return NextResponse.json({ error: "No encrypted password stored for this user." }, { status: 404 })
|
||||
}
|
||||
|
||||
const plaintextPassword = await decryptPassword(user.password_encrypted)
|
||||
if (!plaintextPassword) {
|
||||
return NextResponse.json({ error: "Failed to decrypt password. Master key may have changed." }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: `${user.first_name} ${user.last_name}`,
|
||||
},
|
||||
password: plaintextPassword,
|
||||
}, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Password recovery error:", error)
|
||||
return NextResponse.json({ error: "Recovery service unavailable." }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { query } from "@/lib/db"
|
||||
import { getSessionUser, setSessionContext } from "@/lib/auth"
|
||||
|
||||
export async function PATCH(request: NextRequest, { params: routeParams }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) {
|
||||
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
|
||||
}
|
||||
if (sessionUser.role !== "admin" && sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden. Only admins can update bug reports." }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await routeParams
|
||||
const { status, assigned_to, resolution_notes } = await request.json()
|
||||
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"127.0.0.1"
|
||||
|
||||
await setSessionContext(sessionUser.id, ipAddress)
|
||||
|
||||
const validStatuses = ["open", "in_progress", "resolved", "closed"]
|
||||
if (status && !validStatuses.includes(status)) {
|
||||
return NextResponse.json({ error: "Invalid status." }, { status: 400 })
|
||||
}
|
||||
|
||||
const existing = await query("SELECT id, status FROM bug_reports WHERE id = $1", [id])
|
||||
if (existing.rows.length === 0) {
|
||||
return NextResponse.json({ error: "Bug report not found." }, { status: 404 })
|
||||
}
|
||||
|
||||
const updates: string[] = []
|
||||
const values: unknown[] = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (status !== undefined) {
|
||||
updates.push(`status = $${paramIndex++}`)
|
||||
values.push(status)
|
||||
}
|
||||
if (assigned_to !== undefined) {
|
||||
updates.push(`assigned_to = $${paramIndex++}`)
|
||||
values.push(assigned_to === "null" ? null : assigned_to)
|
||||
}
|
||||
if (resolution_notes !== undefined) {
|
||||
updates.push(`resolution_notes = $${paramIndex++}`)
|
||||
values.push(resolution_notes)
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return NextResponse.json({ error: "No fields to update." }, { status: 400 })
|
||||
}
|
||||
|
||||
updates.push(`updated_at = NOW()`)
|
||||
values.push(id)
|
||||
|
||||
await query(
|
||||
`UPDATE bug_reports SET ${updates.join(", ")} WHERE id = $${paramIndex}`,
|
||||
values
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error updating bug report:", error)
|
||||
return NextResponse.json({ error: "Failed to update bug report." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { query } from "@/lib/db"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) {
|
||||
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
|
||||
}
|
||||
|
||||
const { title, description, severity, page_url, screenshot_url } = await request.json()
|
||||
|
||||
if (!title || !description) {
|
||||
return NextResponse.json({ error: "Title and description are required." }, { status: 400 })
|
||||
}
|
||||
|
||||
const validSeverities = ["low", "medium", "high", "critical"]
|
||||
if (severity && !validSeverities.includes(severity)) {
|
||||
return NextResponse.json({ error: "Invalid severity." }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO bug_reports (reported_by, title, description, severity, page_url, screenshot_url)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_at`,
|
||||
[sessionUser.id, title, description, severity || "medium", page_url || null, screenshot_url || null]
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id: result.rows[0].id,
|
||||
created_at: result.rows[0].created_at,
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("Error creating bug report:", error)
|
||||
return NextResponse.json({ error: "Failed to submit bug report." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) {
|
||||
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
|
||||
}
|
||||
if (sessionUser.role !== "admin" && sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden. Only admins can view bug reports." }, { status: 403 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const status = searchParams.get("status")
|
||||
const severity = searchParams.get("severity")
|
||||
const limit = parseInt(searchParams.get("limit") || "50", 10)
|
||||
const offset = parseInt(searchParams.get("offset") || "0", 10)
|
||||
|
||||
let sql = `SELECT br.id, br.title, br.description, br.severity, br.page_url,
|
||||
br.screenshot_url, br.status, br.resolution_notes,
|
||||
br.created_at, br.updated_at,
|
||||
reporter.id AS reporter_id,
|
||||
reporter.first_name AS reporter_first_name,
|
||||
reporter.last_name AS reporter_last_name,
|
||||
reporter.email AS reporter_email,
|
||||
assignee.id AS assignee_id,
|
||||
assignee.first_name AS assignee_first_name,
|
||||
assignee.last_name AS assignee_last_name
|
||||
FROM bug_reports br
|
||||
JOIN users reporter ON reporter.id = br.reported_by
|
||||
LEFT JOIN users assignee ON assignee.id = br.assigned_to`
|
||||
const params: unknown[] = []
|
||||
const conditions: string[] = []
|
||||
|
||||
if (status) {
|
||||
conditions.push(`br.status = $${params.length + 1}`)
|
||||
params.push(status)
|
||||
}
|
||||
if (severity) {
|
||||
conditions.push(`br.severity = $${params.length + 1}`)
|
||||
params.push(severity)
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
sql += " WHERE " + conditions.join(" AND ")
|
||||
}
|
||||
|
||||
sql += " ORDER BY br.created_at DESC LIMIT $" + (params.length + 1) + " OFFSET $" + (params.length + 2)
|
||||
params.push(limit, offset)
|
||||
|
||||
const result = await query(sql, params)
|
||||
|
||||
const reports = result.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
severity: row.severity,
|
||||
page_url: row.page_url,
|
||||
screenshot_url: row.screenshot_url,
|
||||
status: row.status,
|
||||
resolution_notes: row.resolution_notes,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
reporter: {
|
||||
id: row.reporter_id,
|
||||
name: `${row.reporter_first_name} ${row.reporter_last_name}`,
|
||||
email: row.reporter_email,
|
||||
},
|
||||
assignee: row.assignee_id ? {
|
||||
id: row.assignee_id,
|
||||
name: `${row.assignee_first_name} ${row.assignee_last_name}`,
|
||||
} : null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ reports }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error fetching bug reports:", error)
|
||||
return NextResponse.json({ error: "Failed to fetch bug reports." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
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 { searchParams } = new URL(_request.url)
|
||||
const limit = parseInt(searchParams.get("limit") || "100", 10)
|
||||
const offset = parseInt(searchParams.get("offset") || "0", 10)
|
||||
const before = searchParams.get("before") || ""
|
||||
|
||||
// Verify user is a participant
|
||||
const partCheck = await query(
|
||||
`SELECT 1 FROM conversation_participants WHERE conversation_id = $1 AND user_id = $2`,
|
||||
[id, user.id]
|
||||
)
|
||||
if (partCheck.rows.length === 0) {
|
||||
return NextResponse.json({ error: "Not a participant" }, { status: 403 })
|
||||
}
|
||||
|
||||
let msgSql = `SELECT m.id, m.sender_id, m.content, m.created_at, m.updated_at, m.deleted_at,
|
||||
u.first_name || ' ' || u.last_name AS sender_name,
|
||||
u.email AS sender_email,
|
||||
u.avatar_url AS sender_avatar_url
|
||||
FROM messages m
|
||||
JOIN users u ON u.id = m.sender_id
|
||||
WHERE m.conversation_id = $1 AND m.deleted_at IS NULL`
|
||||
const msgParams: any[] = [id]
|
||||
|
||||
if (before) {
|
||||
msgSql += ` AND m.created_at < $2`
|
||||
msgParams.push(before)
|
||||
}
|
||||
|
||||
msgSql += ` ORDER BY m.created_at ASC`
|
||||
msgSql += ` LIMIT $${msgParams.length + 1} OFFSET $${msgParams.length + 2}`
|
||||
msgParams.push(limit, offset)
|
||||
|
||||
const [msgResult, otherReadResult] = await Promise.all([
|
||||
query(msgSql, msgParams),
|
||||
query(
|
||||
`SELECT last_read_at FROM conversation_participants
|
||||
WHERE conversation_id = $1 AND user_id != $2`,
|
||||
[id, user.id],
|
||||
),
|
||||
])
|
||||
|
||||
const otherLastReadAt = otherReadResult.rows[0]?.last_read_at
|
||||
? new Date(otherReadResult.rows[0].last_read_at).getTime()
|
||||
: 0
|
||||
|
||||
const messages = msgResult.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
conversationId: id,
|
||||
senderId: row.sender_id,
|
||||
senderName: row.sender_name,
|
||||
senderAvatar: avatarSvgUrl(row.sender_name),
|
||||
content: row.content,
|
||||
timestamp: formatTime(new Date(row.created_at)),
|
||||
createdAt: row.created_at,
|
||||
read: row.sender_id === user.id
|
||||
? new Date(row.created_at).getTime() <= otherLastReadAt
|
||||
: true,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ messages })
|
||||
} catch (error) {
|
||||
console.error("Messages error:", error)
|
||||
return NextResponse.json({ error: "Failed to load messages" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
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
|
||||
|
||||
// Verify user is a participant
|
||||
const partCheck = await query(
|
||||
`SELECT 1 FROM conversation_participants WHERE conversation_id = $1 AND user_id = $2`,
|
||||
[id, user.id]
|
||||
)
|
||||
if (partCheck.rows.length === 0) {
|
||||
return NextResponse.json({ error: "Not a participant" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { content } = await request.json()
|
||||
if (!content?.trim()) {
|
||||
return NextResponse.json({ error: "Message content is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO messages (conversation_id, sender_id, content)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, created_at`,
|
||||
[id, user.id, content.trim()],
|
||||
)
|
||||
|
||||
await query(
|
||||
`UPDATE conversations SET updated_at = NOW() WHERE id = $1`,
|
||||
[id],
|
||||
)
|
||||
|
||||
const msg = result.rows[0]
|
||||
const senderName = `${user.firstName} ${user.lastName}`
|
||||
|
||||
const otherResult = await query(
|
||||
`SELECT user_id FROM conversation_participants
|
||||
WHERE conversation_id = $1 AND user_id != $2`,
|
||||
[id, user.id],
|
||||
)
|
||||
|
||||
for (const row of otherResult.rows) {
|
||||
await query(
|
||||
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type)
|
||||
VALUES ($1, 'chat_message', 'New Message', $2, '/chats', $3, 'conversation')`,
|
||||
[row.user_id, `${senderName} sent a message`, id],
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: {
|
||||
id: msg.id,
|
||||
conversationId: id,
|
||||
senderId: user.id,
|
||||
senderName,
|
||||
senderAvatar: user.avatar,
|
||||
content: content.trim(),
|
||||
timestamp: formatTime(new Date(msg.created_at)),
|
||||
createdAt: msg.created_at,
|
||||
read: false,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Send message error:", error)
|
||||
return NextResponse.json({ error: "Failed to send message" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
const now = new Date()
|
||||
const isToday = date.toDateString() === now.toDateString()
|
||||
if (isToday) {
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
return date.toLocaleDateString([], { month: "short", day: "numeric" }) + " " +
|
||||
date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function POST(
|
||||
_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
|
||||
|
||||
await query(
|
||||
`UPDATE conversation_participants
|
||||
SET last_read_at = NOW()
|
||||
WHERE conversation_id = $1 AND user_id = $2`,
|
||||
[id, user.id],
|
||||
)
|
||||
|
||||
await query(
|
||||
`UPDATE notifications SET is_read = TRUE
|
||||
WHERE user_id = $1 AND context_type = 'conversation' AND context_id = $2 AND is_read = FALSE`,
|
||||
[user.id, id],
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Mark read error:", error)
|
||||
return NextResponse.json({ error: "Failed to mark as read" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const result = await query(
|
||||
`SELECT
|
||||
c.id,
|
||||
c.updated_at,
|
||||
cp_me.last_read_at,
|
||||
u.id AS other_user_id,
|
||||
u.first_name || ' ' || u.last_name AS other_user_name,
|
||||
u.email AS other_user_email,
|
||||
u.avatar_url AS other_user_avatar_url,
|
||||
(SELECT content FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message,
|
||||
(SELECT created_at FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message_time,
|
||||
(SELECT count(*) FROM messages WHERE conversation_id = c.id AND sender_id != $1 AND created_at > COALESCE(cp_me.last_read_at, '1970-01-01')) AS unread
|
||||
FROM conversations c
|
||||
JOIN conversation_participants cp_me ON cp_me.conversation_id = c.id AND cp_me.user_id = $1
|
||||
JOIN conversation_participants cp ON cp.conversation_id = c.id
|
||||
JOIN users u ON u.id = cp.user_id AND u.id != $1
|
||||
WHERE c.id IN (
|
||||
SELECT conversation_id FROM conversation_participants WHERE user_id = $1
|
||||
)
|
||||
ORDER BY c.updated_at DESC
|
||||
LIMIT 50`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
const conversations = result.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
updatedAt: row.updated_at,
|
||||
otherUser: {
|
||||
id: row.other_user_id,
|
||||
name: row.other_user_name,
|
||||
email: row.other_user_email,
|
||||
avatar: avatarSvgUrl(row.other_user_name),
|
||||
},
|
||||
lastMessage: row.last_message || "",
|
||||
lastMessageTime: row.last_message_time ? timeAgo(new Date(row.last_message_time)) : "",
|
||||
unread: parseInt(row.unread) || 0,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ conversations })
|
||||
} catch (error) {
|
||||
console.error("Conversations error:", error)
|
||||
return NextResponse.json({ error: "Failed to load conversations" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const { userId } = await request.json()
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "userId is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (userId === user.id) {
|
||||
return NextResponse.json({ error: "Cannot start a conversation with yourself" }, { status: 400 })
|
||||
}
|
||||
|
||||
const existing = await query(
|
||||
`SELECT c.id FROM conversations c
|
||||
JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||
JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||
LIMIT 1`,
|
||||
[user.id, userId],
|
||||
)
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
return NextResponse.json({ conversationId: existing.rows[0].id })
|
||||
}
|
||||
|
||||
const convResult = await query(
|
||||
`INSERT INTO conversations DEFAULT VALUES RETURNING id`,
|
||||
)
|
||||
const conversationId = convResult.rows[0].id
|
||||
|
||||
await query(
|
||||
`INSERT INTO conversation_participants (conversation_id, user_id, last_read_at) VALUES ($1, $2, NOW()), ($1, $3, NOW())`,
|
||||
[conversationId, user.id, userId],
|
||||
)
|
||||
|
||||
const otherUser = await query(
|
||||
`SELECT id, first_name || ' ' || last_name AS name, email, avatar_url
|
||||
FROM users WHERE id = $1`,
|
||||
[userId],
|
||||
)
|
||||
|
||||
const other = otherUser.rows[0]
|
||||
|
||||
return NextResponse.json({
|
||||
conversation: {
|
||||
id: conversationId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
otherUser: {
|
||||
id: other.id,
|
||||
name: other.name,
|
||||
email: other.email,
|
||||
avatar: avatarSvgUrl(other.name),
|
||||
},
|
||||
lastMessage: "",
|
||||
lastMessageTime: "",
|
||||
unread: 0,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Create conversation error:", error)
|
||||
return NextResponse.json({ error: "Failed to create conversation" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
function timeAgo(date: Date): string {
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
|
||||
if (seconds < 60) return "now"
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${minutes}m ago`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
const days = Math.floor(hours / 24)
|
||||
if (days < 7) return `${days}d ago`
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
function getPeriodDateRange(period: string): { start: Date; end: Date } {
|
||||
const end = new Date()
|
||||
let start: Date
|
||||
switch (period) {
|
||||
case "7days":
|
||||
start = new Date(end); start.setDate(start.getDate() - 7); break
|
||||
case "30days":
|
||||
start = new Date(end); start.setDate(start.getDate() - 30); break
|
||||
case "12months":
|
||||
start = new Date(end); start.setFullYear(start.getFullYear() - 12); break
|
||||
case "6months":
|
||||
default:
|
||||
start = new Date(end); start.setMonth(start.getMonth() - 6); break
|
||||
}
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
function getPreviousPeriodRange(period: string, currentStart: Date): { start: Date; end: Date } {
|
||||
const end = new Date(currentStart)
|
||||
const diff = end.getTime() - currentStart.getTime()
|
||||
const start = new Date(end.getTime() - diff)
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
const periodLabels: Record<string, string> = {
|
||||
"7days": "Last 7 days",
|
||||
"30days": "Last 30 days",
|
||||
"6months": "Last 6 months",
|
||||
"12months": "Last 12 months",
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLeadsInRange(start: Date, end: Date, userId?: string, isAdmin?: boolean) {
|
||||
const result = await query(
|
||||
`SELECT l.id, l.created_at, l.company_name, l.contact_name, l.email, l.phone,
|
||||
l.notes, l.assigned_to, l.score,
|
||||
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.deleted_at IS NULL
|
||||
AND l.created_at >= $1 AND l.created_at <= $2
|
||||
${isAdmin ? "" : "AND l.assigned_to = $3"}
|
||||
ORDER BY l.created_at DESC`,
|
||||
isAdmin
|
||||
? [start.toISOString(), end.toISOString()]
|
||||
: [start.toISOString(), end.toISOString(), userId]
|
||||
)
|
||||
return result.rows.map((r: any) => ({
|
||||
...r,
|
||||
status: stageToStatus(r.stage_name),
|
||||
}))
|
||||
}
|
||||
|
||||
function countStatuses(leads: any[]) {
|
||||
const counts = { open: 0, contacted: 0, pending: 0, closed: 0, ignored: 0 }
|
||||
leads.forEach((l: any) => {
|
||||
const s = l.status as keyof typeof counts
|
||||
if (s in counts) counts[s]++
|
||||
})
|
||||
return counts
|
||||
}
|
||||
|
||||
function buildMonthlyBreakdown(leads: any[], period: string) {
|
||||
const { start, end } = getPeriodDateRange(period)
|
||||
const result: { label: string; total: number; open: number; contacted: number; pending: number; closed: number; ignored: number }[] = []
|
||||
const current = new Date(start)
|
||||
const isMonthly = period === "6months" || period === "12months"
|
||||
|
||||
while (current <= end) {
|
||||
const label = isMonthly
|
||||
? current.toLocaleDateString("en-US", { month: "short", year: "2-digit" })
|
||||
: current.toLocaleDateString("en-US", { month: "short", day: "numeric" })
|
||||
|
||||
const ps = new Date(current)
|
||||
const pe = isMonthly
|
||||
? new Date(current.getFullYear(), current.getMonth() + 1, 0, 23, 59, 59)
|
||||
: (() => { const d = new Date(current); d.setHours(23, 59, 59, 999); return d })()
|
||||
|
||||
const inPeriod = leads.filter((l: any) => {
|
||||
const d = new Date(l.created_at)
|
||||
return d >= ps && d <= pe
|
||||
})
|
||||
|
||||
const counts = countStatuses(inPeriod)
|
||||
result.push({ label, total: inPeriod.length, ...counts })
|
||||
|
||||
if (isMonthly) current.setMonth(current.getMonth() + 1)
|
||||
else current.setDate(current.getDate() + 1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function computeTrend(current: number, previous: number): { pct: number; up: boolean } {
|
||||
if (previous === 0) return { pct: current > 0 ? 100 : 0, up: current > 0 }
|
||||
const pct = Math.round(((current - previous) / previous) * 100)
|
||||
return { pct: Math.abs(pct), up: pct >= 0 }
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const period = searchParams.get("period") || "6months"
|
||||
const yearParam = searchParams.get("year")
|
||||
let start: Date, end: Date, prevRange: { start: Date; end: Date }
|
||||
if (yearParam) {
|
||||
const y = parseInt(yearParam)
|
||||
start = new Date(y, 0, 1)
|
||||
end = new Date(y, 11, 31, 23, 59, 59)
|
||||
prevRange = { start: new Date(y - 1, 0, 1), end: new Date(y - 1, 11, 31, 23, 59, 59) }
|
||||
} else {
|
||||
const r = getPeriodDateRange(period)
|
||||
start = r.start; end = r.end
|
||||
prevRange = getPreviousPeriodRange(period, start)
|
||||
}
|
||||
|
||||
const [currentLeads, prevLeads] = await Promise.all([
|
||||
fetchLeadsInRange(start, end, user.id, isAdmin),
|
||||
fetchLeadsInRange(prevRange.start, prevRange.end, user.id, isAdmin),
|
||||
])
|
||||
|
||||
const currentCounts = countStatuses(currentLeads)
|
||||
const prevCounts = countStatuses(prevLeads)
|
||||
|
||||
const totalLeads = currentLeads.length
|
||||
const closedLeads = currentCounts.closed
|
||||
const conversionRate = totalLeads > 0 ? Math.round((closedLeads / totalLeads) * 100) : 0
|
||||
|
||||
const mappedLeads = currentLeads.map((r: any) => ({
|
||||
id: r.id,
|
||||
companyName: r.company_name || "",
|
||||
contactName: r.contact_name,
|
||||
email: r.email || "",
|
||||
phone: r.phone || "",
|
||||
source: "",
|
||||
description: r.notes || "",
|
||||
status: r.status,
|
||||
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,
|
||||
createdAt: r.created_at,
|
||||
updatedAt: r.updated_at,
|
||||
}))
|
||||
|
||||
const monthlyBreakdown = buildMonthlyBreakdown(currentLeads, period)
|
||||
|
||||
const trends = {
|
||||
totalLeads: computeTrend(currentCounts.open + currentCounts.contacted + currentCounts.pending + currentCounts.closed + currentCounts.ignored,
|
||||
prevCounts.open + prevCounts.contacted + prevCounts.pending + prevCounts.closed + prevCounts.ignored),
|
||||
openLeads: computeTrend(currentCounts.open, prevCounts.open),
|
||||
contactedLeads: computeTrend(currentCounts.contacted, prevCounts.contacted),
|
||||
pendingLeads: computeTrend(currentCounts.pending, prevCounts.pending),
|
||||
closedLeads: computeTrend(currentCounts.closed, prevCounts.closed),
|
||||
conversionRate: computeTrend(conversionRate,
|
||||
prevLeads.length > 0 ? Math.round((prevCounts.closed / prevLeads.length) * 100) : 0),
|
||||
}
|
||||
|
||||
const stats = {
|
||||
totalLeads,
|
||||
openLeads: currentCounts.open,
|
||||
contactedLeads: currentCounts.contacted,
|
||||
pendingLeads: currentCounts.pending,
|
||||
closedLeads,
|
||||
ignoredLeads: currentCounts.ignored,
|
||||
conversionRate,
|
||||
monthlyBreakdown,
|
||||
leadsPerMonth: monthlyBreakdown.map((m: any) => ({ label: m.label, leads: m.total, closed: m.closed })),
|
||||
trends,
|
||||
recentLeads: mappedLeads.slice(0, 10),
|
||||
statusDistribution: [
|
||||
{ name: "Open", value: currentCounts.open, color: "#3b82f6" },
|
||||
{ name: "Contacted", value: currentCounts.contacted, color: "#f59e0b" },
|
||||
{ name: "Pending", value: currentCounts.pending, color: "#8b5cf6" },
|
||||
{ name: "Closed", value: currentCounts.closed, color: "#10b981" },
|
||||
{ name: "Ignored", value: currentCounts.ignored, color: "#6B7280" },
|
||||
],
|
||||
periodLabel: periodLabels[period] ?? "Selected period",
|
||||
}
|
||||
|
||||
return NextResponse.json(stats)
|
||||
} catch (error) {
|
||||
console.error("Dashboard API error:", error)
|
||||
return NextResponse.json({ error: "Failed to load dashboard stats" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { query } from "@/lib/db"
|
||||
import crypto from "crypto"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { phone, token: clientToken } = await request.json()
|
||||
if (!phone) {
|
||||
return NextResponse.json({ error: "Phone number required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const token = clientToken || crypto.randomBytes(24).toString("hex")
|
||||
await query(
|
||||
`INSERT INTO invites (token, phone) VALUES ($1, $2)`,
|
||||
[token, phone],
|
||||
)
|
||||
|
||||
const origin = request.headers.get("origin") || "http://localhost:3000"
|
||||
const inviteUrl = `${origin}/join/${token}`
|
||||
|
||||
return NextResponse.json({ token, inviteUrl })
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Failed to generate invite" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
async function checkLeadAccess(leadId: string, userId: string): Promise<boolean> {
|
||||
const result = await query(
|
||||
`SELECT 1 FROM leads WHERE id = $1 AND deleted_at IS NULL
|
||||
AND (assigned_to = $2 OR EXISTS (
|
||||
SELECT 1 FROM user_roles ur JOIN roles r ON r.id = ur.role_id
|
||||
WHERE ur.user_id = $2 AND r.name IN ('ADMIN', 'SUPER_ADMIN')
|
||||
))`,
|
||||
[leadId, userId]
|
||||
)
|
||||
return result.rows.length > 0
|
||||
}
|
||||
|
||||
export async function POST(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
|
||||
if (!await checkLeadAccess(id, user.id)) {
|
||||
return NextResponse.json({ error: "Lead not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
const { content } = await request.json()
|
||||
if (!content?.trim()) {
|
||||
return NextResponse.json({ error: "Content is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
await query(
|
||||
`INSERT INTO customer_notes (customer_id, author_id, content)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[id, user.id, content.trim()]
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Create note error:", error)
|
||||
return NextResponse.json({ error: "Failed to create note" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (!await checkLeadAccess(id, user.id)) {
|
||||
return NextResponse.json({ error: "Lead not found" }, { status: 404 })
|
||||
}
|
||||
const { searchParams } = new URL(request.url)
|
||||
const limit = parseInt(searchParams.get("limit") || "50", 10)
|
||||
const offset = parseInt(searchParams.get("offset") || "0", 10)
|
||||
|
||||
const result = await query(
|
||||
`SELECT cn.id, cn.created_at, cn.updated_at, cn.content,
|
||||
u.id AS user_id, u.first_name, u.last_name, u.avatar_url
|
||||
FROM customer_notes cn
|
||||
JOIN users u ON u.id = cn.author_id
|
||||
WHERE cn.customer_id = $1 AND cn.deleted_at IS NULL
|
||||
ORDER BY cn.created_at DESC
|
||||
LIMIT $2 OFFSET $3`,
|
||||
[id, limit, offset]
|
||||
)
|
||||
|
||||
const notes = result.rows.map((r: any) => ({
|
||||
id: r.id,
|
||||
leadId: id,
|
||||
userId: r.user_id,
|
||||
authorName: `${r.first_name} ${r.last_name}`,
|
||||
authorAvatar: avatarSvgUrl(`${r.first_name} ${r.last_name}`),
|
||||
note: r.content,
|
||||
createdAt: r.created_at,
|
||||
updatedAt: r.updated_at,
|
||||
}))
|
||||
|
||||
return NextResponse.json(notes)
|
||||
} catch (error) {
|
||||
console.error("Lead notes API error:", error)
|
||||
return NextResponse.json({ error: "Failed to load notes" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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,
|
||||
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,
|
||||
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 (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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
function getPeriodDateRange(period: string): { start: Date; end: Date } | null {
|
||||
if (period === "all") return null
|
||||
const end = new Date()
|
||||
let start: Date
|
||||
switch (period) {
|
||||
case "7days": start = new Date(end); start.setDate(start.getDate() - 7); break
|
||||
case "30days": start = new Date(end); start.setDate(start.getDate() - 30); break
|
||||
case "12months": start = new Date(end); start.setFullYear(start.getFullYear() - 12); break
|
||||
case "6months": default: start = new Date(end); start.setMonth(start.getMonth() - 6); break
|
||||
}
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
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 status = searchParams.get("status") || "all"
|
||||
const period = searchParams.get("period") || "all"
|
||||
const limit = parseInt(searchParams.get("limit") || "50", 10)
|
||||
const offset = parseInt(searchParams.get("offset") || "0", 10)
|
||||
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
||||
|
||||
let sql = `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,
|
||||
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.deleted_at IS NULL`
|
||||
|
||||
const params: any[] = []
|
||||
let paramIdx = 1
|
||||
|
||||
if (period !== "all") {
|
||||
const range = getPeriodDateRange(period)
|
||||
if (range) {
|
||||
sql += ` AND l.created_at >= $${paramIdx} AND l.created_at <= $${paramIdx + 1}`
|
||||
params.push(range.start.toISOString(), range.end.toISOString())
|
||||
paramIdx += 2
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
sql += ` AND (l.contact_name ILIKE $${paramIdx} OR l.company_name ILIKE $${paramIdx} OR l.email ILIKE $${paramIdx} OR l.phone ILIKE $${paramIdx})`
|
||||
params.push(`%${search}%`)
|
||||
paramIdx++
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
sql += ` AND l.assigned_to = $${paramIdx}`
|
||||
params.push(user.id)
|
||||
paramIdx++
|
||||
}
|
||||
|
||||
sql += ` ORDER BY l.created_at DESC`
|
||||
sql += ` LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`
|
||||
params.push(limit, offset)
|
||||
paramIdx += 2
|
||||
|
||||
const result = await query(sql, params)
|
||||
|
||||
let leads = result.rows.map((r: any) => {
|
||||
const s = stageToStatus(r.stage_name)
|
||||
return {
|
||||
id: r.id,
|
||||
companyName: r.company_name || "",
|
||||
contactName: r.contact_name,
|
||||
email: r.email || "",
|
||||
phone: r.phone || "",
|
||||
source: "",
|
||||
description: r.notes || "",
|
||||
status: s,
|
||||
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,
|
||||
createdAt: r.created_at,
|
||||
updatedAt: r.updated_at,
|
||||
}
|
||||
})
|
||||
|
||||
if (status !== "all") {
|
||||
leads = leads.filter((l: any) => l.status === status)
|
||||
}
|
||||
|
||||
return NextResponse.json(leads)
|
||||
} catch (error) {
|
||||
console.error("Leads API error:", error)
|
||||
return NextResponse.json({ error: "Failed to load leads" }, { 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 isAdmin = user.role === "admin" || user.role === "super_admin"
|
||||
|
||||
// Non-admin users can only assign leads to themselves; admin/super_admin can assign to anyone
|
||||
let assignedUserId = body.assignedUserId
|
||||
if (!isAdmin) {
|
||||
assignedUserId = user.id
|
||||
} else if (assignedUserId === "none" || !assignedUserId) {
|
||||
assignedUserId = null
|
||||
}
|
||||
|
||||
const stageResult = await query(
|
||||
"SELECT id FROM lead_stages WHERE name = $1",
|
||||
[body.status === "open" ? "New" : "Contacted"]
|
||||
)
|
||||
const stageId = stageResult.rows[0]?.id || 1
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO leads (company_name, contact_name, email, phone, notes, source_id, stage_id, assigned_to, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
||||
RETURNING id`,
|
||||
[
|
||||
body.companyName,
|
||||
body.contactName,
|
||||
body.email,
|
||||
body.phone || null,
|
||||
body.description || null,
|
||||
body.source || null,
|
||||
stageId,
|
||||
assignedUserId,
|
||||
]
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, id: result.rows[0].id }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("Leads POST error:", error)
|
||||
return NextResponse.json({ error: "Failed to create lead" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const id = searchParams.get("id")
|
||||
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 })
|
||||
|
||||
const result = await query(
|
||||
"UPDATE leads SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL AND ($2 = true OR assigned_to = $3) RETURNING id",
|
||||
[id, isAdmin, user.id]
|
||||
)
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return NextResponse.json({ error: "Lead not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Leads DELETE error:", error)
|
||||
return NextResponse.json({ error: "Failed to delete lead" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
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 result = await query(
|
||||
`UPDATE notifications SET is_read = TRUE WHERE id = $1 AND user_id = $2 RETURNING id`,
|
||||
[id, user.id],
|
||||
)
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return NextResponse.json({ error: "Notification not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Notification PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to mark notification as read" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(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 result = await query(
|
||||
`DELETE FROM notifications WHERE id = $1 AND user_id = $2 RETURNING id`,
|
||||
[id, user.id],
|
||||
)
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return NextResponse.json({ error: "Notification not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Notification DELETE error:", error)
|
||||
return NextResponse.json({ error: "Failed to dismiss notification" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const result = await query(
|
||||
`SELECT lead_assigned, lead_status, note_added, daily_digest, weekly_report
|
||||
FROM notification_preferences
|
||||
WHERE user_id = $1`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return NextResponse.json({
|
||||
leadAssigned: true,
|
||||
leadStatus: true,
|
||||
noteAdded: false,
|
||||
dailyDigest: false,
|
||||
weeklyReport: true,
|
||||
})
|
||||
}
|
||||
|
||||
const r = result.rows[0]
|
||||
return NextResponse.json({
|
||||
leadAssigned: r.lead_assigned,
|
||||
leadStatus: r.lead_status,
|
||||
noteAdded: r.note_added,
|
||||
dailyDigest: r.daily_digest,
|
||||
weeklyReport: r.weekly_report,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Preferences GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load preferences" }, { 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()
|
||||
|
||||
await query(
|
||||
`INSERT INTO notification_preferences (user_id, lead_assigned, lead_status, note_added, daily_digest, weekly_report, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET lead_assigned = $2, lead_status = $3, note_added = $4,
|
||||
daily_digest = $5, weekly_report = $6, updated_at = NOW()`,
|
||||
[
|
||||
user.id,
|
||||
body.leadAssigned ?? true,
|
||||
body.leadStatus ?? true,
|
||||
body.noteAdded ?? false,
|
||||
body.dailyDigest ?? false,
|
||||
body.weeklyReport ?? true,
|
||||
],
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Preferences PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to save preferences" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const result = await query(
|
||||
`SELECT id, type, title, description, link, is_read, created_at, context_id, context_type
|
||||
FROM notifications
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
const notifications = result.rows.map((r: any) => ({
|
||||
id: r.id,
|
||||
type: r.type,
|
||||
title: r.title,
|
||||
description: r.description,
|
||||
link: r.link,
|
||||
read: r.is_read,
|
||||
timestamp: r.created_at,
|
||||
contextId: r.context_id,
|
||||
contextType: r.context_type,
|
||||
}))
|
||||
|
||||
const unreadResult = await query(
|
||||
`SELECT COUNT(*) AS count FROM notifications WHERE user_id = $1 AND is_read = FALSE`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
notifications,
|
||||
unreadCount: parseInt(unreadResult.rows[0].count, 10),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Notifications GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load notifications" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const { type, title, description, link, userId } = await request.json()
|
||||
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
||||
const targetUserId = (userId && isAdmin) ? userId : user.id
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO notifications (user_id, type, title, description, link)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, type, title, description, link, is_read, created_at`,
|
||||
[targetUserId, type, title, description || null, link || null],
|
||||
)
|
||||
|
||||
const notif = result.rows[0]
|
||||
return NextResponse.json({
|
||||
id: notif.id,
|
||||
type: notif.type,
|
||||
title: notif.title,
|
||||
description: notif.description,
|
||||
link: notif.link,
|
||||
read: notif.is_read,
|
||||
timestamp: notif.created_at,
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("Notifications POST error:", error)
|
||||
return NextResponse.json({ error: "Failed to create notification" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
await query(
|
||||
`UPDATE notifications SET is_read = TRUE WHERE user_id = $1 AND is_read = FALSE`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Notifications PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to mark all as read" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (user.role !== "admin" && user.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const result = await query(`SELECT * FROM company_settings ORDER BY updated_at DESC LIMIT 1`)
|
||||
const row = result.rows[0]
|
||||
|
||||
if (!row) {
|
||||
return NextResponse.json({
|
||||
companyName: "Coastal IT Solutions",
|
||||
companyEmail: "info@coastalit.com",
|
||||
companyPhone: "(555) 123-4567",
|
||||
companyWebsite: "https://coastalit.com",
|
||||
companyAddress: "123 Business Ave, Suite 100, San Francisco, CA 94105",
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
companyName: row.company_name || "",
|
||||
companyEmail: row.company_email || "",
|
||||
companyPhone: row.company_phone || "",
|
||||
companyWebsite: row.company_website || "",
|
||||
companyAddress: row.company_address || "",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Company settings GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load company settings" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (user.role !== "admin" && user.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
await query(
|
||||
`UPDATE company_settings SET
|
||||
company_name = $1, company_email = $2, company_phone = $3,
|
||||
company_website = $4, company_address = $5, updated_by = $6, updated_at = NOW()`,
|
||||
[
|
||||
body.companyName || "",
|
||||
body.companyEmail || "",
|
||||
body.companyPhone || "",
|
||||
body.companyWebsite || "",
|
||||
body.companyAddress || "",
|
||||
user.id,
|
||||
],
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Company settings PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to save company settings" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
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 })
|
||||
if (user.role !== "admin" && user.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const fields: string[] = []
|
||||
const values: any[] = []
|
||||
let idx = 1
|
||||
|
||||
if (body.isActive !== undefined) {
|
||||
fields.push(`is_active = $${idx++}`)
|
||||
values.push(body.isActive)
|
||||
}
|
||||
if (body.label !== undefined) {
|
||||
fields.push(`label = $${idx++}`)
|
||||
values.push(body.label.trim())
|
||||
}
|
||||
if (body.profilePath !== undefined) {
|
||||
fields.push(`profile_path = $${idx++}, cookie_file = $${idx}`)
|
||||
values.push(body.profilePath.trim())
|
||||
values.push(`${body.profilePath.replace(/\\+$/, '')}\\cookies.sqlite`)
|
||||
idx++
|
||||
}
|
||||
if (body.unflag === true) {
|
||||
fields.push(`flagged = FALSE, flagged_at = NULL, flagged_reason = NULL, consecutive_failures = 0`)
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return NextResponse.json({ error: "No fields to update" }, { status: 400 })
|
||||
}
|
||||
|
||||
fields.push(`updated_at = NOW()`)
|
||||
values.push(id)
|
||||
|
||||
await query(
|
||||
`UPDATE facebook_accounts SET ${fields.join(", ")} WHERE id = $${idx}`,
|
||||
values
|
||||
)
|
||||
|
||||
const updated = await query(
|
||||
`SELECT id, label, profile_path, is_active, flagged, flagged_reason,
|
||||
consecutive_failures, updated_at
|
||||
FROM facebook_accounts WHERE id = $1`,
|
||||
[id]
|
||||
)
|
||||
|
||||
return NextResponse.json(updated.rows[0] || { success: true })
|
||||
} catch (error) {
|
||||
console.error("Facebook accounts PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to update Facebook account" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (user.role !== "admin" && user.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const accounts = await query(
|
||||
`SELECT fa.id, fa.label, fa.profile_path, fa.is_active,
|
||||
fa.last_scrape_at, fa.last_success_at, fa.last_error_at,
|
||||
fa.last_error_message, fa.consecutive_failures,
|
||||
fa.flagged, fa.flagged_at, fa.flagged_reason,
|
||||
fa.created_at, fa.updated_at,
|
||||
COALESCE(sl.leads_found, 0) AS last_leads_found,
|
||||
sl.success AS last_success,
|
||||
sl.detected_flag AS last_detected_flag
|
||||
FROM facebook_accounts fa
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT leads_found, success, detected_flag
|
||||
FROM facebook_scrape_logs
|
||||
WHERE account_id = fa.id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) sl ON TRUE
|
||||
ORDER BY fa.created_at ASC`
|
||||
)
|
||||
|
||||
return NextResponse.json(accounts.rows)
|
||||
} catch (error) {
|
||||
console.error("Facebook accounts GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load Facebook accounts" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (user.role !== "admin" && user.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { label, profilePath } = await request.json()
|
||||
if (!label?.trim() || !profilePath?.trim()) {
|
||||
return NextResponse.json({ error: "Label and profile path are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const cookieFile = `${profilePath.replace(/\\+$/, '')}\\cookies.sqlite`
|
||||
const result = await query(
|
||||
`INSERT INTO facebook_accounts (label, profile_path, cookie_file)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, label, profile_path, is_active, created_at`,
|
||||
[label.trim(), profilePath.trim(), cookieFile]
|
||||
)
|
||||
|
||||
return NextResponse.json(result.rows[0], { status: 201 })
|
||||
} catch (error) {
|
||||
console.error("Facebook accounts POST error:", error)
|
||||
return NextResponse.json({ error: "Failed to create Facebook account" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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 })
|
||||
if (user.role !== "admin" && user.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const accountId = searchParams.get("accountId")
|
||||
const limit = parseInt(searchParams.get("limit") || "50", 10)
|
||||
const offset = parseInt(searchParams.get("offset") || "0", 10)
|
||||
|
||||
let sql = `SELECT sl.id, sl.account_id, fa.label AS account_label,
|
||||
sl.started_at, sl.completed_at, sl.success,
|
||||
sl.leads_found, sl.error_message, sl.detected_flag,
|
||||
sl.created_at
|
||||
FROM facebook_scrape_logs sl
|
||||
JOIN facebook_accounts fa ON fa.id = sl.account_id`
|
||||
const params: any[] = []
|
||||
let paramIdx = 1
|
||||
|
||||
if (accountId) {
|
||||
sql += ` WHERE sl.account_id = $${paramIdx++}`
|
||||
params.push(accountId)
|
||||
}
|
||||
|
||||
sql += ` ORDER BY sl.created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx++}`
|
||||
params.push(limit, offset)
|
||||
|
||||
const result = await query(sql, params)
|
||||
return NextResponse.json(result.rows)
|
||||
} catch (error) {
|
||||
console.error("Facebook scrape logs GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load scrape logs" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const result = await query(
|
||||
`SELECT timezone, date_format, items_per_page FROM user_preferences WHERE user_id = $1`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return NextResponse.json({
|
||||
timezone: "america-los_angeles",
|
||||
dateFormat: "mdy",
|
||||
itemsPerPage: 20,
|
||||
})
|
||||
}
|
||||
|
||||
const r = result.rows[0]
|
||||
return NextResponse.json({
|
||||
timezone: r.timezone,
|
||||
dateFormat: r.date_format,
|
||||
itemsPerPage: r.items_per_page,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Preferences GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load preferences" }, { 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()
|
||||
|
||||
await query(
|
||||
`INSERT INTO user_preferences (user_id, timezone, date_format, items_per_page, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET timezone = $2, date_format = $3, items_per_page = $4, updated_at = NOW()`,
|
||||
[
|
||||
user.id,
|
||||
body.timezone || "america-los_angeles",
|
||||
body.dateFormat || "mdy",
|
||||
body.itemsPerPage || 20,
|
||||
],
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Preferences PATCH error:", error)
|
||||
return NextResponse.json({ error: "Failed to save preferences" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const result = await query(
|
||||
`SELECT preferences->>'website_theme' AS website_theme FROM users WHERE id = $1`,
|
||||
[user.id],
|
||||
)
|
||||
|
||||
const websiteTheme = result.rows[0]?.website_theme || "spidey"
|
||||
return NextResponse.json({ websiteTheme })
|
||||
} catch (error) {
|
||||
console.error("Website theme GET error:", error)
|
||||
return NextResponse.json({ error: "Failed to load website theme" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const body = await request.json()
|
||||
const theme = body.websiteTheme || "spidey"
|
||||
|
||||
await query(
|
||||
`UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`,
|
||||
[user.id, JSON.stringify({ website_theme: theme })],
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, websiteTheme: theme })
|
||||
} catch (error) {
|
||||
console.error("Website theme PUT error:", error)
|
||||
return NextResponse.json({ error: "Failed to save website theme" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import os from "os"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
|
||||
let prevCpu = process.cpuUsage()
|
||||
let prevTime = Date.now()
|
||||
|
||||
export async function GET() {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (sessionUser.role !== "admin" && sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const elapsed = now - prevTime
|
||||
const currentCpu = process.cpuUsage()
|
||||
|
||||
const user = currentCpu.user - prevCpu.user
|
||||
const sys = currentCpu.system - prevCpu.system
|
||||
const totalUs = user + sys
|
||||
|
||||
// CPU time (ms) / wall time (ms) * 100 = % of one core
|
||||
const cpuPct = elapsed > 0 ? Math.round((totalUs / 1000) / elapsed * 100 * 10) / 10 : 0
|
||||
|
||||
prevCpu = currentCpu
|
||||
prevTime = now
|
||||
|
||||
const mem = process.memoryUsage()
|
||||
|
||||
return NextResponse.json({
|
||||
rssMB: Math.round(mem.rss / 1024 / 1024),
|
||||
cpuPct,
|
||||
cores: os.cpus().length,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { query } from "@/lib/db"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) {
|
||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
|
||||
}
|
||||
if (sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Only super admins can delete users" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
if (id === sessionUser.id) {
|
||||
return NextResponse.json({ error: "Cannot delete yourself" }, { status: 400 })
|
||||
}
|
||||
|
||||
await query(
|
||||
`UPDATE users SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[id]
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error deleting user:", error)
|
||||
return NextResponse.json({ error: "Failed to delete user" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
const ALLOWED_PREFIXES = ["data:image/png;base64,", "data:image/jpeg;base64,", "data:image/gif;base64,"]
|
||||
const MAX_AVATAR_BYTES = 2 * 1024 * 1024 // 2MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
|
||||
const { avatar } = await request.json()
|
||||
if (!avatar || typeof avatar !== "string") {
|
||||
return NextResponse.json({ error: "Invalid avatar data" }, { status: 400 })
|
||||
}
|
||||
|
||||
const allowed = ALLOWED_PREFIXES.some((p) => avatar.startsWith(p))
|
||||
if (!allowed) {
|
||||
return NextResponse.json({ error: "Avatar must be a PNG, JPEG, or GIF data URL" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Approximate decoded size: base64 is ~4/3 of original
|
||||
const base64Data = avatar.split(",")[1] || ""
|
||||
const estimatedBytes = Math.round(base64Data.length * 0.75)
|
||||
if (estimatedBytes > MAX_AVATAR_BYTES) {
|
||||
return NextResponse.json({ error: "Avatar exceeds 2MB size limit" }, { status: 400 })
|
||||
}
|
||||
|
||||
await query(
|
||||
`UPDATE users SET avatar_url = $1 WHERE id = $2`,
|
||||
[avatar, user.id],
|
||||
)
|
||||
|
||||
return NextResponse.json({ avatar })
|
||||
} catch (error) {
|
||||
console.error("Avatar upload error:", error)
|
||||
return NextResponse.json({ error: "Failed to update avatar" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const phone = request.nextUrl.searchParams.get("phone")
|
||||
if (!phone) {
|
||||
return NextResponse.json({ error: "Phone parameter required" }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await query(
|
||||
`SELECT id, username, first_name, last_name, phone, avatar_url
|
||||
FROM users
|
||||
WHERE phone = $1 AND deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[phone],
|
||||
)
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return NextResponse.json({ found: false })
|
||||
}
|
||||
|
||||
const user = result.rows[0]
|
||||
return NextResponse.json({
|
||||
found: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
phone: user.phone,
|
||||
avatar: user.avatar_url,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Lookup failed" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { query } from "@/lib/db"
|
||||
import { hashPassword, getSessionUser } from "@/lib/auth"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (sessionUser.role !== "admin" && sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const limit = parseInt(searchParams.get("limit") || "50", 10)
|
||||
const offset = parseInt(searchParams.get("offset") || "0", 10)
|
||||
|
||||
const result = await query(
|
||||
`SELECT u.id, u.username, u.email, u.first_name, u.last_name,
|
||||
u.is_active AS active, u.created_at, u.avatar_url,
|
||||
r.name AS role
|
||||
FROM users u
|
||||
JOIN user_roles ur ON ur.user_id = u.id
|
||||
JOIN roles r ON r.id = ur.role_id
|
||||
WHERE u.deleted_at IS NULL
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT $1 OFFSET $2`,
|
||||
[limit, offset]
|
||||
)
|
||||
const users = result.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
name: `${row.first_name} ${row.last_name}`,
|
||||
email: row.email,
|
||||
role: row.role.toLowerCase(),
|
||||
active: row.active,
|
||||
avatar: avatarSvgUrl(`${row.first_name} ${row.last_name}`),
|
||||
createdAt: row.created_at,
|
||||
}))
|
||||
return NextResponse.json({ users }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error)
|
||||
return NextResponse.json({ error: "Failed to fetch users" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const sessionUser = await getSessionUser()
|
||||
if (!sessionUser) {
|
||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
|
||||
}
|
||||
if (sessionUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Only super admins can create users" }, { status: 403 })
|
||||
}
|
||||
|
||||
const { name, email, password, role, active } = await request.json()
|
||||
if (!name || !email || !password || !role) {
|
||||
return NextResponse.json({ error: "Name, email, password, and role are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const validRoles = ["sales", "admin", "super_admin", "dev"]
|
||||
if (!validRoles.includes(role)) {
|
||||
return NextResponse.json({ error: "Invalid role" }, { status: 400 })
|
||||
}
|
||||
|
||||
const nameParts = name.trim().split(/\s+/)
|
||||
const firstName = nameParts[0]
|
||||
const lastName = nameParts.slice(1).join(" ") || firstName
|
||||
const username = email.split("@")[0]
|
||||
const passwordHash = await hashPassword(password)
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO users (username, email, password_hash, first_name, last_name, is_active, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id`,
|
||||
[username.toLowerCase(), email.toLowerCase(), passwordHash, firstName, lastName, active ?? true, sessionUser.id]
|
||||
)
|
||||
|
||||
const roleId = (
|
||||
await query(`SELECT id FROM roles WHERE LOWER(name) = LOWER($1)`, [role])
|
||||
).rows[0]?.id
|
||||
|
||||
if (roleId) {
|
||||
await query(
|
||||
`INSERT INTO user_roles (user_id, role_id, assigned_by)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[result.rows[0].id, roleId, sessionUser.id]
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, id: result.rows[0].id }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
console.error("Error creating user:", error)
|
||||
if (error?.constraint === "uq_users_username" || error?.constraint === "uq_users_email") {
|
||||
return NextResponse.json({ error: "A user with this email or username already exists" }, { status: 409 })
|
||||
}
|
||||
return NextResponse.json({ error: error?.message || "Failed to create user" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const currentUser = await getSessionUser()
|
||||
if (!currentUser) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
if (currentUser.role !== "admin" && currentUser.role !== "super_admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const q = request.nextUrl.searchParams.get("q") || ""
|
||||
|
||||
if (!q.trim()) {
|
||||
return NextResponse.json({ users: [] })
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`SELECT id, first_name || ' ' || last_name AS name, email, avatar_url
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL
|
||||
AND id != $1
|
||||
AND (LOWER(first_name || ' ' || last_name) LIKE LOWER($2)
|
||||
OR LOWER(email) LIKE LOWER($2))
|
||||
ORDER BY first_name ASC
|
||||
LIMIT 10`,
|
||||
[currentUser.id, `%${q}%`],
|
||||
)
|
||||
|
||||
const users = result.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
avatar: avatarSvgUrl(row.name),
|
||||
}))
|
||||
|
||||
return NextResponse.json({ users })
|
||||
} catch (error) {
|
||||
console.error("User search error:", error)
|
||||
return NextResponse.json({ error: "Search failed" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user