mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 11:15:43 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { DashboardStats } from "@/types"
|
||||
import { leads } from "./leads"
|
||||
|
||||
function getLeadsPerMonth() {
|
||||
const months: { month: string; leads: number; closed: number }[] = []
|
||||
const now = new Date()
|
||||
|
||||
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")}`
|
||||
|
||||
const leadsCount = leads.filter((l) => l.createdAt.startsWith(yearMonth)).length
|
||||
const closedCount = leads.filter(
|
||||
(l) => l.status === "closed" && l.updatedAt.startsWith(yearMonth)
|
||||
).length
|
||||
|
||||
months.push({ month: monthStr, leads: leadsCount, closed: closedCount })
|
||||
}
|
||||
|
||||
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: "#71717a" },
|
||||
]
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Lead, LeadStatus } from "@/types"
|
||||
import { users } from "./users"
|
||||
|
||||
const companyNames = [
|
||||
"Brightwave Studios", "Nexus Digital", "Pinnacle Web Solutions", "Vertex Media Group",
|
||||
"Apex Interactive", "Meridian Design Co", "Titan Software", "Crafted Web Agency",
|
||||
"Driftwood Creative", "Summit Digital Labs", "Horizon Interactive", "Pulse Marketing",
|
||||
"Ironclad Systems", "Northstar Digital", "Vanguard Web Services", "Cascade Solutions",
|
||||
"Atlas Creative Group", "Precision Web Co", "Momentum Digital", "Catalyst Agency",
|
||||
"Ridgewood Tech", "Silverline Interactive", "Harbor Media Group", "Providence Web",
|
||||
"Sentinel Systems", "Legacy Digital Agency", "Frontier Creative", "Summit Point Tech",
|
||||
"Barrel House Digital", "Anchor Web Services", "Trinity Media", "Skyline Interactive",
|
||||
"Cornerstone Digital", "Horizon Web Craft", "Noble Systems", "Crescent Creative",
|
||||
"Blue Ridge Digital", "Pathfinder Web Co", "Ravenswood Agency", "Sterling Media Group",
|
||||
"Cedar Creek Interactive", "Iron Gate Digital", "Fox Run Agency", "Oakwood Systems",
|
||||
"Clover Creative", "Birchwood Digital", "Stone Ridge Media", "Riverbend Tech",
|
||||
"Fairway Web Services", "Heritage Digital Group", "Maple Leaf Interactive", "Granite Systems",
|
||||
"Portside Creative", "Westwind Digital", "Crestview Agency", "Sunburst Media",
|
||||
"Broadwood Systems", "Highland Interactive", "Clearwater Web Co", "Emerald Digital",
|
||||
]
|
||||
|
||||
const firstNames = ["James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda", "David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah", "Christopher", "Karen", "Daniel", "Lisa", "Matthew", "Nancy", "Anthony", "Betty", "Mark", "Margaret", "Donald", "Sandra", "Steven", "Ashley", "Andrew", "Kimberly", "Paul", "Emily", "Joshua", "Donna", "Kenneth", "Michelle", "Kevin", "Carol", "Brian", "Amanda", "George", "Melissa", "Timothy", "Deborah", "Ronald", "Stephanie"]
|
||||
|
||||
const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", "Carter", "Roberts"]
|
||||
|
||||
const sources = ["Website", "Referral", "LinkedIn", "Cold Call", "Email", "Google", "Social Media", "Other"]
|
||||
const statuses: LeadStatus[] = ["open", "contacted", "pending", "closed", "ignored"]
|
||||
|
||||
function randomItem<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
function randomDate(startMonthsAgo: number): string {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth() - startMonthsAgo, 1)
|
||||
const randomTime = start.getTime() + Math.random() * (now.getTime() - start.getTime())
|
||||
return new Date(randomTime).toISOString()
|
||||
}
|
||||
|
||||
const phonePrefixes = ["555", "123", "456", "789", "321", "654", "987", "234", "876", "432"]
|
||||
|
||||
function randomPhone(): string {
|
||||
const prefix = randomItem(phonePrefixes)
|
||||
const suffix = Math.floor(Math.random() * 10000000).toString().padStart(7, "0")
|
||||
return `(${prefix}) ${suffix.slice(0, 3)}-${suffix.slice(3)}`
|
||||
}
|
||||
|
||||
export const leads: Lead[] = Array.from({ length: 120 }, (_, i) => {
|
||||
const firstName = randomItem(firstNames)
|
||||
const lastName = randomItem(lastNames)
|
||||
const status = randomItem(statuses)
|
||||
const assignedUser = Math.random() > 0.15 ? randomItem(users.filter((u) => u.active)) : null
|
||||
const createdAt = randomDate(6)
|
||||
|
||||
return {
|
||||
id: `lead-${String(i + 1).padStart(3, "0")}`,
|
||||
companyName: companyNames[i % companyNames.length],
|
||||
contactName: `${firstName} ${lastName}`,
|
||||
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${companyNames[i % companyNames.length].toLowerCase().replace(/\s+/g, "")}.com`,
|
||||
phone: randomPhone(),
|
||||
source: randomItem(sources),
|
||||
description: `Looking for a complete website redesign and modern digital presence. Interested in responsive design, SEO optimization, and a custom CMS solution.`,
|
||||
status,
|
||||
assignedUserId: assignedUser?.id ?? null,
|
||||
assignedUser,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
}
|
||||
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
|
||||
export function getLeadById(id: string): Lead | undefined {
|
||||
return leads.find((l) => l.id === id)
|
||||
}
|
||||
|
||||
export function getLeadsByStatus(status: LeadStatus): Lead[] {
|
||||
return leads.filter((l) => l.status === status)
|
||||
}
|
||||
|
||||
export function searchLeads(query: string): Lead[] {
|
||||
const q = query.toLowerCase()
|
||||
return leads.filter(
|
||||
(l) =>
|
||||
l.companyName.toLowerCase().includes(q) ||
|
||||
l.contactName.toLowerCase().includes(q) ||
|
||||
l.email.toLowerCase().includes(q) ||
|
||||
l.phone.includes(q)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Note } from "@/types"
|
||||
import { leads } from "./leads"
|
||||
import { users } from "./users"
|
||||
|
||||
const noteTemplates = [
|
||||
"Called the prospect to discuss their requirements. They are looking for a complete website overhaul with e-commerce capabilities.",
|
||||
"Sent over the proposal and pricing breakdown. Waiting for their feedback on the package options.",
|
||||
"Followed up via email. They mentioned they're comparing a few agencies but liked our portfolio.",
|
||||
"Had a discovery call. They need the project completed within 8 weeks. Budget seems flexible.",
|
||||
"Sent a customized demo of our recent e-commerce work. They were particularly impressed with the checkout flow.",
|
||||
"Received their requirements document. Scope includes 12 pages, blog integration, and a contact form.",
|
||||
"Meeting scheduled for next Tuesday to discuss technical specifications in detail.",
|
||||
"They requested references from similar projects. Sent over three case studies.",
|
||||
"Discussed hosting options and ongoing maintenance packages. They're interested in a monthly retainer.",
|
||||
"Sent over the contract for signature. Waiting for their legal team to review.",
|
||||
"They asked about our experience with custom CRM integrations. Shared relevant project examples.",
|
||||
"Follow-up call completed. They have some additional questions about the timeline.",
|
||||
"Exchanged emails about the design direction. They prefer a minimalist approach.",
|
||||
"Discussed SEO strategy and content creation. They want to handle content themselves.",
|
||||
"They requested a revised quote for a smaller scope. Adjusted the proposal accordingly.",
|
||||
"Had a productive call about their target audience and user personas.",
|
||||
"Sent over wireframes for the homepage. Waiting for their feedback.",
|
||||
"They mentioned a referral from one of our past clients - good rapport building.",
|
||||
"Discussed payment terms. They prefer milestone-based payments.",
|
||||
"They're ready to move forward! Sending the final contract today.",
|
||||
"Checked in to see if they had any questions about the proposal. No response yet.",
|
||||
"Left a voicemail. Will try again next week.",
|
||||
"They mentioned their budget is slightly lower than our minimum. Discussed scope adjustments.",
|
||||
"Great meeting! They love our approach and want to start as soon as possible.",
|
||||
"They need to get approval from their partner before proceeding.",
|
||||
]
|
||||
|
||||
function randomDateNear(leadDate: string, maxDaysAfter: number): string {
|
||||
const base = new Date(leadDate)
|
||||
const offset = Math.floor(Math.random() * maxDaysAfter * 24 * 60 * 60 * 1000)
|
||||
return new Date(base.getTime() + offset).toISOString()
|
||||
}
|
||||
|
||||
export const notes: Note[] = leads.flatMap((lead) => {
|
||||
const leadNotes: Note[] = []
|
||||
const numNotes = Math.floor(Math.random() * 4) + 1
|
||||
|
||||
const assignedUser = lead.assignedUser ?? users[0]
|
||||
|
||||
for (let i = 0; i < numNotes; i++) {
|
||||
const author = Math.random() > 0.3 ? assignedUser : users[Math.floor(Math.random() * users.length)]
|
||||
const createdAt = randomDateNear(lead.createdAt, 45)
|
||||
leadNotes.push({
|
||||
id: `note-${lead.id}-${i}`,
|
||||
leadId: lead.id,
|
||||
userId: author.id,
|
||||
authorName: author.name,
|
||||
authorAvatar: author.avatar,
|
||||
authorRole: author.role,
|
||||
note: noteTemplates[Math.floor(Math.random() * noteTemplates.length)],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
}
|
||||
|
||||
return leadNotes
|
||||
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
|
||||
export function getNotesByLeadId(leadId: string): Note[] {
|
||||
return notes.filter((n) => n.leadId === leadId).sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { User } from "@/types"
|
||||
|
||||
export const users: User[] = [
|
||||
{
|
||||
id: "u1",
|
||||
name: "Sarah Chen",
|
||||
email: "sarah@coastalit.com",
|
||||
role: "admin",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=Sarah+Chen&background=1d4ed8&color=fff&size=128",
|
||||
createdAt: "2025-01-15T08:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u2",
|
||||
name: "Marcus Johnson",
|
||||
email: "marcus@coastalit.com",
|
||||
role: "sales",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=Marcus+Johnson&background=2563eb&color=fff&size=128",
|
||||
createdAt: "2025-02-01T09:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u3",
|
||||
name: "Emily Rodriguez",
|
||||
email: "emily@coastalit.com",
|
||||
role: "sales",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=Emily+Rodriguez&background=3b82f6&color=fff&size=128",
|
||||
createdAt: "2025-02-15T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u4",
|
||||
name: "David Kim",
|
||||
email: "david@coastalit.com",
|
||||
role: "sales",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=David+Kim&background=60a5fa&color=fff&size=128",
|
||||
createdAt: "2025-03-01T08:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u5",
|
||||
name: "Jessica Patel",
|
||||
email: "jessica@coastalit.com",
|
||||
role: "sales",
|
||||
active: false,
|
||||
avatar: "https://ui-avatars.com/api/?name=Jessica+Patel&background=93c5fd&color=fff&size=128",
|
||||
createdAt: "2025-03-10T09:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u6",
|
||||
name: "Alex Thompson",
|
||||
email: "alex@coastalit.com",
|
||||
role: "sales",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=Alex+Thompson&background=1d4ed8&color=fff&size=128",
|
||||
createdAt: "2025-04-01T08:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u7",
|
||||
name: "Rachel Williams",
|
||||
email: "rachel@coastalit.com",
|
||||
role: "sales",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=Rachel+Williams&background=2563eb&color=fff&size=128",
|
||||
createdAt: "2025-04-15T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "u8",
|
||||
name: "Tom Nakamura",
|
||||
email: "tom@coastalit.com",
|
||||
role: "admin",
|
||||
active: true,
|
||||
avatar: "https://ui-avatars.com/api/?name=Tom+Nakamura&background=3b82f6&color=fff&size=128",
|
||||
createdAt: "2025-05-01T09:00:00Z",
|
||||
},
|
||||
]
|
||||
|
||||
export const currentUser: User = users[0]
|
||||
|
||||
export function getUserById(id: string): User | undefined {
|
||||
return users.find((u) => u.id === id)
|
||||
}
|
||||
|
||||
export function getUsersByRole(role: "admin" | "sales"): User[] {
|
||||
return users.filter((u) => u.role === role)
|
||||
}
|
||||
|
||||
export function getActiveUsers(): User[] {
|
||||
return users.filter((u) => u.active)
|
||||
}
|
||||
Reference in New Issue
Block a user