Files
daily_merchant_web/src/components/CustomerDetailPanel.tsx
2026-07-09 15:49:45 +05:30

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>
);
}