feat: Enhance provider components with detailed documentation and comments
- Added comprehensive comments and JSDoc-style documentation to NotificationProvider, ThemeProvider, UserProvider, and WebsiteThemeProvider for better clarity and maintainability. - Improved type definitions in index.ts for better code understanding and usage. - Introduced Docker support with Dockerfiles for various services including AI server, signaling server, and browser-use service. - Created a docker-compose.yml file to orchestrate multiple services including PostgreSQL, AI, scraper, and frontend. - Added a startup guide (startup.txt) for setting up the CRM environment on Ubuntu with Docker. - Included a .dockerignore file to exclude unnecessary files from Docker builds.
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
// ── Dashboard ────────────────────────────────────────────────────────────────
|
||||
// GET /api/dashboard?period=6months&year=2025
|
||||
// — Returns aggregated stats, trends, monthly breakdown, and recent leads.
|
||||
//
|
||||
// Auth: authenticated; non-admin users scoped to their own leads
|
||||
//
|
||||
// Periods: 7days, 30days, 6months (default), 12months, or a specific year
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getSessionUser } from "@/lib/auth"
|
||||
import { query } from "@/lib/db"
|
||||
import { avatarSvgUrl } from "@/lib/avatar"
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the { start, end } Date range for a named period.
|
||||
*/
|
||||
function getPeriodDateRange(period: string): { start: Date; end: Date } {
|
||||
const end = new Date()
|
||||
let start: Date
|
||||
@@ -20,6 +33,9 @@ function getPeriodDateRange(period: string): { start: Date; end: Date } {
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the range for the *previous* period of the same length.
|
||||
*/
|
||||
function getPreviousPeriodRange(period: string, currentStart: Date): { start: Date; end: Date } {
|
||||
const end = new Date(currentStart)
|
||||
const diff = end.getTime() - currentStart.getTime()
|
||||
@@ -34,6 +50,9 @@ const periodLabels: Record<string, string> = {
|
||||
"12months": "Last 12 months",
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps DB stage name to client-facing status string.
|
||||
*/
|
||||
function stageToStatus(name: string): string {
|
||||
switch (name) {
|
||||
case "New": return "open"
|
||||
@@ -48,12 +67,16 @@ function stageToStatus(name: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a trend object from current vs previous count.
|
||||
*/
|
||||
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 }
|
||||
}
|
||||
|
||||
// Reusable SQL snippet that maps stage names to status strings
|
||||
const stageStatusSql = `
|
||||
CASE
|
||||
WHEN ls.name = 'New' THEN 'open'
|
||||
@@ -64,6 +87,8 @@ const stageStatusSql = `
|
||||
ELSE 'open'
|
||||
END`
|
||||
|
||||
// ── GET ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await getSessionUser()
|
||||
@@ -76,6 +101,7 @@ export async function GET(request: NextRequest) {
|
||||
const yearParam = searchParams.get("year")
|
||||
let start: Date, end: Date, prevRange: { start: Date; end: Date }
|
||||
if (yearParam) {
|
||||
// Year mode: compare full year to previous full year
|
||||
const y = parseInt(yearParam)
|
||||
start = new Date(y, 0, 1)
|
||||
end = new Date(y, 11, 31, 23, 59, 59)
|
||||
@@ -86,9 +112,10 @@ export async function GET(request: NextRequest) {
|
||||
prevRange = getPreviousPeriodRange(period, start)
|
||||
}
|
||||
|
||||
// Non-admin users only see their own leads
|
||||
const ownerFilter = isAdmin ? "" : "AND l.assigned_to = $3"
|
||||
|
||||
// Status counts for current period (SQL aggregation)
|
||||
// ── Status counts for current period (SQL aggregation) ──────────
|
||||
const countSql = `
|
||||
SELECT ${stageStatusSql} AS status, COUNT(*) AS count
|
||||
FROM leads l
|
||||
@@ -129,7 +156,7 @@ export async function GET(request: NextRequest) {
|
||||
const closedLeads = currentCounts.closed
|
||||
const conversionRate = totalLeads > 0 ? Math.round((closedLeads / totalLeads) * 100) : 0
|
||||
|
||||
// Monthly/weekly breakdown via date_trunc
|
||||
// ── Monthly/weekly breakdown via date_trunc ─────────────────────
|
||||
const isMonthly = period === "6months" || period === "12months"
|
||||
const truncUnit = isMonthly ? "month" : "day"
|
||||
const breakdownSql = `
|
||||
@@ -148,7 +175,7 @@ export async function GET(request: NextRequest) {
|
||||
: [truncUnit, start.toISOString(), end.toISOString(), user.id]
|
||||
const breakdownResult = await query(breakdownSql, breakdownParams)
|
||||
|
||||
// Build monthly breakdown from aggregated data
|
||||
// Aggregate the grouped rows into a label-keyed map
|
||||
const breakdownMap: Record<string, { label: string; total: number; open: number; contacted: number; pending: number; closed: number; ignored: number }> = {}
|
||||
for (const r of breakdownResult.rows) {
|
||||
const d = new Date(r.period_start)
|
||||
@@ -165,7 +192,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
const monthlyBreakdown = Object.values(breakdownMap)
|
||||
|
||||
// Recent 10 leads
|
||||
// ── Recent 10 leads ─────────────────────────────────────────────
|
||||
const recentSql = `
|
||||
SELECT l.id, l.created_at, l.company_name, l.contact_name, l.email, l.phone,
|
||||
l.notes, l.assigned_to, l.score,
|
||||
@@ -206,6 +233,7 @@ export async function GET(request: NextRequest) {
|
||||
updatedAt: r.updated_at,
|
||||
}))
|
||||
|
||||
// ── Trends (current vs previous period) ─────────────────────────
|
||||
const trends = {
|
||||
totalLeads: computeTrend(totalLeads, prevTotal),
|
||||
openLeads: computeTrend(currentCounts.open, prevCounts.open),
|
||||
|
||||
Reference in New Issue
Block a user