Initial commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { StatCard } from "@/components/dashboard/stat-card"
|
||||
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 {
|
||||
Users,
|
||||
UserPlus,
|
||||
PhoneCall,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
TrendingUp,
|
||||
ListFilter,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
|
||||
export default function DashboardPage() {
|
||||
const stats = dashboardStats
|
||||
const [period, setPeriod] = useState("6months")
|
||||
|
||||
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` },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="Dashboard" description="Overview of your sales pipeline">
|
||||
<Select value={period} onValueChange={setPeriod}>
|
||||
<SelectTrigger className="h-9 w-[160px]">
|
||||
<ListFilter className="mr-2 h-4 w-4" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<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>
|
||||
</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} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<LeadStatusChart data={stats.statusDistribution} />
|
||||
<LeadsPerMonthChart data={stats.leadsPerMonth} />
|
||||
</div>
|
||||
|
||||
<RecentLeadsTable leads={stats.recentLeads} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { AppShell } from "@/components/layout/app-shell"
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <AppShell>{children}</AppShell>
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { motion } from "framer-motion"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { LeadDetailsCard } from "@/components/leads/lead-details-card"
|
||||
import { LeadStatusBadge } from "@/components/leads/lead-status-badge"
|
||||
import { NoteTimeline } from "@/components/notes/note-timeline"
|
||||
import { NoteForm } from "@/components/notes/note-form"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { getLeadById } from "@/data/leads"
|
||||
import { getNotesByLeadId } from "@/data/notes"
|
||||
import { ArrowLeft, Edit, ExternalLink } from "lucide-react"
|
||||
|
||||
export default function LeadDetailsPage({ params }: { params: { id: string } }) {
|
||||
const lead = getLeadById(params.id)
|
||||
const leadNotes = lead ? getNotesByLeadId(lead.id) : []
|
||||
|
||||
if (!lead) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh]">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold">Lead not found</h2>
|
||||
<p className="mt-2 text-muted-foreground">The lead you are looking for does not exist.</p>
|
||||
<Link href="/leads" className="mt-4 inline-block">
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Leads
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Link
|
||||
href="/leads"
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to leads
|
||||
</Link>
|
||||
|
||||
<PageHeader
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<span>{lead.companyName}</span>
|
||||
<LeadStatusBadge status={lead.status} />
|
||||
</div>
|
||||
}
|
||||
description={lead.contactName}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select defaultValue={lead.status}>
|
||||
<SelectTrigger className="h-9 w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="open">Open</SelectItem>
|
||||
<SelectItem value="contacted">Contacted</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="closed">Closed</SelectItem>
|
||||
<SelectItem value="ignored">Ignored</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button className="gap-2">
|
||||
<Edit className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<LeadDetailsCard lead={lead} />
|
||||
<div className="space-y-6">
|
||||
<NoteForm leadId={lead.id} />
|
||||
<NoteTimeline notes={leadNotes} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="border-b px-6 py-4">
|
||||
<h3 className="font-semibold">Activity</h3>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{[
|
||||
{ action: "Lead created", date: lead.createdAt, user: lead.assignedUser?.name || "System" },
|
||||
...leadNotes.slice(0, 3).map((n) => ({
|
||||
action: "Note added",
|
||||
date: n.createdAt,
|
||||
user: n.authorName,
|
||||
})),
|
||||
].map((activity, i) => (
|
||||
<div key={i} className="px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-primary/60 shrink-0" />
|
||||
<p className="text-sm">{activity.action}</p>
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground pl-4">
|
||||
by {activity.user} · {new Date(activity.date).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="border-b px-6 py-4">
|
||||
<h3 className="font-semibold">Quick Actions</h3>
|
||||
</div>
|
||||
<div className="p-4 space-y-2">
|
||||
<Button variant="outline" className="w-full justify-start" size="sm">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Send Email
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full justify-start" size="sm">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Call Lead
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo } from "react"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
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"
|
||||
|
||||
export default function LeadsPage() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
|
||||
const filteredLeads = useMemo(() => {
|
||||
let result = leads
|
||||
|
||||
if (search) {
|
||||
const q = search.toLowerCase()
|
||||
result = result.filter(
|
||||
(l) =>
|
||||
l.companyName.toLowerCase().includes(q) ||
|
||||
l.contactName.toLowerCase().includes(q) ||
|
||||
l.email.toLowerCase().includes(q) ||
|
||||
l.phone.includes(q)
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter && statusFilter !== "all") {
|
||||
result = result.filter((l) => l.status === statusFilter)
|
||||
}
|
||||
|
||||
return result
|
||||
}, [search, statusFilter])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader
|
||||
title="Leads"
|
||||
description="Manage and track your sales leads"
|
||||
/>
|
||||
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="p-4">
|
||||
<LeadsTableToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onCreateClick={() => setCreateOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<LeadsTable data={filteredLeads} />
|
||||
</div>
|
||||
|
||||
<LeadFormDialog open={createOpen} onOpenChange={setCreateOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { CompanySettingsForm } from "@/components/settings/company-settings-form"
|
||||
import { UserPreferencesForm } from "@/components/settings/user-preferences-form"
|
||||
import { ThemeSettings } from "@/components/settings/theme-settings"
|
||||
import { NotificationSettings } from "@/components/settings/notification-settings"
|
||||
import { Building2, User, Palette, Bell } from "lucide-react"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const tabs = [
|
||||
{ value: "company", label: "Company", icon: Building2, component: CompanySettingsForm },
|
||||
{ value: "preferences", label: "Preferences", icon: User, component: UserPreferencesForm },
|
||||
{ value: "theme", label: "Theme", icon: Palette, component: ThemeSettings },
|
||||
{ value: "notifications", label: "Notifications", icon: Bell, component: NotificationSettings },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader
|
||||
title="Settings"
|
||||
description="Manage your CRM configuration and preferences"
|
||||
/>
|
||||
|
||||
<Tabs defaultValue="company" className="space-y-6">
|
||||
<TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value} className="gap-2">
|
||||
<tab.icon className="h-4 w-4" />
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<TabsContent key={tab.value} value={tab.value}>
|
||||
<tab.component />
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { PageHeader } from "@/components/shared/page-header"
|
||||
import { UsersTable } from "@/components/users/users-table"
|
||||
import { UserFormDialog } from "@/components/users/user-form-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { users } from "@/data/users"
|
||||
import { Plus, Users as UsersIcon } from "lucide-react"
|
||||
|
||||
export default function UsersPage() {
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader
|
||||
title="Users"
|
||||
description="Manage team members and their roles"
|
||||
>
|
||||
<Button className="gap-2" onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add User
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-6">
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10">
|
||||
<UsersIcon className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{users.length}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10">
|
||||
<UsersIcon className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{users.filter((u) => u.active).length}</p>
|
||||
<p className="text-xs text-muted-foreground">Active</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10">
|
||||
<UsersIcon className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{users.filter((u) => u.role === "admin").length}</p>
|
||||
<p className="text-xs text-muted-foreground">Admins</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/10">
|
||||
<UsersIcon className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{users.filter((u) => u.role === "sales").length}</p>
|
||||
<p className="text-xs text-muted-foreground">Sales</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card">
|
||||
<UsersTable data={users} />
|
||||
</div>
|
||||
|
||||
<UserFormDialog open={createOpen} onOpenChange={setCreateOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user