feat: add system monitor component and API for performance metrics
feat: implement conversations API with message retrieval and posting feat: add avatar URL handling for users and update user role definitions feat: create chat system tables in the database with initial seed data fix: update user roles from "sales_user" to "sales" for consistency chore: add middleware for system API route access fixed fuckups on john's side, added a benchmark Made Graphs actualy load from database and realtime data, graphs will update and percentages will be mathematically made, and made realtime added chatcart edits, made graphs glow. added in some little small home feeling fuctions added in a slide show, in login with qoutes from creators because i want to leave a print on it saying we made this shit, we built it brick for brick login page added qoutes some random, and made them shuffle from left to right one dissapears other come in adding shape to the textbox for the qoutes Fixed my chat fuckups when it comes to chats
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
|
||||
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) {
|
||||
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
|
||||
ORDER BY l.created_at DESC`,
|
||||
[start.toISOString(), end.toISOString()]
|
||||
)
|
||||
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 { 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),
|
||||
fetchLeadsInRange(prevRange.start, prevRange.end),
|
||||
])
|
||||
|
||||
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: r.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(`${r.first_name} ${r.last_name}`)}&background=1d4ed8&color=fff&size=128`,
|
||||
} : 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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user