bc83af8e00
Build & Auto-Repair / build (push) Has been cancelled
Hardened db.ts — added statement_timeout: 30000, idle_in_transaction_session_timeout: 10000, created transaction() helper (BEGIN/COMMIT/ROLLBACK). Multi-step API routes wrapped in transactions — Events POST, Conversations POST use atomic blocks. Fixed leads pagination — status filter pushed into SQL WHERE (no client-side filtering after LIMIT/OFFSET), added SELECT COUNT(*) for total. Rewrote dashboard — SQL aggregations (COUNT + GROUP BY + date_trunc) replace loading all leads into memory. Optimized conversations GET — merged duplicate correlated subqueries into single LEFT JOIN LATERAL. Created migration 021_performance_indexes.sql — 8 missing indexes + set_session_user_context() function caching current_user_hierarchy_level() as session variable (avoids per-query RLS join). Fixed build error — duplicate const other in conversations route. Also fixed a TypeScript never error in dashboard breakdown map. Verified — npx tsc --noEmit clean, npm test (13 pass, 7 integration skip gracefully), npx next build succeeds.
142 lines
4.9 KiB
TypeScript
142 lines
4.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server"
|
|
import { getSessionUser } from "@/lib/auth"
|
|
import { query, transaction } 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.phone AS other_user_phone,
|
|
u.avatar_url AS other_user_avatar_url,
|
|
lm.last_message_data->>'content' AS last_message,
|
|
(lm.last_message_data->>'created_at')::timestamptz 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
|
|
LEFT JOIN LATERAL (
|
|
SELECT jsonb_build_object('content', content, 'created_at', created_at) AS last_message_data
|
|
FROM messages WHERE conversation_id = c.id AND deleted_at IS NULL
|
|
ORDER BY created_at DESC LIMIT 1
|
|
) lm ON true
|
|
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 result = await transaction(async (client) => {
|
|
const convResult = await client.query(
|
|
`INSERT INTO conversations DEFAULT VALUES RETURNING id`,
|
|
)
|
|
const conversationId = convResult.rows[0].id
|
|
|
|
await client.query(
|
|
`INSERT INTO conversation_participants (conversation_id, user_id, last_read_at) VALUES ($1, $2, NOW()), ($1, $3, NOW())`,
|
|
[conversationId, user.id, userId],
|
|
)
|
|
|
|
const otherResult = await client.query(
|
|
`SELECT id, first_name || ' ' || last_name AS name, email, avatar_url
|
|
FROM users WHERE id = $1`,
|
|
[userId],
|
|
)
|
|
|
|
return { conversationId, other: otherResult.rows[0] }
|
|
})
|
|
|
|
const { conversationId, other } = result
|
|
|
|
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()
|
|
}
|