mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 03:05:43 +02:00
I fixed the filter for all the leads
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 3006",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { StatCard } from "@/components/dashboard/stat-card"
|
||||
import { StatCardSkeleton } from "@/components/dashboard/stat-card-skeleton"
|
||||
import { RecentLeadsTable } from "@/components/dashboard/recent-leads-table"
|
||||
import { LeadStatusChart } from "@/components/dashboard/lead-status-chart"
|
||||
import { LeadsPerMonthChart } from "@/components/dashboard/leads-per-month-chart"
|
||||
import { dashboardStats } from "@/data/dashboard"
|
||||
import { getDashboardStats } from "@/data/dashboard"
|
||||
import {
|
||||
Users,
|
||||
UserPlus,
|
||||
@@ -23,19 +24,26 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { DashboardStats } from "@/types"
|
||||
|
||||
export default function DashboardPage() {
|
||||
const stats = dashboardStats
|
||||
const [period, setPeriod] = useState("6months")
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
|
||||
const statCards = [
|
||||
{ title: "Total Leads", value: stats.totalLeads, icon: Users, variant: "blue" as const, description: "All time" },
|
||||
{ title: "Open Leads", value: stats.openLeads, icon: UserPlus, variant: "blue" as const, description: "Needs attention" },
|
||||
{ title: "Contacted", value: stats.contactedLeads, icon: PhoneCall, variant: "amber" as const, description: "In conversation" },
|
||||
{ title: "Pending", value: stats.pendingLeads, icon: Clock, variant: "purple" as const, description: "Awaiting decision" },
|
||||
{ title: "Closed", value: stats.closedLeads, icon: CheckCircle2, variant: "emerald" as const, description: "Won" },
|
||||
{ title: "Conversion Rate", value: `${stats.conversionRate}%`, icon: TrendingUp, variant: "emerald" as const, description: `${stats.closedLeads} of ${stats.totalLeads} leads` },
|
||||
]
|
||||
useEffect(() => {
|
||||
setStats(getDashboardStats(period))
|
||||
}, [period])
|
||||
|
||||
const statCards = stats
|
||||
? [
|
||||
{ title: "Total Leads", value: stats.totalLeads, icon: Users, variant: "blue" as const, description: stats.periodLabel },
|
||||
{ title: "Open Leads", value: stats.openLeads, icon: UserPlus, variant: "blue" as const, description: "Needs attention" },
|
||||
{ title: "Contacted", value: stats.contactedLeads, icon: PhoneCall, variant: "amber" as const, description: "In conversation" },
|
||||
{ title: "Pending", value: stats.pendingLeads, icon: Clock, variant: "purple" as const, description: "Awaiting decision" },
|
||||
{ title: "Closed", value: stats.closedLeads, icon: CheckCircle2, variant: "emerald" as const, description: "Won" },
|
||||
{ title: "Conversion Rate", value: `${stats.conversionRate}%`, icon: TrendingUp, variant: "emerald" as const, description: `${stats.closedLeads} of ${stats.totalLeads} leads` },
|
||||
]
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -55,17 +63,20 @@ export default function DashboardPage() {
|
||||
</PageHeader>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
{statCards.map((card, i) => (
|
||||
<StatCard key={card.title} {...card} index={i} />
|
||||
))}
|
||||
{stats
|
||||
? statCards.map((card, i) => (
|
||||
<StatCard key={card.title} {...card} index={i} />
|
||||
))
|
||||
: Array.from({ length: 6 }).map((_, i) => <StatCardSkeleton key={i} />)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<LeadStatusChart data={stats.statusDistribution} />
|
||||
<LeadsPerMonthChart data={stats.leadsPerMonth} />
|
||||
<LeadStatusChart data={stats?.statusDistribution ?? []} />
|
||||
<LeadsPerMonthChart data={stats?.leadsPerMonth ?? []} />
|
||||
</div>
|
||||
|
||||
<RecentLeadsTable leads={stats.recentLeads} />
|
||||
<RecentLeadsTable leads={stats?.recentLeads ?? []} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,21 @@ import { LeadsTable } from "@/components/leads/leads-table"
|
||||
import { LeadsTableToolbar } from "@/components/leads/leads-table-toolbar"
|
||||
import { LeadFormDialog } from "@/components/leads/lead-form-dialog"
|
||||
import { leads } from "@/data/leads"
|
||||
import { filterLeadsByPeriod } from "@/lib/date-utils"
|
||||
|
||||
export default function LeadsPage() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [periodFilter, setPeriodFilter] = useState("all")
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
|
||||
const filteredLeads = useMemo(() => {
|
||||
let result = leads
|
||||
|
||||
if (periodFilter && periodFilter !== "all") {
|
||||
result = filterLeadsByPeriod(result, periodFilter)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const q = search.toLowerCase()
|
||||
result = result.filter(
|
||||
@@ -31,7 +37,7 @@ export default function LeadsPage() {
|
||||
}
|
||||
|
||||
return result
|
||||
}, [search, statusFilter])
|
||||
}, [search, statusFilter, periodFilter])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -47,6 +53,8 @@ export default function LeadsPage() {
|
||||
onSearchChange={setSearch}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
periodFilter={periodFilter}
|
||||
onPeriodFilterChange={setPeriodFilter}
|
||||
onCreateClick={() => setCreateOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,14 @@ import { useState, useMemo } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
|
||||
interface MonthlyData {
|
||||
month: string
|
||||
interface IntervalData {
|
||||
label: string
|
||||
leads: number
|
||||
closed: number
|
||||
}
|
||||
|
||||
interface LeadsPerMonthChartProps {
|
||||
data: MonthlyData[]
|
||||
data: IntervalData[]
|
||||
}
|
||||
|
||||
const BAR_GRADIENT_TOP = "#3B6AB8"
|
||||
@@ -138,7 +138,7 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) {
|
||||
|
||||
return (
|
||||
<g
|
||||
key={d.month}
|
||||
key={d.label}
|
||||
onMouseEnter={() => setHovered(i)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
style={{ cursor: "pointer" }}
|
||||
@@ -188,7 +188,7 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) {
|
||||
fontWeight={isActive ? 600 : 400}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{d.month}
|
||||
{d.label}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
@@ -216,7 +216,7 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) {
|
||||
}}
|
||||
>
|
||||
<div className="mb-1.5 text-xs font-semibold" style={{ color: "hsl(var(--foreground))" }}>
|
||||
{activeDatum.month}
|
||||
{activeDatum.label}
|
||||
</div>
|
||||
<div className="mb-1 flex items-center justify-between gap-4 text-xs" style={{ color: "hsl(var(--muted-foreground))" }}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -10,14 +9,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Search, Plus, SlidersHorizontal } from "lucide-react"
|
||||
import { LeadStatus } from "@/types"
|
||||
import { Search, Plus } from "lucide-react"
|
||||
|
||||
interface LeadsTableToolbarProps {
|
||||
search: string
|
||||
onSearchChange: (value: string) => void
|
||||
statusFilter: string
|
||||
onStatusFilterChange: (value: string) => void
|
||||
periodFilter: string
|
||||
onPeriodFilterChange: (value: string) => void
|
||||
onCreateClick: () => void
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export function LeadsTableToolbar({
|
||||
onSearchChange,
|
||||
statusFilter,
|
||||
onStatusFilterChange,
|
||||
periodFilter,
|
||||
onPeriodFilterChange,
|
||||
onCreateClick,
|
||||
}: LeadsTableToolbarProps) {
|
||||
return (
|
||||
@@ -40,6 +42,18 @@ export function LeadsTableToolbar({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={periodFilter} onValueChange={onPeriodFilterChange}>
|
||||
<SelectTrigger className="h-9 w-[150px]">
|
||||
<SelectValue placeholder="All Time" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Time</SelectItem>
|
||||
<SelectItem value="7days">Last 7 days</SelectItem>
|
||||
<SelectItem value="30days">Last 30 days</SelectItem>
|
||||
<SelectItem value="6months">Last 6 months</SelectItem>
|
||||
<SelectItem value="12months">Last 12 months</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger className="h-9 w-[140px]">
|
||||
<SelectValue placeholder="All Statuses" />
|
||||
@@ -53,10 +67,6 @@ export function LeadsTableToolbar({
|
||||
<SelectItem value="ignored">Ignored</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" className="h-9 gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Filters</span>
|
||||
</Button>
|
||||
<Button size="sm" className="h-9 gap-2" onClick={onCreateClick}>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Create Lead</span>
|
||||
|
||||
+45
-43
@@ -1,52 +1,54 @@
|
||||
import { DashboardStats } from "@/types"
|
||||
import { leads } from "./leads"
|
||||
import { filterLeadsByPeriod, groupLeadsByInterval } from "@/lib/date-utils"
|
||||
|
||||
function getLeadsPerMonth() {
|
||||
const months: { month: string; leads: number; closed: number }[] = []
|
||||
const now = new Date()
|
||||
const periodLabels: Record<string, string> = {
|
||||
"7days": "Last 7 days",
|
||||
"30days": "Last 30 days",
|
||||
"6months": "Last 6 months",
|
||||
"12months": "Last 12 months",
|
||||
}
|
||||
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
|
||||
const monthStr = d.toLocaleDateString("en-US", { month: "short", year: "2-digit" })
|
||||
const yearMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
|
||||
export function getDashboardStats(period: string): DashboardStats {
|
||||
const filtered = filterLeadsByPeriod(leads, period)
|
||||
const label = periodLabels[period] ?? "Selected period"
|
||||
|
||||
const leadsCount = leads.filter((l) => l.createdAt.startsWith(yearMonth)).length
|
||||
const closedCount = leads.filter(
|
||||
(l) => l.status === "closed" && l.updatedAt.startsWith(yearMonth)
|
||||
).length
|
||||
const totalLeads = filtered.length
|
||||
const openLeads = filtered.filter((l) => l.status === "open").length
|
||||
const contactedLeads = filtered.filter((l) => l.status === "contacted").length
|
||||
const pendingLeads = filtered.filter((l) => l.status === "pending").length
|
||||
const closedLeads = filtered.filter((l) => l.status === "closed").length
|
||||
const ignoredLeads = filtered.filter((l) => l.status === "ignored").length
|
||||
const conversionRate = totalLeads > 0
|
||||
? Math.round((closedLeads / totalLeads) * 100)
|
||||
: 0
|
||||
|
||||
months.push({ month: monthStr, leads: leadsCount, closed: closedCount })
|
||||
function getStatusDistribution() {
|
||||
const statusCounts: Record<string, number> = { open: 0, contacted: 0, pending: 0, closed: 0, ignored: 0 }
|
||||
filtered.forEach((l) => {
|
||||
statusCounts[l.status]++
|
||||
})
|
||||
|
||||
return [
|
||||
{ name: "Open", value: statusCounts.open, color: "#3b82f6" },
|
||||
{ name: "Contacted", value: statusCounts.contacted, color: "#f59e0b" },
|
||||
{ name: "Pending", value: statusCounts.pending, color: "#8b5cf6" },
|
||||
{ name: "Closed", value: statusCounts.closed, color: "#10b981" },
|
||||
{ name: "Ignored", value: statusCounts.ignored, color: "#6B7280" },
|
||||
]
|
||||
}
|
||||
|
||||
return months
|
||||
}
|
||||
|
||||
function getStatusDistribution() {
|
||||
const statusCounts: Record<string, number> = { open: 0, contacted: 0, pending: 0, closed: 0, ignored: 0 }
|
||||
leads.forEach((l) => {
|
||||
statusCounts[l.status]++
|
||||
})
|
||||
|
||||
return [
|
||||
{ name: "Open", value: statusCounts.open, color: "#3b82f6" },
|
||||
{ name: "Contacted", value: statusCounts.contacted, color: "#f59e0b" },
|
||||
{ name: "Pending", value: statusCounts.pending, color: "#8b5cf6" },
|
||||
{ name: "Closed", value: statusCounts.closed, color: "#10b981" },
|
||||
{ name: "Ignored", value: statusCounts.ignored, color: "#6B7280" },
|
||||
]
|
||||
}
|
||||
|
||||
export const dashboardStats: DashboardStats = {
|
||||
totalLeads: leads.length,
|
||||
openLeads: leads.filter((l) => l.status === "open").length,
|
||||
contactedLeads: leads.filter((l) => l.status === "contacted").length,
|
||||
pendingLeads: leads.filter((l) => l.status === "pending").length,
|
||||
closedLeads: leads.filter((l) => l.status === "closed").length,
|
||||
ignoredLeads: leads.filter((l) => l.status === "ignored").length,
|
||||
conversionRate: Math.round(
|
||||
(leads.filter((l) => l.status === "closed").length / leads.length) * 100
|
||||
),
|
||||
leadsPerMonth: getLeadsPerMonth(),
|
||||
recentLeads: leads.slice(0, 10),
|
||||
statusDistribution: getStatusDistribution(),
|
||||
return {
|
||||
totalLeads,
|
||||
openLeads,
|
||||
contactedLeads,
|
||||
pendingLeads,
|
||||
closedLeads,
|
||||
ignoredLeads,
|
||||
conversionRate,
|
||||
leadsPerMonth: groupLeadsByInterval(filtered, period),
|
||||
recentLeads: filtered.slice(0, 10),
|
||||
statusDistribution: getStatusDistribution(),
|
||||
periodLabel: label,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Lead } from "@/types"
|
||||
|
||||
export function getPeriodDateRange(period: string): { start: Date; end: Date } {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
|
||||
switch (period) {
|
||||
case "7days":
|
||||
start.setDate(start.getDate() - 7)
|
||||
break
|
||||
case "30days":
|
||||
start.setDate(start.getDate() - 30)
|
||||
break
|
||||
case "6months":
|
||||
start.setMonth(start.getMonth() - 6)
|
||||
break
|
||||
case "12months":
|
||||
start.setMonth(start.getMonth() - 12)
|
||||
break
|
||||
default:
|
||||
start.setMonth(start.getMonth() - 6)
|
||||
}
|
||||
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
export function filterLeadsByPeriod(leads: Lead[], period: string): Lead[] {
|
||||
const { start, end } = getPeriodDateRange(period)
|
||||
return leads.filter((l) => {
|
||||
const d = new Date(l.createdAt)
|
||||
return d >= start && d <= end
|
||||
})
|
||||
}
|
||||
|
||||
export function groupLeadsByInterval(
|
||||
leads: Lead[],
|
||||
period: string
|
||||
): { label: string; leads: number; closed: number }[] {
|
||||
const { start, end } = getPeriodDateRange(period)
|
||||
|
||||
if (period === "7days" || period === "30days") {
|
||||
return groupByDay(leads, start, end)
|
||||
}
|
||||
return groupByMonth(leads, start, end)
|
||||
}
|
||||
|
||||
function groupByDay(
|
||||
leads: Lead[],
|
||||
start: Date,
|
||||
end: Date
|
||||
): { label: string; leads: number; closed: number }[] {
|
||||
const result: { label: string; leads: number; closed: number }[] = []
|
||||
const current = new Date(start)
|
||||
|
||||
while (current <= end) {
|
||||
const dateStr = current.toISOString().slice(0, 10)
|
||||
const dayLeads = leads.filter((l) => l.createdAt.startsWith(dateStr))
|
||||
const closedCount = dayLeads.filter((l) => l.status === "closed").length
|
||||
|
||||
result.push({
|
||||
label: current.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
|
||||
leads: dayLeads.length,
|
||||
closed: closedCount,
|
||||
})
|
||||
|
||||
current.setDate(current.getDate() + 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function groupByMonth(
|
||||
leads: Lead[],
|
||||
start: Date,
|
||||
end: Date
|
||||
): { label: string; leads: number; closed: number }[] {
|
||||
const result: { label: string; leads: number; closed: number }[] = []
|
||||
const current = new Date(start.getFullYear(), start.getMonth(), 1)
|
||||
|
||||
while (current <= end) {
|
||||
const yearMonth = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}`
|
||||
const monthLeads = leads.filter((l) => l.createdAt.startsWith(yearMonth))
|
||||
const closedCount = monthLeads.filter((l) => l.status === "closed").length
|
||||
|
||||
result.push({
|
||||
label: current.toLocaleDateString("en-US", { month: "short", year: "2-digit" }),
|
||||
leads: monthLeads.length,
|
||||
closed: closedCount,
|
||||
})
|
||||
|
||||
current.setMonth(current.getMonth() + 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+2
-1
@@ -46,9 +46,10 @@ export interface DashboardStats {
|
||||
closedLeads: number
|
||||
ignoredLeads: number
|
||||
conversionRate: number
|
||||
leadsPerMonth: { month: string; leads: number; closed: number }[]
|
||||
leadsPerMonth: { label: string; leads: number; closed: number }[]
|
||||
recentLeads: Lead[]
|
||||
statusDistribution: { name: string; value: number; color: string }[]
|
||||
periodLabel: string
|
||||
}
|
||||
|
||||
export interface ColumnFilter {
|
||||
|
||||
Reference in New Issue
Block a user