354 lines
15 KiB
TypeScript
354 lines
15 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Users,
|
|
Search,
|
|
Filter,
|
|
Plus,
|
|
Eye,
|
|
Edit,
|
|
Mail,
|
|
MoreHorizontal,
|
|
Download,
|
|
Send,
|
|
UserCheck,
|
|
UserX,
|
|
Building,
|
|
Phone,
|
|
Mail as MailIcon,
|
|
Calendar,
|
|
DollarSign,
|
|
Server
|
|
} from 'lucide-react';
|
|
import { mockCustomers } from '../../data/mockData';
|
|
import DualCurrencyDisplay from '../../components/DualCurrencyDisplay';
|
|
import { formatDate, formatNumber } from '../../utils/format';
|
|
import { cn } from '../../utils/cn';
|
|
import Modal from '../../components/Modal';
|
|
import DetailView from '../../components/DetailView';
|
|
import AddCustomerForm from '../../components/forms/AddCustomerForm';
|
|
import EditCustomerForm from '../../components/forms/EditCustomerForm';
|
|
import MailComposeForm from '../../components/forms/MailComposeForm';
|
|
import MoreOptionsDropdown from '../../components/MoreOptionsDropdown';
|
|
|
|
const Customers: React.FC = () => {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedStatus, setSelectedStatus] = useState('all');
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
const [isMailModalOpen, setIsMailModalOpen] = useState(false);
|
|
const [showMoreOptions, setShowMoreOptions] = useState<string | null>(null);
|
|
const [selectedCustomer, setSelectedCustomer] = useState<any>(null);
|
|
|
|
const filteredCustomers = mockCustomers.filter(customer => {
|
|
const matchesSearch = customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
customer.email.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesStatus = selectedStatus === 'all' || customer.status === selectedStatus;
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
|
|
const stats = {
|
|
total: mockCustomers.length,
|
|
active: mockCustomers.filter(c => c.status === 'active').length,
|
|
pending: mockCustomers.filter(c => c.status === 'pending').length,
|
|
totalRevenue: mockCustomers.reduce((sum, c) => sum + c.totalSpent, 0)
|
|
};
|
|
|
|
const handleAddCustomer = (data: any) => {
|
|
console.log('Adding customer:', data);
|
|
setIsAddModalOpen(false);
|
|
};
|
|
|
|
const handleViewCustomer = (customer: any) => {
|
|
setSelectedCustomer(customer);
|
|
setIsDetailModalOpen(true);
|
|
};
|
|
|
|
const handleEditCustomer = (customer: any) => {
|
|
setSelectedCustomer(customer);
|
|
setIsEditModalOpen(true);
|
|
};
|
|
|
|
const handleMailCustomer = (customer: any) => {
|
|
setSelectedCustomer(customer);
|
|
setIsMailModalOpen(true);
|
|
};
|
|
|
|
const handleMoreOptions = (customerId: string) => {
|
|
setShowMoreOptions(showMoreOptions === customerId ? null : customerId);
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case 'active':
|
|
return <span className="badge badge-success">Active</span>;
|
|
case 'pending':
|
|
return <span className="badge badge-warning">Pending</span>;
|
|
case 'inactive':
|
|
return <span className="badge badge-danger">Inactive</span>;
|
|
default:
|
|
return <span className="badge badge-secondary">{status}</span>;
|
|
}
|
|
};
|
|
|
|
const getTierBadge = (tier: string) => {
|
|
switch (tier) {
|
|
case 'platinum':
|
|
return <span className="badge badge-primary">Platinum</span>;
|
|
case 'gold':
|
|
return <span className="badge badge-warning">Gold</span>;
|
|
case 'silver':
|
|
return <span className="badge badge-secondary">Silver</span>;
|
|
default:
|
|
return <span className="badge badge-secondary">{tier}</span>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-900/20">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-4 sm:space-y-0 mb-8">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">
|
|
Customers
|
|
</h1>
|
|
<p className="text-slate-600 dark:text-slate-400 mt-1">
|
|
Manage your customer relationships and accounts
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsAddModalOpen(true)}
|
|
className="btn btn-primary btn-lg"
|
|
>
|
|
<Plus className="w-5 h-5 mr-2" />
|
|
Add New Customer
|
|
</button>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-8">
|
|
<div className="card-elevated p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Customers</p>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.total}</p>
|
|
</div>
|
|
<div className="w-12 h-12 bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl flex items-center justify-center">
|
|
<Users className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-elevated p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Active Customers</p>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.active}</p>
|
|
</div>
|
|
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
|
<Users className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-elevated p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Pending</p>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">{stats.pending}</p>
|
|
</div>
|
|
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
|
|
<Users className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-elevated p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Total Revenue</p>
|
|
<DualCurrencyDisplay
|
|
amount={stats.totalRevenue}
|
|
currency="INR"
|
|
className="text-2xl font-bold"
|
|
/>
|
|
</div>
|
|
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
|
<DollarSign className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters and Search */}
|
|
<div className="card-elevated p-6 mb-8">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search customers..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="input pl-10 w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={selectedStatus}
|
|
onChange={(e) => setSelectedStatus(e.target.value)}
|
|
className="input"
|
|
>
|
|
<option value="all">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
<button className="btn btn-secondary btn-md">
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customers Table */}
|
|
<div className="card-elevated overflow-hidden">
|
|
<div className="table-responsive">
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Customer</th>
|
|
<th>Contact</th>
|
|
<th>Status</th>
|
|
<th>Tier</th>
|
|
<th>Total Spent</th>
|
|
<th>Instances</th>
|
|
<th>Last Active</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredCustomers.map((customer) => (
|
|
<tr key={customer.id}>
|
|
<td>
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-center">
|
|
<Building className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-slate-900 dark:text-white">{customer.name}</p>
|
|
<p className="text-sm text-slate-500 dark:text-slate-400">{customer.email}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div className="flex items-center space-x-2">
|
|
<Phone className="w-4 h-4 text-slate-400" />
|
|
<p className="text-sm text-slate-600 dark:text-slate-400">{customer.phone}</p>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{getStatusBadge(customer.status)}
|
|
</td>
|
|
<td>
|
|
{getTierBadge(customer.tier)}
|
|
</td>
|
|
<td>
|
|
<DualCurrencyDisplay
|
|
amount={customer.totalSpent}
|
|
currency="INR"
|
|
className="font-semibold"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<div className="flex items-center space-x-2">
|
|
<Server className="w-4 h-4 text-slate-400" />
|
|
<span className="text-sm font-medium">{customer.instances}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div className="flex items-center space-x-1">
|
|
<Calendar className="w-4 h-4 text-slate-400" />
|
|
<span className="text-sm">{formatDate(customer.lastActive)}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => handleViewCustomer(customer)}
|
|
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
|
title="View Details"
|
|
>
|
|
<Eye className="w-4 h-4 text-slate-600 dark:text-slate-400" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleEditCustomer(customer)}
|
|
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
|
title="Edit Customer"
|
|
>
|
|
<Edit className="w-4 h-4 text-slate-600 dark:text-slate-400" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleMailCustomer(customer)}
|
|
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
|
title="Send Email"
|
|
>
|
|
<Mail className="w-4 h-4 text-slate-600 dark:text-slate-400" />
|
|
</button>
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => handleMoreOptions(customer.id)}
|
|
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
|
title="More Options"
|
|
>
|
|
<MoreHorizontal className="w-4 h-4 text-slate-600 dark:text-slate-400" />
|
|
</button>
|
|
{showMoreOptions === customer.id && (
|
|
<MoreOptionsDropdown
|
|
itemType="customer"
|
|
onViewPerformance={() => handleViewCustomer(customer)}
|
|
onDownloadReport={() => console.log('Download report')}
|
|
onSendNotification={() => console.log('Send notification')}
|
|
onChangeTier={() => console.log('Change tier')}
|
|
onDeactivate={() => console.log('Deactivate customer')}
|
|
onDelete={() => console.log('Delete customer')}
|
|
onSendMail={() => handleMailCustomer(customer)}
|
|
onClose={() => setShowMoreOptions(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modals */}
|
|
<Modal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} title="Add New Customer">
|
|
<AddCustomerForm onSubmit={handleAddCustomer} onCancel={() => setIsAddModalOpen(false)} />
|
|
</Modal>
|
|
|
|
<Modal isOpen={isDetailModalOpen} onClose={() => setIsDetailModalOpen(false)} title="Customer Details">
|
|
{selectedCustomer && <DetailView data={selectedCustomer} type="customer" />}
|
|
</Modal>
|
|
|
|
<Modal isOpen={isEditModalOpen} onClose={() => setIsEditModalOpen(false)} title="Edit Customer">
|
|
{selectedCustomer && <EditCustomerForm customer={selectedCustomer} onSubmit={handleEditCustomer} onCancel={() => setIsEditModalOpen(false)} />}
|
|
</Modal>
|
|
|
|
<Modal isOpen={isMailModalOpen} onClose={() => setIsMailModalOpen(false)} title="Send Email">
|
|
{selectedCustomer && <MailComposeForm recipient={selectedCustomer.name} onSubmit={() => setIsMailModalOpen(false)} onCancel={() => setIsMailModalOpen(false)} />}
|
|
</Modal>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Customers;
|