Current state
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Lead } from "@/types"
|
||||
|
||||
interface DeleteLeadDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
lead: Lead
|
||||
}
|
||||
|
||||
export function DeleteLeadDialog({ open, onOpenChange, lead }: DeleteLeadDialogProps) {
|
||||
const handleDelete = () => {
|
||||
console.log("Delete lead:", lead.id)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Lead</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete <strong>{lead.companyName}</strong>? This action
|
||||
cannot be undone. All associated notes and data will be permanently removed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Lead } from "@/types"
|
||||
import { LeadFormDialog } from "./lead-form-dialog"
|
||||
import { DeleteLeadDialog } from "./delete-lead-dialog"
|
||||
import { MoreHorizontal, Eye, Edit, Trash2, UserCheck } from "lucide-react"
|
||||
|
||||
interface LeadActionsDropdownProps {
|
||||
lead: Lead
|
||||
}
|
||||
|
||||
export function LeadActionsDropdown({ lead }: LeadActionsDropdownProps) {
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/leads/${lead.id}`}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setEditOpen(true)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<UserCheck className="mr-2 h-4 w-4" />
|
||||
Assign
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => setDeleteOpen(true)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<LeadFormDialog open={editOpen} onOpenChange={setEditOpen} lead={lead} />
|
||||
<DeleteLeadDialog open={deleteOpen} onOpenChange={setDeleteOpen} lead={lead} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { LeadStatusBadge } from "./lead-status-badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Lead } from "@/types"
|
||||
import {
|
||||
Building2,
|
||||
Mail,
|
||||
Phone,
|
||||
Globe,
|
||||
User,
|
||||
CalendarDays,
|
||||
Clock,
|
||||
Tag,
|
||||
} from "lucide-react"
|
||||
|
||||
interface LeadDetailsCardProps {
|
||||
lead: Lead
|
||||
}
|
||||
|
||||
export function LeadDetailsCard({ lead }: LeadDetailsCardProps) {
|
||||
const fields = [
|
||||
{ icon: Building2, label: "Company", value: lead.companyName },
|
||||
{ icon: User, label: "Contact", value: lead.contactName },
|
||||
{ icon: Mail, label: "Email", value: lead.email },
|
||||
{ icon: Phone, label: "Phone", value: lead.phone || "—" },
|
||||
{ icon: Globe, label: "Source", value: lead.source || "—" },
|
||||
{ icon: Tag, label: "Status", value: <LeadStatusBadge status={lead.status} /> },
|
||||
{ icon: CalendarDays, label: "Created", value: new Date(lead.createdAt).toLocaleDateString() },
|
||||
{ icon: Clock, label: "Updated", value: new Date(lead.updatedAt).toLocaleDateString() },
|
||||
]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lead Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{fields.map((field, i) => (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted">
|
||||
<field.icon className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-muted-foreground">{field.label}</p>
|
||||
<div className="text-sm font-medium">{field.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{lead.description && (
|
||||
<div className="mt-6 border-t pt-6">
|
||||
<h4 className="mb-2 text-sm font-medium text-muted-foreground">Description</h4>
|
||||
<p className="text-sm leading-relaxed">{lead.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 border-t pt-6">
|
||||
<h4 className="mb-3 text-sm font-medium text-muted-foreground">Assigned User</h4>
|
||||
{lead.assignedUser ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={lead.assignedUser.avatar} />
|
||||
<AvatarFallback>{lead.assignedUser.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{lead.assignedUser.name}</p>
|
||||
<Badge variant="secondary" className="mt-0.5 text-xs">
|
||||
{lead.assignedUser.role}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No user assigned</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import * as z from "zod"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Lead, LeadStatus, User } from "@/types"
|
||||
import { LEAD_SOURCES, LEAD_STATUSES } from "@/lib/constants"
|
||||
import { useNotifications } from "@/providers/notification-provider"
|
||||
|
||||
const leadFormSchema = z.object({
|
||||
companyName: z.string().min(1, "Company name is required"),
|
||||
contactName: z.string().min(1, "Contact name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
phone: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.string(),
|
||||
assignedUserId: z.string().optional(),
|
||||
})
|
||||
|
||||
type LeadFormValues = z.infer<typeof leadFormSchema>
|
||||
|
||||
interface LeadFormDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
lead?: Lead | null
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export function LeadFormDialog({ open, onOpenChange, lead, onSuccess }: LeadFormDialogProps) {
|
||||
const isEditing = !!lead
|
||||
const { addNotification } = useNotifications()
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/users")
|
||||
.then((r) => r.json())
|
||||
.then((data) => setUsers(data.users || []))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const form = useForm<LeadFormValues>({
|
||||
resolver: zodResolver(leadFormSchema),
|
||||
defaultValues: {
|
||||
companyName: "",
|
||||
contactName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
source: "",
|
||||
description: "",
|
||||
status: "open",
|
||||
assignedUserId: "none",
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (lead) {
|
||||
form.reset({
|
||||
companyName: lead.companyName,
|
||||
contactName: lead.contactName,
|
||||
email: lead.email,
|
||||
phone: lead.phone || "",
|
||||
source: lead.source || "",
|
||||
description: lead.description || "",
|
||||
status: lead.status,
|
||||
assignedUserId: lead.assignedUserId || "none",
|
||||
})
|
||||
} else {
|
||||
form.reset({
|
||||
companyName: "",
|
||||
contactName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
source: "",
|
||||
description: "",
|
||||
status: "open",
|
||||
assignedUserId: "none",
|
||||
})
|
||||
}
|
||||
}, [lead, form])
|
||||
|
||||
async function onSubmit(values: LeadFormValues) {
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload = {
|
||||
...values,
|
||||
assignedUserId: values.assignedUserId === "none" ? null : values.assignedUserId,
|
||||
}
|
||||
if (isEditing && lead) {
|
||||
const res = await fetch(`/api/leads/${lead.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (!res.ok) throw new Error("Failed to update")
|
||||
addNotification("lead_status_changed", "Lead Updated", `${values.companyName}`, "#")
|
||||
} else {
|
||||
const res = await fetch("/api/leads", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (!res.ok) throw new Error("Failed to create")
|
||||
addNotification("lead_created", "New Lead Created", `${values.companyName} — ${values.contactName}`, "#")
|
||||
}
|
||||
onOpenChange(false)
|
||||
onSuccess?.()
|
||||
} catch (e) {
|
||||
console.error("Lead save error:", e)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? "Edit Lead" : "Create New Lead"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing
|
||||
? "Update the lead information below."
|
||||
: "Fill in the details to add a new lead."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="companyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Company Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Acme Corp" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contactName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Contact Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john@acme.com" type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Phone</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="(555) 123-4567" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="source"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Source</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select source" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{LEAD_SOURCES.map((source) => (
|
||||
<SelectItem key={source} value={source}>
|
||||
{source}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.entries(LEAD_STATUSES).map(([key, { label }]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Brief description of the lead and their requirements..."
|
||||
className="min-h-[100px]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="assignedUserId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Assign To</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select user" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Unassigned</SelectItem>
|
||||
{users.filter(u => u.active).map((user) => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.name} ({user.role})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving}>{saving ? "Saving..." : isEditing ? "Save Changes" : "Create Lead"}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { LeadStatus } from "@/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const statusConfig: Record<LeadStatus, { label: string; class: string }> = {
|
||||
open: {
|
||||
label: "Open",
|
||||
class: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
|
||||
},
|
||||
contacted: {
|
||||
label: "Contacted",
|
||||
class: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
|
||||
},
|
||||
pending: {
|
||||
label: "Pending",
|
||||
class: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20",
|
||||
},
|
||||
closed: {
|
||||
label: "Closed",
|
||||
class: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
|
||||
},
|
||||
ignored: {
|
||||
label: "Ignored",
|
||||
class: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20",
|
||||
},
|
||||
}
|
||||
|
||||
interface LeadStatusBadgeProps {
|
||||
status: LeadStatus
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LeadStatusBadge({ status, className }: LeadStatusBadgeProps) {
|
||||
const config = statusConfig[status]
|
||||
return (
|
||||
<Badge variant="outline" className={cn(config.class, className)}>
|
||||
<span className={cn("mr-1.5 h-1.5 w-1.5 rounded-full", {
|
||||
"bg-blue-500": status === "open",
|
||||
"bg-amber-500": status === "contacted",
|
||||
"bg-purple-500": status === "pending",
|
||||
"bg-emerald-500": status === "closed",
|
||||
"bg-zinc-500": status === "ignored",
|
||||
})} />
|
||||
{config.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
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
|
||||
}
|
||||
|
||||
export function LeadsTableToolbar({
|
||||
search,
|
||||
onSearchChange,
|
||||
statusFilter,
|
||||
onStatusFilterChange,
|
||||
periodFilter,
|
||||
onPeriodFilterChange,
|
||||
onCreateClick,
|
||||
}: LeadsTableToolbarProps) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by name, company, email..."
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="h-9 pl-9 w-full sm:max-w-sm"
|
||||
/>
|
||||
</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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Statuses</SelectItem>
|
||||
<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 size="sm" className="h-9 gap-2" onClick={onCreateClick}>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Create Lead</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { DataTable } from "@/components/shared/data-table"
|
||||
import { DataTableColumnHeader } from "@/components/shared/data-table-column-header"
|
||||
import { LeadStatusBadge } from "./lead-status-badge"
|
||||
import { LeadActionsDropdown } from "./lead-actions-dropdown"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Lead } from "@/types"
|
||||
|
||||
interface LeadsTableProps {
|
||||
data: Lead[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function LeadsTable({ data, loading }: LeadsTableProps) {
|
||||
const columns: ColumnDef<Lead>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "companyName",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Company" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
href={`/leads/${row.original.id}`}
|
||||
className="font-medium hover:text-primary transition-colors"
|
||||
>
|
||||
{row.getValue("companyName")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "contactName",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Contact" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">{row.getValue("contactName")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Email" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">{row.getValue("email")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "phone",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Phone" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">{row.getValue("phone")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => <LeadStatusBadge status={row.getValue("status")} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "assignedUser",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Assigned" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original.assignedUser
|
||||
if (!user) return <span className="text-muted-foreground/50">—</span>
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{user.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-muted-foreground text-sm">{user.name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Date Added" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">
|
||||
{new Date(row.getValue("createdAt")).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => <LeadActionsDropdown lead={row.original} />,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
loading={loading}
|
||||
emptyMessage="No leads found."
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user