309 lines
15 KiB
TypeScript
309 lines
15 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { orderBookingClient } from '../../api/clients';
|
|
import { ORDER_BOOKING } from '../../api/config';
|
|
import { Card } from '../reusable/Card';
|
|
import { Spinner } from '../reusable/Spinner';
|
|
import { EmptyState } from '../reusable/EmptyState';
|
|
import { Store, User, Package, Calendar, Hash, Mail, Weight, List, MapPin, ArrowLeft } from 'lucide-react';
|
|
|
|
export interface WiredDetailViewProps {
|
|
instanceId: number | string;
|
|
columns?: 1 | 2 | 3;
|
|
onDataLoad?: (data: Record<string, unknown>) => void;
|
|
onBack?: () => void;
|
|
}
|
|
|
|
export function OrderDetail({ instanceId, onDataLoad, onBack }: WiredDetailViewProps) {
|
|
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let live = true;
|
|
async function run() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const r = await orderBookingClient.detailView(ORDER_BOOKING.detailViews.ORDERS, instanceId);
|
|
if (live) {
|
|
setData(r.data || {});
|
|
if (onDataLoad) onDataLoad(r.data || {});
|
|
}
|
|
} catch (e) {
|
|
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
|
} finally {
|
|
if (live) setLoading(false);
|
|
}
|
|
}
|
|
run();
|
|
return () => { live = false; };
|
|
}, [instanceId]);
|
|
|
|
if (error) {
|
|
return (
|
|
<Card title={`Order #${instanceId}`}>
|
|
<EmptyState title={`Couldn't load record`} hint={error} />
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card title={`Order #${instanceId}`}>
|
|
<div className="py-8 flex justify-center"><Spinner label="Loading details..." /></div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!data || Object.keys(data).length === 0) {
|
|
return (
|
|
<Card title={`Order #${instanceId}`}>
|
|
<EmptyState title="No details found" />
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const remainingData = { ...data };
|
|
const extract = (key: string, obj: any = data) => {
|
|
if (obj && key in obj) {
|
|
const val = obj[key];
|
|
delete obj[key];
|
|
return val;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// State
|
|
const stateName = extract('current_state_name', remainingData) || 'Ordered';
|
|
delete remainingData.current_state_id;
|
|
|
|
// Order Details
|
|
const orderId = extract('order_id', remainingData) || '-';
|
|
let dateOfOrder = extract('date_of_order_3', remainingData);
|
|
if (dateOfOrder) {
|
|
dateOfOrder = new Date(dateOfOrder as string).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
} else {
|
|
dateOfOrder = '-';
|
|
}
|
|
|
|
// SO Info
|
|
const soKey = Object.keys(remainingData).find(k => k.endsWith('__user_id')) || 'so_name';
|
|
const soRaw = extract(soKey, remainingData);
|
|
let soName = '-';
|
|
let soEmail = '-';
|
|
let soId = '-';
|
|
if (soRaw && typeof soRaw === 'object') {
|
|
soName = (soRaw as any).name || '-';
|
|
soEmail = (soRaw as any).email || '-';
|
|
soId = (soRaw as any).user_id || (soRaw as any).id || (soRaw as any).uid || '-';
|
|
}
|
|
const soInitials = soName.length > 2 ? soName.substring(0,2).toUpperCase() : 'SO';
|
|
|
|
// Store Info
|
|
const storeRaw = extract('select_store', remainingData);
|
|
let storeName = '-';
|
|
let distributorName = '-';
|
|
let routeName = '-';
|
|
let routeCode = '-';
|
|
if (storeRaw && typeof storeRaw === 'object') {
|
|
storeName = (storeRaw as any).business_name || '-';
|
|
distributorName = (storeRaw as any).distributor_name || '-';
|
|
routeName = (storeRaw as any).route_name || '-';
|
|
routeCode = (storeRaw as any).route_code || '-';
|
|
}
|
|
|
|
// Line Items
|
|
const orderDetailsGrid = extract('order_details_3', remainingData);
|
|
const itemsArray = Array.isArray(orderDetailsGrid) ? orderDetailsGrid : [];
|
|
|
|
// Totals
|
|
const totalBags = extract('total_bags_3', remainingData) || 0;
|
|
const totalKgs = extract('total_kgs_3', remainingData) || 0;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 bg-transparent p-1">
|
|
{/* Top Section */}
|
|
<div className="border border-slate-200 rounded-xl p-6 bg-[var(--tiles-card-bg)] shadow-sm flex flex-col md:flex-row md:items-start justify-between gap-4">
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
{onBack && (
|
|
<button onClick={onBack} className="p-1.5 text-slate-800 hover:text-slate-800 hover:bg-slate-100 rounded-full transition-colors focus:outline-none -ml-2">
|
|
<ArrowLeft size={18} strokeWidth={2.5} />
|
|
</button>
|
|
)}
|
|
<div className="text-[10px] font-bold text-slate-800 uppercase tracking-wider">Order ID</div>
|
|
</div>
|
|
<div className={`text-2xl font-black text-slate-900 mb-2 ${onBack ? 'ml-8' : ''}`}>{String(orderId)}</div>
|
|
<div className={`flex items-center gap-3 text-xs text-slate-800 font-medium ${onBack ? 'ml-8' : ''}`}>
|
|
<span className="flex items-center gap-1.5"><Calendar size={14} /> {String(dateOfOrder)}</span>
|
|
<span className="flex items-center gap-1.5"><Hash size={14} /> Instance #{instanceId}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ backgroundColor: '#e8fbf0', border: '1px solid #bcecd0', borderRadius: '16px', padding: '16px', display: 'flex', flexDirection: 'column', gap: '4px', minWidth: '240px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '10px', fontWeight: 700, color: 'var(--primary-color)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '4px' }}>
|
|
<Store size={14} /> Store
|
|
</div>
|
|
<div style={{ fontSize: '14px', fontWeight: 800, color: 'var(--primary-color)' }}>{String(storeName)}</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: 'var(--primary-color)', marginTop: '4px', fontWeight: 500 }}>
|
|
<span className="flex items-center gap-1.5"><MapPin size={13} /> {String(routeCode)} - {String(routeName)}</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: 'var(--primary-color)', marginTop: '2px', fontWeight: 500 }}>
|
|
<span className="flex items-center gap-1.5"><User size={13} /> {String(distributorName)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Middle Stats Section */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div style={{ backgroundColor: '#e8fbf0', border: '1px solid #bcecd0', borderRadius: '16px', padding: '12px', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--primary-color)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '2px' }}>TOTAL BAGS</div>
|
|
<div style={{ fontSize: '20px', fontWeight: 800, color: 'var(--primary-color)' }}>{String(totalBags)}</div>
|
|
</div>
|
|
<div style={{ backgroundColor: '#e8fbf0', border: '1px solid #bcecd0', borderRadius: '16px', padding: '12px', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--primary-color)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '2px' }}>TOTAL KGS</div>
|
|
<div style={{ fontSize: '20px', fontWeight: 800, color: 'var(--primary-color)' }}>{Number(totalKgs).toLocaleString()}</div>
|
|
</div>
|
|
<div style={{ backgroundColor: '#e8fbf0', border: '1px solid #bcecd0', borderRadius: '16px', padding: '12px', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--primary-color)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '2px' }}>LINE ITEMS</div>
|
|
<div style={{ fontSize: '20px', fontWeight: 800, color: 'var(--primary-color)' }}>{itemsArray.length}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Section */}
|
|
<div className="flex flex-col lg:flex-row gap-4">
|
|
{/* Left Table */}
|
|
<div className="flex-[2] border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden flex flex-col min-w-0" style={{ padding: '20px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '20px' }}>
|
|
<div style={{ backgroundColor: '#e6f9ed', padding: '6px', borderRadius: '50%' }}>
|
|
<Package size={16} color="#0d7d81" />
|
|
</div>
|
|
<span style={{ color: '#0d7d81', fontWeight: 600, fontSize: '13px', letterSpacing: '0.05em' }}>
|
|
ORDER PRODUCTS
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
{itemsArray.map((item: any, idx: number) => {
|
|
const productName = String(item.product_name_3 || item.product_name || 'Unknown Product');
|
|
const skuCodeRaw = item.sku_code_3 || item.sku_code || '';
|
|
const skuCode = isNaN(Number(skuCodeRaw)) || String(skuCodeRaw).length > 3 ? String(skuCodeRaw) : '';
|
|
const category = String(item.product_category_3 || item.product_category || '');
|
|
const brCode = String(item.br_code_3 || item.br_code || '');
|
|
|
|
const subtitleArr = [skuCode, category, brCode].filter(v => v && v !== '-' && v !== '—');
|
|
const subtitle = subtitleArr.join(' . ').toUpperCase();
|
|
|
|
const bagsNum = Number(item.bags_3 || item.bags || 0);
|
|
const bagsStr = String(bagsNum);
|
|
|
|
const skuNum = Number(item.sku_3 || item.sku || 0);
|
|
const rowKgsRaw = item.row_kgs_3 || item.row_kgs || item.weight;
|
|
const kgsNum = skuNum > 0 ? skuNum * bagsNum : Number(rowKgsRaw || 0);
|
|
const kgsStr = kgsNum > 0 ? `${kgsNum} kg` : '0 kg';
|
|
|
|
return (
|
|
<div key={idx} style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
backgroundColor: '#e8fbf0',
|
|
border: '1px solid #bcecd0',
|
|
borderRadius: '24px',
|
|
padding: '14px 20px',
|
|
}}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
<span style={{ color: 'var(--primary-color)', fontWeight: 600, fontSize: '14px' }}>{productName}</span>
|
|
</div>
|
|
<div style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
|
|
<span style={{ color: 'var(--primary-color)', fontWeight: 700, fontSize: '14px' }}>{bagsStr} Bags</span>
|
|
<span style={{ color: 'var(--primary-color)', fontSize: '13px', marginLeft: '6px' }}>({kgsStr})</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{itemsArray.length > 0 && (
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginTop: '8px',
|
|
padding: '16px 20px',
|
|
border: '1px dashed #cbd5e1',
|
|
borderRadius: '24px'
|
|
}}>
|
|
<span style={{ color: 'var(--primary-color)', fontWeight: 600, fontSize: '13px' }}>TOTAL</span>
|
|
<div>
|
|
<span style={{ color: 'var(--primary-color)', fontWeight: 700, fontSize: '15px' }}>{totalBags} Bags</span>
|
|
<span style={{ color: 'var(--primary-color)', fontSize: '14px', marginLeft: '6px' }}>({Number(totalKgs).toLocaleString()} kg)</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Sidebar */}
|
|
<div className="flex-1 flex flex-col gap-4 min-w-[280px]">
|
|
{/* SO Card */}
|
|
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-5">
|
|
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-800 uppercase tracking-wider mb-4">
|
|
<User size={14} className="text-slate-800" /> Sales Officer
|
|
</div>
|
|
<div className="flex items-center gap-3 mb-5">
|
|
<div className="w-10 h-10 rounded-full bg-slate-900 text-white flex items-center justify-center font-bold text-sm shrink-0">
|
|
{soInitials}
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-bold text-slate-900">{String(soName)}</div>
|
|
<div className="text-[10px] text-slate-800">Sales Officer - ID {String(soId)}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center shrink-0">
|
|
<Mail size={12} className="text-slate-800" />
|
|
</div>
|
|
<div className="overflow-hidden">
|
|
<div className="text-[10px] font-bold text-slate-800 uppercase tracking-wider mb-0.5">Email</div>
|
|
<div className="text-xs font-semibold text-slate-900 truncate">{String(soEmail)}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center shrink-0">
|
|
<Hash size={12} className="text-slate-800" />
|
|
</div>
|
|
<div>
|
|
<div className="text-[10px] font-bold text-slate-800 uppercase tracking-wider mb-0.5">User ID</div>
|
|
<div className="text-xs font-semibold text-slate-900">{String(soId)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Meta */}
|
|
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-5">
|
|
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-800 uppercase tracking-wider mb-4">
|
|
<Hash size={14} className="text-slate-800" /> Order Meta
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="text-slate-800">Instance</span>
|
|
<span className="font-bold text-slate-900">#{instanceId}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="text-slate-800">State</span>
|
|
<span className="font-bold text-slate-900">{String(stateName)}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="text-slate-800">Date of Order</span>
|
|
<span className="font-bold text-slate-900">{String(dateOfOrder)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|