308 lines
13 KiB
TypeScript
308 lines
13 KiB
TypeScript
import { Store, MapPin, TrendingUp, Calendar, Clock, Info, Truck } from 'lucide-react';
|
|
import { formatValue } from '../../lib/format';
|
|
import './card.css';
|
|
|
|
export function CallCard({ row, isDetailView = false }: { row: Record<string, any>; isDetailView?: boolean }) {
|
|
// Status Mapping
|
|
const stateName = row.current_state_name;
|
|
const lowerState = stateName.toLowerCase();
|
|
const showOrderDetails = lowerState.includes('ordered') || lowerState.includes('order closed') || lowerState.includes('close order');
|
|
|
|
let statusClass = 'z-card-status--neutral';
|
|
if (lowerState.includes('productive call')) {
|
|
statusClass = 'z-card-status--success';
|
|
} else if (lowerState.includes('close order') || lowerState.includes('completed') || lowerState.includes('success')) {
|
|
statusClass = 'z-card-status--success';
|
|
} else if (lowerState.includes('no order')) {
|
|
statusClass = 'z-card-status--danger';
|
|
} else if (lowerState.includes('visited')) {
|
|
statusClass = 'z-card-status--primary';
|
|
} else {
|
|
statusClass = 'z-card-status--primary'; // default fallback for other active states
|
|
}
|
|
|
|
// Extract nested objects if they exist
|
|
const storeObj = typeof row.select_store === 'object' ? row.select_store : (typeof row.store === 'object' ? row.store : {});
|
|
const userKey = Object.keys(row).find(k => k.includes('user_id'));
|
|
const userObj = userKey ? row[userKey] : null;
|
|
|
|
|
|
const storeName = formatValue(storeObj?.business_name || storeObj?.distributor_name || row.store_name || row.customer_name || (typeof row.store === 'string' ? row.store : 'Unknown Store'));
|
|
const storeCode = formatValue(storeObj?.store_code || row.store_code || '—');
|
|
|
|
// Phone and Email
|
|
const storeEmail = formatValue(storeObj?.email || row.email || row.store_email || '—');
|
|
const storePhoneRaw = storeObj?.phone_number || row.phone_number || row.phone || row.store_phone;
|
|
let storePhone = '—';
|
|
if (typeof storePhoneRaw === 'object' && storePhoneRaw !== null) {
|
|
storePhone = formatValue(storePhoneRaw.phone_with_dial_code || storePhoneRaw.phone || '—');
|
|
} else if (typeof storePhoneRaw === 'string') {
|
|
storePhone = formatValue(storePhoneRaw);
|
|
}
|
|
|
|
// Distributor Details
|
|
const distName = formatValue(storeObj?.distributor_name || '—');
|
|
const distOwner = formatValue(storeObj?.distributor_owner_name || '—');
|
|
const distEmail = formatValue(storeObj?.distributor_email || '—');
|
|
const distPhoneRaw = storeObj?.distributor_phone_number;
|
|
let distPhone = '—';
|
|
if (typeof distPhoneRaw === 'object' && distPhoneRaw !== null) {
|
|
distPhone = formatValue(distPhoneRaw.phone_with_dial_code || distPhoneRaw.phone || '—');
|
|
} else if (typeof distPhoneRaw === 'string') {
|
|
distPhone = formatValue(distPhoneRaw);
|
|
}
|
|
const hasDistributor = distName !== '—' || distOwner !== '—' || distEmail !== '—' || distPhone !== '—';
|
|
|
|
// Grid Details (Calculate from potential or orders if missing)
|
|
let totalWeightVal = Number(row.total_kgs_3 || row.total_kgs || row.total_weight || row.weight || 0);
|
|
let totalQuantityVal = Number(row.total_bags_3 || row.total_bags || row.total_quantity || row.quantity || 0);
|
|
let totalOrdersVal = Number(row.order_count || row.total_orders || (row.order_details_3 ? row.order_details_3.length : 0));
|
|
|
|
if (totalWeightVal === 0 && totalQuantityVal === 0 && storeObj?.potential) {
|
|
try {
|
|
const pot = typeof storeObj.potential === 'string' ? JSON.parse(storeObj.potential) : storeObj.potential;
|
|
if (Array.isArray(pot)) {
|
|
totalOrdersVal = pot.length;
|
|
pot.forEach((p: any) => {
|
|
totalWeightVal += Number(p.row_kgs || 0);
|
|
totalQuantityVal += Number(p.quantity || 0);
|
|
});
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
|
|
const totalWeight = formatValue(totalWeightVal);
|
|
const totalQuantity = formatValue(totalQuantityVal);
|
|
const totalOrders = formatValue(totalOrdersVal);
|
|
|
|
// Route & Assignment
|
|
const routeName = formatValue(storeObj?.route_name || storeObj?.route || row.route_name || row.route || '—');
|
|
const areaName = formatValue(storeObj?.area || row.area || row.area_name || '—');
|
|
|
|
// User
|
|
const userName = formatValue(userObj?.name || row.sales_officer || row.user_name || '—');
|
|
const userEmail = formatValue(userObj?.email || row.performed_by_email || row.user_email || '—');
|
|
const initials = userName !== '—' ? String(userName).substring(0, 2).toUpperCase() : 'SO';
|
|
|
|
// Date & Time
|
|
let dateStr = '—';
|
|
let timeStr = '—';
|
|
|
|
if (row.date_of_visit || row.visit_date) {
|
|
try {
|
|
const dVal = String(row.date_of_visit || row.visit_date);
|
|
const d = new Date(dVal);
|
|
if (!isNaN(d.getTime())) {
|
|
dateStr = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-');
|
|
} else dateStr = dVal.split('T')[0];
|
|
} catch {
|
|
dateStr = String(row.date_of_visit || row.visit_date).split('T')[0];
|
|
}
|
|
}
|
|
|
|
if (row.time_of_visit || row.visit_time) {
|
|
try {
|
|
const t = String(row.time_of_visit || row.visit_time);
|
|
let timeParts = t.split(':');
|
|
if (t.includes('T')) {
|
|
timeParts = t.split('T')[1].replace('Z', '').split(':');
|
|
}
|
|
if (timeParts.length >= 2) {
|
|
let h = parseInt(timeParts[0]);
|
|
const m = timeParts[1];
|
|
const ampm = h >= 12 ? 'PM' : 'AM';
|
|
h = h % 12;
|
|
h = h ? h : 12;
|
|
const hStr = h < 10 ? '0' + h : h;
|
|
timeStr = `${hStr}:${m} ${ampm}`;
|
|
} else {
|
|
timeStr = t;
|
|
}
|
|
} catch {
|
|
timeStr = String(row.time_of_visit || row.visit_time);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="z-card">
|
|
|
|
{/* Top Row */}
|
|
<div className="z-card-header">
|
|
<div className="z-card-header-icon">
|
|
<TrendingUp size={16} />
|
|
<span>{storeCode}</span>
|
|
</div>
|
|
<span className={`z-card-status ${statusClass}`}>
|
|
{stateName}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Store Name & Location */}
|
|
<div className="z-card-store-row">
|
|
<Store size={18} className="z-card-store-icon" />
|
|
<h2 className="z-card-store-name">{storeName}</h2>
|
|
</div>
|
|
|
|
<div className="z-card-area-row">
|
|
<MapPin size={16} className="z-card-area-icon" />
|
|
<p className="z-card-area-name">{areaName}</p>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="z-card-divider"></div>
|
|
|
|
{/* Date & Time */}
|
|
<div className="z-card-footer">
|
|
<div className="z-card-footer-item">
|
|
<div className="z-card-footer-label"><Calendar size={14} /> DATE</div>
|
|
<span className="z-card-footer-value">{dateStr}</span>
|
|
</div>
|
|
<div className="z-card-footer-item" style={{ textAlign: 'left', width: '96px' }}>
|
|
<div className="z-card-footer-label"><Clock size={14} /> TIME</div>
|
|
<span className="z-card-footer-value">{timeStr}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Button (List View Only) */}
|
|
{!isDetailView && (
|
|
<div className="z-card-action-btn">
|
|
<Info size={16} />
|
|
<span>See More Details</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Detail View Additions */}
|
|
{isDetailView && (
|
|
<div className="z-card-details">
|
|
{storePhone !== '—' && (
|
|
<p className="z-card-meta">
|
|
Phone: <span className="z-card-meta-val">{storePhone}</span>
|
|
</p>
|
|
)}
|
|
{storeEmail !== '—' && (
|
|
<p className="z-card-meta">
|
|
Email: <span className="z-card-meta-val lowercase">{storeEmail}</span>
|
|
</p>
|
|
)}
|
|
|
|
|
|
|
|
{/* Route & Assignment */}
|
|
<div className="z-card-route">
|
|
<div>
|
|
<p className="z-card-grid-item-label">Route</p>
|
|
<p className="z-card-route-val">{routeName}</p>
|
|
</div>
|
|
<div>
|
|
<p className="z-card-grid-item-label">Area</p>
|
|
<p className="z-card-route-val">{areaName}</p>
|
|
</div>
|
|
<div className="z-card-so-box">
|
|
<p className="z-card-grid-item-label">Sales Officer</p>
|
|
<div className="z-card-so-info">
|
|
<div className="z-card-so-avatar">
|
|
{initials}
|
|
</div>
|
|
<div className="z-card-so-details">
|
|
<p className="z-card-so-name">{userName}</p>
|
|
{userEmail !== '—' && (
|
|
<p className="z-card-so-email">{userEmail}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Call Meta Chips */}
|
|
<div className="z-card-route" style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
{row.order_received_channel && (
|
|
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">
|
|
Channel: {String(row.order_received_channel).replace(/_/g, ' ').toUpperCase()}
|
|
</span>
|
|
)}
|
|
{row.remarks && (
|
|
<div className="w-full mt-2 text-sm text-gray-600 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
|
<span className="font-semibold text-gray-800">Remarks: </span>
|
|
{row.remarks}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Product Cards (Order Details) */}
|
|
{showOrderDetails && row.order_details_3 && Array.isArray(row.order_details_3) && row.order_details_3.length > 0 && (
|
|
<div className="mt-6 border-t border-gray-100 pt-6">
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
|
<p className="z-card-meta" style={{ margin: 0, color: '#334155', fontWeight: 700 }}>Order Products</p>
|
|
<span className="text-xs font-bold text-gray-800 bg-gray-100 px-3 py-1 rounded-full border border-gray-200">
|
|
{totalOrders} {Number(totalOrders) === 1 ? 'Item' : 'Items'}
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
{row.order_details_3.map((prod: any, idx: number) => {
|
|
const productName = formatValue(prod.product_name_3 || prod.product_category_3 || 'Unknown Product');
|
|
const bagsStr = formatValue(prod.bags_3 || 0);
|
|
const bagsNum = Number(prod.bags_3 || 0);
|
|
const skuNum = Number(prod.sku_3 || prod.sku || 0);
|
|
const kgsNum = skuNum > 0 ? skuNum * bagsNum : Number(prod.row_kgs_3 || prod.row_kgs || prod.kgs_3 || prod.weight_3 || 0);
|
|
const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : '';
|
|
return (
|
|
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '14px', background: '#f8fafc', padding: '12px 16px', borderRadius: '8px', border: '1px solid #e2e8f0' }}>
|
|
<span style={{ fontWeight: 700, color: '#1e293b' }}>{productName}</span>
|
|
<span style={{ fontWeight: 800, color: '#3730a3' }}>{bagsStr} Bags{kgsStr}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Logistics Totals at the bottom */}
|
|
{showOrderDetails && (Number(totalWeightVal) > 0 || Number(totalQuantityVal) > 0) && (
|
|
<div className="mt-4 flex justify-between items-center bg-gray-50 p-4 rounded-xl border border-gray-200 shadow-sm">
|
|
<div className="flex flex-col">
|
|
<span className="text-[11px] uppercase tracking-wider font-bold text-gray-700">Total Weight</span>
|
|
<span className="text-xl font-black text-gray-900 mt-1">{totalWeight} <span className="text-sm font-bold text-gray-600">Kgs</span></span>
|
|
</div>
|
|
<div className="flex flex-col text-right">
|
|
<span className="text-[11px] uppercase tracking-wider font-bold text-gray-700">Total Quantity</span>
|
|
<span className="text-xl font-black text-gray-900 mt-1">{totalQuantity} <span className="text-sm font-bold text-gray-600">Bags</span></span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Distributor Details */}
|
|
{hasDistributor && (
|
|
<div className="mt-6 border-t border-gray-100 pt-6">
|
|
<p className="z-card-meta" style={{ marginBottom: '12px', color: '#334155', fontWeight: 700 }}>Distributor Details</p>
|
|
|
|
{distName !== '—' && (
|
|
<div className="z-card-store-row" style={{ marginBottom: '12px' }}>
|
|
<Truck size={18} className="z-card-store-icon" />
|
|
<h2 className="z-card-store-name">{distName}</h2>
|
|
</div>
|
|
)}
|
|
|
|
{distOwner !== '—' && (
|
|
<p className="z-card-meta">
|
|
Owner: <span className="z-card-meta-val">{distOwner}</span>
|
|
</p>
|
|
)}
|
|
{distPhone !== '—' && (
|
|
<p className="z-card-meta">
|
|
Phone: <span className="z-card-meta-val">{distPhone}</span>
|
|
</p>
|
|
)}
|
|
{distEmail !== '—' && (
|
|
<p className="z-card-meta">
|
|
Email: <span className="z-card-meta-val lowercase">{distEmail}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|