229 lines
9.1 KiB
TypeScript
229 lines
9.1 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
|
import { useFiestaCustomerOrders } from '../services/fiestaQueries';
|
|
import { num as fnum, str as fstr, type Row } from '../services/fiestaApi';
|
|
import { Phone, MapPin, Mail, Receipt, X, Calendar, ShoppingBag, Wallet, TrendingUp, IndianRupee } from 'lucide-react';
|
|
import OrderDetailsModal from './OrderDetailsModal';
|
|
import './CustomerDetailPanel.css';
|
|
|
|
interface CustomerDetailPanelProps {
|
|
customer: Row;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
function initialsFor(name: string): string {
|
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
if (parts.length === 0) return '?';
|
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
|
|
function getStatusClass(status: string): string {
|
|
switch (status.toLowerCase()) {
|
|
case 'created':
|
|
return 'cdp-status-blue';
|
|
case 'pending':
|
|
return 'cdp-status-amber';
|
|
case 'delivered':
|
|
return 'cdp-status-green';
|
|
case 'cancelled':
|
|
return 'cdp-status-red';
|
|
default:
|
|
return 'cdp-status-blue';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export default function CustomerDetailPanel({ customer, onClose }: CustomerDetailPanelProps) {
|
|
const customerId = fnum(customer.customerid) || fstr(customer.contactno) || 0;
|
|
const { data: orders, isLoading } = useFiestaCustomerOrders({ customerid: customerId, pagesize: 100 });
|
|
const [selectedModalOrder, setSelectedModalOrder] = useState<Row | null>(null);
|
|
|
|
const { totalOrders, totalSpend, highestBill, avgOrderValue, groupedOrders } = useMemo(() => {
|
|
if (!orders || orders.length === 0) {
|
|
return { totalOrders: 0, totalSpend: 0, highestBill: 0, avgOrderValue: 0, groupedOrders: [] };
|
|
}
|
|
|
|
let highest = 0;
|
|
let spend = 0;
|
|
const groups = new Map<string, Row[]>();
|
|
|
|
for (const order of orders) {
|
|
const total = fnum(order.totalamount) || fnum(order.payableamount) || 0;
|
|
spend += total;
|
|
if (total > highest) highest = total;
|
|
|
|
const orderDateStr = fstr(order.createddate) || fstr(order.orderdate) || '';
|
|
const date = orderDateStr ? new Date(orderDateStr) : new Date();
|
|
const monthYear = date.toLocaleString('default', { month: 'long', year: 'numeric' });
|
|
|
|
if (!groups.has(monthYear)) groups.set(monthYear, []);
|
|
groups.get(monthYear)!.push(order);
|
|
}
|
|
|
|
return {
|
|
totalOrders: orders.length,
|
|
totalSpend: spend,
|
|
highestBill: highest,
|
|
avgOrderValue: spend / orders.length,
|
|
groupedOrders: Array.from(groups.entries()),
|
|
};
|
|
}, [orders]);
|
|
|
|
const name = fstr(customer.customername) || fstr(customer.name) || 'Unknown Customer';
|
|
const phone = fstr(customer.contactno) || fstr(customer.phone) || '';
|
|
const email = fstr(customer.email) || '';
|
|
const address = fstr(customer.address) || fstr(customer.deliveryaddress) || '';
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
if (!dateStr) return 'Date unknown';
|
|
const d = new Date(dateStr);
|
|
return new Intl.DateTimeFormat('en-IN', {
|
|
day: 'numeric', month: 'short',
|
|
hour: 'numeric', minute: '2-digit', hour12: true,
|
|
}).format(d);
|
|
};
|
|
|
|
return (
|
|
<div className="cdp-container">
|
|
|
|
{/* Pinned Top Area (Header + Contact + Metrics) */}
|
|
<div className="cdp-top-pinned">
|
|
<div className="cdp-header">
|
|
{onClose && (
|
|
<button className="cdp-close-btn" onClick={onClose} aria-label="Close panel">
|
|
<X size={18} />
|
|
</button>
|
|
)}
|
|
|
|
<div className="cdp-header-row">
|
|
<div className="cdp-header-main">
|
|
<div className="cdp-avatar-large">
|
|
{initialsFor(name)}
|
|
</div>
|
|
<div className="cdp-header-info">
|
|
<div className="cdp-name-row">
|
|
<span className="cdp-customer-name">{name}</span>
|
|
<span className="cdp-id-pill">ID {customerId}</span>
|
|
</div>
|
|
|
|
<div className="cdp-contact-inline">
|
|
{phone && (
|
|
<a href={`tel:${phone}`} className="cdp-contact-chip">
|
|
<span className="cdp-contact-icon-bg"><Phone size={12} /></span>
|
|
{phone}
|
|
</a>
|
|
)}
|
|
{email && (
|
|
<a href={`mailto:${email}`} className="cdp-contact-chip">
|
|
<span className="cdp-contact-icon-bg"><Mail size={12} /></span>
|
|
{email}
|
|
</a>
|
|
)}
|
|
{address && (
|
|
<div className="cdp-contact-chip" style={{ cursor: 'default' }}>
|
|
<span className="cdp-contact-icon-bg"><MapPin size={12} /></span>
|
|
{address}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`cdp-ltv-card ${totalSpend > 0 ? 'positive' : 'zero'}`}>
|
|
<span className="cdp-ltv-icon"><Wallet size={16} /></span>
|
|
<div className="cdp-ltv-text">
|
|
<span className="cdp-ltv-label">Lifetime Value</span>
|
|
<span className="cdp-ltv-val">₹{totalSpend.toLocaleString('en-IN')}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="cdp-metrics-row">
|
|
<div className="cdp-metric-tile">
|
|
<span className="cdp-metric-icon"><ShoppingBag size={16} /></span>
|
|
<div className="cdp-metric-text">
|
|
<span className={`cdp-metric-value ${totalOrders === 0 ? 'zero' : ''}`}>{totalOrders}</span>
|
|
<span className="cdp-metric-label">Orders</span>
|
|
</div>
|
|
</div>
|
|
<div className="cdp-metric-tile">
|
|
<span className="cdp-metric-icon"><TrendingUp size={16} /></span>
|
|
<div className="cdp-metric-text">
|
|
<span className={`cdp-metric-value ${highestBill === 0 ? 'zero' : ''}`}>₹{highestBill.toLocaleString('en-IN')}</span>
|
|
<span className="cdp-metric-label">Highest Bill</span>
|
|
</div>
|
|
</div>
|
|
<div className="cdp-metric-tile">
|
|
<span className="cdp-metric-icon"><IndianRupee size={16} /></span>
|
|
<div className="cdp-metric-text">
|
|
<span className={`cdp-metric-value ${avgOrderValue === 0 ? 'zero' : ''}`}>₹{avgOrderValue.toLocaleString('en-IN', { maximumFractionDigits: 0 })}</span>
|
|
<span className="cdp-metric-label">Avg Value</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Scrollable Order History Area */}
|
|
<div className="cdp-scrollable-body">
|
|
{isLoading ? (
|
|
<div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8', fontSize: '14px', fontWeight: 600 }}>
|
|
Loading order history...
|
|
</div>
|
|
) : groupedOrders.length > 0 ? (
|
|
<div className="cdp-history-container">
|
|
{groupedOrders.map(([monthYear, monthOrders], idx) => (
|
|
<div key={idx} className="cdp-month-group">
|
|
<div className="cdp-month-label">{monthYear}</div>
|
|
{monthOrders.map((order, orderIdx) => {
|
|
const orderDate = fstr(order.createddate) || fstr(order.orderdate) || '';
|
|
const orderId = fstr(order.orderid) || String(fnum(order.orderheaderid));
|
|
const total = fnum(order.totalamount) || fnum(order.payableamount) || 0;
|
|
const statusStr = fstr(order.orderstatus) || 'CREATED';
|
|
const statusBadgeClass = getStatusClass(statusStr);
|
|
return (
|
|
<div
|
|
key={orderIdx}
|
|
className="cdp-order-card"
|
|
onClick={() => setSelectedModalOrder(order)}
|
|
>
|
|
<div className="cdp-order-row">
|
|
<div className="cdp-order-info">
|
|
<div className="cdp-order-id">{orderId}</div>
|
|
<div className="cdp-order-date">
|
|
<Calendar size={12} />
|
|
{formatDate(orderDate)}
|
|
</div>
|
|
</div>
|
|
<div className="cdp-order-right">
|
|
<div className="cdp-order-price">₹{total.toLocaleString('en-IN')}</div>
|
|
<div className={`cdp-status-badge ${statusBadgeClass}`}>
|
|
{statusStr}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div style={{ padding: '40px', display: 'flex', flexDirection: 'column', alignItems: 'center', color: '#94a3b8' }}>
|
|
<Receipt size={32} style={{ marginBottom: '8px', opacity: 0.5 }} />
|
|
<div style={{ fontSize: '14px', fontWeight: 600 }}>No past orders found</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{selectedModalOrder && (
|
|
<OrderDetailsModal
|
|
order={selectedModalOrder}
|
|
onClose={() => setSelectedModalOrder(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|