924 lines
46 KiB
TypeScript
924 lines
46 KiB
TypeScript
import { useEffect, useState, type ReactNode } from 'react';
|
||
import { orderBookingClient } from '../../api/clients';
|
||
import { ORDER_BOOKING, APP_ID } from '../../api/config';
|
||
import { Spinner } from '../reusable/Spinner';
|
||
import { EmptyState } from '../reusable/EmptyState';
|
||
import {
|
||
ShoppingCart,
|
||
Store,
|
||
User,
|
||
ClipboardList,
|
||
TrendingUp,
|
||
MapPin,
|
||
Clock,
|
||
Phone,
|
||
Mail,
|
||
Truck,
|
||
FileText,
|
||
Camera,
|
||
Image as ImageIcon,
|
||
ArrowLeft,
|
||
} from 'lucide-react';
|
||
|
||
export interface CallDetailProps {
|
||
instanceId: number | string;
|
||
selectedRow?: Record<string, any>;
|
||
potentialMiningAction?: ReactNode;
|
||
refreshKey?: number;
|
||
onBack?: () => void;
|
||
onDataLoad?: (data: Record<string, unknown>) => void;
|
||
}
|
||
|
||
export function CallDetail({
|
||
instanceId,
|
||
selectedRow,
|
||
potentialMiningAction,
|
||
refreshKey,
|
||
onBack,
|
||
onDataLoad,
|
||
}: CallDetailProps) {
|
||
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
||
const [potentialData, setPotentialData] = useState<any[] | 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.CALLS, instanceId);
|
||
if (live) {
|
||
setData(r.data || {});
|
||
if (onDataLoad) onDataLoad(r.data || {});
|
||
|
||
const storeCode = (r.data?.select_store as any)?.store_code_2 || (r.data?.select_store as any)?.code || (r.data as any)?.store_code || (r.data as any)?.store?.store_code;
|
||
if (storeCode) {
|
||
orderBookingClient.request<{ potential: { potential: any[] } }>(
|
||
'POST',
|
||
'/api/papi2/potential-mining',
|
||
{ store_code: storeCode, instance_id: String(instanceId) },
|
||
{ 'TemplateID': '146' }
|
||
).then(res => {
|
||
if (live) setPotentialData(res.potential?.potential || []);
|
||
}).catch(e => console.warn("Failed to fetch potential mining", e));
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (live) setError((e as { message?: string })?.message ?? 'Failed to load details');
|
||
} finally {
|
||
if (live) setLoading(false);
|
||
}
|
||
}
|
||
run();
|
||
return () => {
|
||
live = false;
|
||
};
|
||
}, [instanceId, refreshKey]);
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="bg-[var(--tiles-card-bg)] p-8 rounded-2xl border border-border-default shadow-sm text-center">
|
||
<EmptyState title="Couldn’t load record" hint={error} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="bg-[var(--tiles-card-bg)] p-12 rounded-2xl border border-border-default shadow-sm flex justify-center items-center">
|
||
<Spinner label="Loading call details…" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const rowSrc = selectedRow || data || {};
|
||
|
||
// Extract store info with fallbacks matching reference
|
||
const selectStore = (rowSrc.select_store || data?.select_store || {}) as Record<string, any>;
|
||
const storeName = selectStore.business_name || selectStore.store_name || rowSrc.store_name || 'No data';
|
||
const storeCode = selectStore.store_code_2 || selectStore.code || 'No data';
|
||
const ownerName = selectStore.owner_name || selectStore.contact_person || 'No data';
|
||
const phone = selectStore.phone_number?.phone || selectStore.phone_number?.phone_with_dial_code || selectStore.phone || 'No data';
|
||
const email = selectStore.email || 'No data';
|
||
const address = selectStore.complete_address || selectStore.address || 'No data';
|
||
const route = selectStore.route_name ? `${selectStore.route_name} (${selectStore.route_code || 'No data'}) • ${selectStore.sub_route || 'No data'}` : 'No data';
|
||
const areaLoc = selectStore.area || 'No data';
|
||
|
||
let lat = '';
|
||
let lng = '';
|
||
try {
|
||
const locStr = selectStore.store_location;
|
||
if (locStr) {
|
||
const loc = typeof locStr === 'string' ? JSON.parse(locStr) : locStr;
|
||
if (loc?.latitude && loc?.longitude) {
|
||
lat = loc.latitude;
|
||
lng = loc.longitude;
|
||
}
|
||
}
|
||
} catch (e) { }
|
||
|
||
// Distributor info
|
||
const distCompany = selectStore.distributor_owner_name || 'No data';
|
||
const distContact = selectStore.distributor_name || 'No data';
|
||
const distPhone = selectStore.distributor_phone_number?.phone_with_dial_code || selectStore.distributor_phone_number?.phone || 'No data';
|
||
const distEmail = selectStore.distributor_email || 'No data';
|
||
|
||
// Performed by info
|
||
const userObj = rowSrc['42b7f47d-96f0-4289-a6d9-38f455ad57dd__user_id'] || data?.['42b7f47d-96f0-4289-a6d9-38f455ad57dd__user_id'] as any;
|
||
const salesOfficer = userObj?.name || String(rowSrc.created_by || data?.created_by || 'No data');
|
||
const officerEmail = userObj?.email || 'No data';
|
||
const officerJob = userObj?.job_title || 'Sales Officer';
|
||
|
||
// Extract activity timestamps
|
||
const formatTimeOnly = (isoStr?: string, fallback = 'No data') => {
|
||
if (!isoStr) return fallback;
|
||
try {
|
||
const d = new Date(isoStr);
|
||
if (isNaN(d.getTime())) return fallback;
|
||
const hours = String(d.getHours()).padStart(2, '0');
|
||
const mins = String(d.getMinutes()).padStart(2, '0');
|
||
return `${hours}:${mins}`;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
};
|
||
|
||
const formatDateOnly = (isoStr?: string, fallback = '') => {
|
||
if (!isoStr) return fallback;
|
||
try {
|
||
const d = new Date(isoStr);
|
||
if (isNaN(d.getTime())) return fallback;
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const year = String(d.getFullYear()).slice(2);
|
||
return `${day}/${month}/${year}`;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
};
|
||
|
||
const formatFullDateTime = (isoStr?: string, fallback = 'No data') => {
|
||
if (!isoStr) return fallback;
|
||
try {
|
||
const d = new Date(isoStr);
|
||
if (isNaN(d.getTime())) return fallback;
|
||
const day = d.getDate();
|
||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||
const month = monthNames[d.getMonth()];
|
||
const year = d.getFullYear();
|
||
let hours = d.getHours();
|
||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||
const ampm = hours >= 12 ? 'pm' : 'am';
|
||
hours = hours % 12;
|
||
hours = hours ? hours : 12;
|
||
return `${day} ${month} ${year}, ${String(hours).padStart(2, '0')}:${minutes} ${ampm}`;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
};
|
||
|
||
// Activity 1: Log Visit timestamp (42b7f47d-96f0-4289-a6d9-38f455ad57dd__created_at)
|
||
const rawLogVisitTs = rowSrc['42b7f47d-96f0-4289-a6d9-38f455ad57dd__created_at'] ?? data?.['42b7f47d-96f0-4289-a6d9-38f455ad57dd__created_at'];
|
||
// Activity 2: Productivity of Visit timestamp (11bd10f9-a001-470e-867f-33dee8eabe4b__created_at)
|
||
const rawProductivityTs = rowSrc['11bd10f9-a001-470e-867f-33dee8eabe4b__created_at'] ?? data?.['11bd10f9-a001-470e-867f-33dee8eabe4b__created_at'];
|
||
// Activity 3: Potential Mining timestamp (af8ac8df-d868-4f7d-88b2-123955d69c56__created_at)
|
||
const rawPotentialMiningTs = rowSrc['af8ac8df-d868-4f7d-88b2-123955d69c56__created_at'] ?? data?.['af8ac8df-d868-4f7d-88b2-123955d69c56__created_at'];
|
||
// Activity 4: Place Order timestamp (f66403d7-e31f-4d64-bcfd-ff5485aec8b8__created_at)
|
||
const rawPlaceOrderTs = rowSrc['f66403d7-e31f-4d64-bcfd-ff5485aec8b8__created_at'] ?? data?.['f66403d7-e31f-4d64-bcfd-ff5485aec8b8__created_at'];
|
||
|
||
const isLogVisitDone = rawLogVisitTs != null && String(rawLogVisitTs).trim() !== '';
|
||
const isProductivityDone = rawProductivityTs != null && String(rawProductivityTs).trim() !== '';
|
||
const isPotentialMiningDone = rawPotentialMiningTs != null && String(rawPotentialMiningTs).trim() !== '';
|
||
const isPlaceOrderDone = rawPlaceOrderTs != null && String(rawPlaceOrderTs).trim() !== '';
|
||
|
||
const time1 = isLogVisitDone ? formatTimeOnly(String(rawLogVisitTs)) : null;
|
||
const time2 = isProductivityDone ? formatTimeOnly(String(rawProductivityTs)) : null;
|
||
const timePm = isPotentialMiningDone ? formatTimeOnly(String(rawPotentialMiningTs)) : null;
|
||
const time3 = isPlaceOrderDone ? formatTimeOnly(String(rawPlaceOrderTs)) : null;
|
||
|
||
const date1 = isLogVisitDone ? formatDateOnly(String(rawLogVisitTs)) : null;
|
||
const date2 = isProductivityDone ? formatDateOnly(String(rawProductivityTs)) : null;
|
||
const datePm = isPotentialMiningDone ? formatDateOnly(String(rawPotentialMiningTs)) : null;
|
||
const date3 = isPlaceOrderDone ? formatDateOnly(String(rawPlaceOrderTs)) : null;
|
||
|
||
const fullDateTimeOrder = formatFullDateTime(String(rawPlaceOrderTs || rawLogVisitTs || ''));
|
||
|
||
// Call Potential items (below orders)
|
||
let callPotentialList: any[] = [];
|
||
const parseCallPotentialItem = (p: any) => {
|
||
const name = p.product_category_ || p.product_category || p.name || p.category || 'No data';
|
||
const actualPotential = Number(p.actual_potential || p.quantity || p.store_potential || 0);
|
||
const totalOrdered = Number(p.total_ordered || 0);
|
||
const difference = Number(p.difference || 0);
|
||
const reason = p.reason || null;
|
||
return {
|
||
name,
|
||
actualPotential,
|
||
totalOrdered,
|
||
difference,
|
||
reason,
|
||
kgs: `${actualPotential} kgs`,
|
||
val: actualPotential,
|
||
maxVal: Math.max(actualPotential, totalOrdered, 400),
|
||
raw: p,
|
||
};
|
||
};
|
||
|
||
const savedPotentialStr = rowSrc.potential || data?.potential;
|
||
let savedPotential: any[] = [];
|
||
if (Array.isArray(savedPotentialStr)) {
|
||
savedPotential = savedPotentialStr;
|
||
} else if (typeof savedPotentialStr === 'string') {
|
||
try {
|
||
savedPotential = JSON.parse(savedPotentialStr);
|
||
} catch (e) { }
|
||
}
|
||
|
||
let mergedCallPot = potentialData ? [...potentialData] : [...savedPotential];
|
||
|
||
if (potentialData && potentialData.length > 0 && savedPotential.length > 0) {
|
||
mergedCallPot = potentialData.map((pd: any) => {
|
||
const pdCat = String(pd.product_category_ || pd.product_category || pd.name || pd.category).toLowerCase().trim();
|
||
const match = savedPotential.find((sp: any) => {
|
||
const spCat = String(sp.product_category_ || sp.product_category || sp.name || sp.category).toLowerCase().trim();
|
||
return spCat === pdCat;
|
||
});
|
||
return { ...pd, reason: match?.reason || pd.reason };
|
||
});
|
||
|
||
savedPotential.forEach(sp => {
|
||
const spCat = String(sp.product_category_ || sp.product_category || sp.name || sp.category).toLowerCase().trim();
|
||
const exists = mergedCallPot.some((mp: any) => {
|
||
const mpCat = String(mp.product_category_ || mp.product_category || mp.name || mp.category).toLowerCase().trim();
|
||
return mpCat === spCat;
|
||
});
|
||
if (!exists) mergedCallPot.push(sp);
|
||
});
|
||
}
|
||
|
||
if (Array.isArray(mergedCallPot) && mergedCallPot.length > 0) {
|
||
callPotentialList = mergedCallPot.map(parseCallPotentialItem);
|
||
}
|
||
|
||
// Store Potential items (below store details)
|
||
let storePotentialList: any[] = [];
|
||
const rawStorePot = selectStore.potential;
|
||
const parseStorePotentialItem = (p: any) => {
|
||
const name = p.product_category_ || p.product_category || p.name || p.category || 'No data';
|
||
const actualPotential = Number(p.actual_potential || p.quantity || p.store_potential || 0);
|
||
return {
|
||
name,
|
||
actualPotential,
|
||
};
|
||
};
|
||
|
||
if (typeof rawStorePot === 'string') {
|
||
try {
|
||
const parsed = JSON.parse(rawStorePot);
|
||
if (Array.isArray(parsed)) {
|
||
storePotentialList = parsed.map(parseStorePotentialItem);
|
||
}
|
||
} catch (e) {
|
||
console.error("Failed to parse store potential JSON:", e);
|
||
}
|
||
} else if (Array.isArray(rawStorePot) && rawStorePot.length > 0) {
|
||
storePotentialList = rawStorePot.map(parseStorePotentialItem);
|
||
}
|
||
|
||
// Extract order line items
|
||
let orderItems: any[] = [];
|
||
const rawOrderItems = rowSrc.order_details || data?.order_details || rowSrc.order_details_3 || data?.order_details_3;
|
||
if (Array.isArray(rawOrderItems) && rawOrderItems.length > 0) {
|
||
orderItems = rawOrderItems.map((item: any) => {
|
||
const bags = Number(item.bags || item.bags_3 || item.quantity || 0);
|
||
const skuVal = Number(item.sku || item.sku_3 || '30');
|
||
return {
|
||
name: item.product_name || item.product_name_3 || item.product_category || 'MAIDA PUFF 30 kgs',
|
||
category: item.product_category || item.product_category_3 || item.category || 'MAIDA PUFF',
|
||
sku: item.sku || item.sku_3 || '30',
|
||
skuCode: item.sku_code || item.sku_code_3 || 'MP30',
|
||
brCode: item.br_code || item.br_code_3 || item.br || 'PM',
|
||
bags,
|
||
totalKgs: bags * skuVal,
|
||
};
|
||
});
|
||
} else {
|
||
orderItems = [];
|
||
}
|
||
|
||
// Calculate totals
|
||
const totalBags = Number(rowSrc.total_bags_3 || rowSrc.total_bags || data?.total_bags_3 || data?.total_bags || orderItems.reduce((acc, i) => acc + i.bags, 0));
|
||
const totalKgs = Number(rowSrc.total_kgs_3 || rowSrc.total_kgs || data?.total_kgs_3 || data?.total_kgs || orderItems.reduce((acc, i) => acc + (i.bags * (Number(i.sku) || 1)), 0));
|
||
const lineItemsCount = orderItems.length;
|
||
|
||
const orderId = String(rowSrc.order_id || data?.order_id || rowSrc.order_number || `No data`);
|
||
const dateOfOrder = String(rowSrc.date_of_order_3 || data?.date_of_order_3 || rowSrc.date_of_order || 'No data');
|
||
|
||
// Extract uploaded proof photos from backend API
|
||
const uploadedImages = (() => {
|
||
const imgData = rowSrc.upload_image || data?.upload_image || selectStore.upload_image;
|
||
if (Array.isArray(imgData)) return imgData;
|
||
if (imgData && typeof imgData === 'object' && (imgData as any).uuid) return [imgData];
|
||
return [];
|
||
})();
|
||
|
||
// Extract store banner photo from backend API
|
||
const storeImages = (() => {
|
||
const rawImg = selectStore.store_image || selectStore.upload_image || rowSrc.store_image || data?.store_image;
|
||
if (typeof rawImg === 'string') {
|
||
try {
|
||
const parsed = JSON.parse(rawImg);
|
||
if (Array.isArray(parsed)) return parsed;
|
||
} catch (e) {
|
||
console.error("Failed to parse store_image JSON:", e);
|
||
}
|
||
}
|
||
if (Array.isArray(rawImg)) return rawImg;
|
||
if (rawImg && typeof rawImg === 'object' && (rawImg as any).uuid) return [rawImg];
|
||
return [];
|
||
})();
|
||
|
||
// Visit log details
|
||
const dateOfVisit = String(rowSrc.date_of_visit || data?.date_of_visit || 'No data');
|
||
const timeOfVisit = String(rowSrc.time_of_visit || data?.time_of_visit || 'No data');
|
||
const storeStatus = String(rowSrc.store_status || data?.store_status || 'No data');
|
||
const channel = String(rowSrc.order_received_channel
|
||
|| rowSrc.channel || data?.order_received_channel || 'No data');
|
||
const storeNotes = selectStore.notes || 'No notes available.';
|
||
const currentStateName = String(rowSrc.current_state_name || data?.current_state_name || 'No data');
|
||
|
||
return (
|
||
<div className="text-slate-800 py-6 lg:py-8 font-sans space-y-6">
|
||
|
||
{/* 1. Primary Summary & Status Banner */}
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-6">
|
||
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
|
||
|
||
{/* Title & Badges */}
|
||
<div className="flex items-start gap-3 sm:gap-4">
|
||
{onBack && (
|
||
<button onClick={onBack} className="mt-1 p-1.5 sm:p-2 text-slate-800 hover:text-slate-800 hover:bg-slate-100/80 rounded-full transition-colors shrink-0 -ml-2">
|
||
<ArrowLeft size={20} strokeWidth={2.5} />
|
||
</button>
|
||
)}
|
||
<div className="space-y-3">
|
||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||
{/* {isPlaceOrderDone && (
|
||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">
|
||
<CheckCircle2 size={12} /> Ordered
|
||
</span>
|
||
)} */}
|
||
{currentStateName !== 'No data' && (
|
||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-blue-50 text-blue-700 border border-blue-200">
|
||
{currentStateName}
|
||
</span>
|
||
)}
|
||
{/* <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-purple-50 text-purple-700 border border-purple-200">
|
||
Productive call
|
||
</span>
|
||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-700 border border-slate-200">
|
||
<Phone size={11} /> On call
|
||
</span> */}
|
||
{/* <span className="text-xs font-medium text-slate-800">
|
||
{isPlaceOrderDone ? `Order ${orderId}` : 'Order Not Placed'}
|
||
</span> */}
|
||
</div>
|
||
|
||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">
|
||
{storeName}
|
||
</h1>
|
||
|
||
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-800 font-medium">
|
||
<span className="flex items-center gap-1">
|
||
<MapPin size={13} className="text-slate-800" /> {areaLoc}
|
||
</span>
|
||
<span>•</span>
|
||
<span>{route}</span>
|
||
<span>•</span>
|
||
<span className="flex items-center gap-1">
|
||
<Clock size={13} className="text-slate-800" /> {fullDateTimeOrder}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Top Right KPI Grid */}
|
||
<div className="flex flex-nowrap items-center gap-2 w-full min-w-0 mt-2">
|
||
<div className="bg-primary-light-bg border border-primary-light-border rounded-2xl p-2.5 text-center flex-1 min-w-0">
|
||
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-0.5 whitespace-nowrap overflow-hidden text-ellipsis">TOTAL BAGS</div>
|
||
<div className="text-lg font-extrabold text-primary leading-tight">{totalBags.toLocaleString()}</div>
|
||
</div>
|
||
<div className="bg-primary-light-bg border border-primary-light-border rounded-2xl p-2.5 text-center flex-1 min-w-0">
|
||
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-0.5 whitespace-nowrap overflow-hidden text-ellipsis">TOTAL KGS</div>
|
||
<div className="text-lg font-extrabold text-primary leading-tight">{totalKgs.toLocaleString()}</div>
|
||
</div>
|
||
<div className="bg-primary-light-bg border border-primary-light-border rounded-2xl p-2.5 text-center flex-1 min-w-0">
|
||
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-0.5 whitespace-nowrap overflow-hidden text-ellipsis">ORDER ITEMS</div>
|
||
<div className="text-lg font-extrabold text-primary leading-tight">{lineItemsCount}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Workflow Progress Stepper - 3 Timestamps */}
|
||
<div className="border-t border-slate-100 pt-4">
|
||
<div className="flex-col flex-nowrap items-center justify-between gap-6 overflow-x-auto pb-2">
|
||
|
||
{/* Step 1: Log Visit */}
|
||
<div className="flex items-center gap-3 pb-2">
|
||
{isLogVisitDone ? (
|
||
<div className="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||
✓
|
||
</div>
|
||
) : (
|
||
<div className="w-6 h-6 rounded-full border-2 border-slate-300 bg-[var(--tiles-card-bg)] text-slate-800 flex items-center justify-center text-xs shrink-0">
|
||
<Clock size={12} />
|
||
</div>
|
||
)}
|
||
<div className="text-xs">
|
||
<div className={`font-bold ${isLogVisitDone ? 'text-slate-800' : 'text-slate-800'}`}>Visit logged</div>
|
||
<div className="text-slate-800 font-medium text-[11px] leading-tight mt-0.5">
|
||
{isLogVisitDone ? `${time1} • ${date1}` : 'Not performed'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step 2: Productivity of Visit */}
|
||
<div className="flex items-center gap-3 pb-2">
|
||
{isProductivityDone ? (
|
||
<div className="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||
✓
|
||
</div>
|
||
) : (
|
||
<div className="w-6 h-6 rounded-full border-2 border-slate-300 bg-[var(--tiles-card-bg)] text-slate-800 flex items-center justify-center text-xs shrink-0">
|
||
<Clock size={12} />
|
||
</div>
|
||
)}
|
||
<div className="text-xs">
|
||
<div className={`font-bold ${isProductivityDone ? 'text-slate-800' : 'text-slate-800'}`}>Productive call</div>
|
||
<div className="text-slate-800 font-medium text-[11px] leading-tight mt-0.5">
|
||
{isProductivityDone ? `${time2} • ${date2}` : 'Not performed'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step 3: Potential Mining */}
|
||
<div className="flex items-center gap-3 pb-2">
|
||
{isPotentialMiningDone ? (
|
||
<div className="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||
✓
|
||
</div>
|
||
) : (
|
||
<div className="w-6 h-6 rounded-full border-2 border-slate-300 bg-[var(--tiles-card-bg)] text-slate-800 flex items-center justify-center text-xs shrink-0">
|
||
<Clock size={12} />
|
||
</div>
|
||
)}
|
||
<div className="text-xs pb-2">
|
||
<div className={`font-bold ${isPotentialMiningDone ? 'text-slate-800' : 'text-slate-800'}`}>Potential Mining</div>
|
||
<div className="text-slate-800 font-medium text-[11px] leading-tight mt-0.5">
|
||
{isPotentialMiningDone ? `${timePm} • ${datePm}` : 'Not performed'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step 4: Place Order */}
|
||
<div className="flex items-center gap-3">
|
||
{isPlaceOrderDone ? (
|
||
<div className="w-6 h-6 rounded-full bg-emerald-500 text-white flex items-center justify-center text-xs font-bold shrink-0">
|
||
✓
|
||
</div>
|
||
) : (
|
||
<div className="w-6 h-6 rounded-full border-2 border-slate-300 bg-[var(--tiles-card-bg)] text-slate-800 flex items-center justify-center text-xs shrink-0">
|
||
<Clock size={12} />
|
||
</div>
|
||
)}
|
||
<div className="text-xs">
|
||
<div className={`font-bold ${isPlaceOrderDone ? 'text-slate-800' : 'text-slate-800'}`}>Ordered</div>
|
||
<div className="text-slate-800 font-medium text-[11px] leading-tight mt-0.5">
|
||
{isPlaceOrderDone ? `${time3} • ${date3}` : 'Not performed'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2. Main Two-Column Layout */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||
|
||
{/* MAIN CONTENT (Now on Right, 2/3 width) */}
|
||
<div className="lg:col-span-2 lg:order-last space-y-6">
|
||
|
||
{/* Order Details Card */}
|
||
{currentStateName.toLowerCase() !== 'no order' && (
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-4">
|
||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between border-b border-slate-100 pb-3 gap-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className="bg-primary-light-bg p-1.5 rounded-full border border-primary-light-border flex items-center justify-center">
|
||
<ShoppingCart size={14} color="currentColor" className="text-primary" />
|
||
</div>
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Order Details</h2>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs font-semibold text-slate-800">
|
||
{isPlaceOrderDone ? `${orderId} • ${dateOfOrder}` : 'Not Performed'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div className="flex flex-col gap-4">
|
||
{orderItems.length > 0 ? (
|
||
<>
|
||
<div className="flex flex-col gap-3">
|
||
{orderItems.map((item, idx) => {
|
||
const bagsStr = item.bags;
|
||
const kgsStr = item.totalKgs > 0 ? `${item.totalKgs.toLocaleString()} kg` : '0 kg';
|
||
return (
|
||
<div key={idx} className="flex justify-between items-center bg-primary-light-bg border border-primary-light-border rounded-3xl px-5 py-3.5">
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-primary font-semibold text-sm">{item.name}</span>
|
||
</div>
|
||
<div className="text-right whitespace-nowrap">
|
||
<span className="text-primary font-bold text-sm">{bagsStr} Bags</span>
|
||
<span className="text-primary text-[13px] ml-1.5">({kgsStr})</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
<div className="mt-2 rounded-xl p-4 flex items-center justify-between shadow-sm bg-primary text-white">
|
||
<span className="text-xs font-bold uppercase tracking-wider opacity-90">Total Order</span>
|
||
<div className="flex items-center gap-6">
|
||
<div className="flex flex-col items-end">
|
||
<span className="text-[10px] font-medium uppercase opacity-80">Bags</span>
|
||
<span className="text-lg font-black">{totalBags}</span>
|
||
</div>
|
||
<div className="flex flex-col items-end">
|
||
<span className="text-[10px] font-medium uppercase opacity-80">Total Kgs</span>
|
||
<span className="text-lg font-black text-emerald-300">{totalKgs.toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="bg-primary-light-bg border border-primary-light-border rounded-xl px-6 py-8 flex flex-col items-center justify-center text-center gap-2">
|
||
<ShoppingCart size={24} color="currentColor" className="text-primary opacity-50" />
|
||
<span className="text-[13px] font-semibold text-primary">Order not placed</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Total Summary Row (Moved to table footer) */}
|
||
|
||
{/* Meta Footer */}
|
||
{isPlaceOrderDone && (
|
||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-100 pt-4 text-xs text-slate-800 font-medium">
|
||
<div>
|
||
CREATED BY <span className="text-slate-800 font-bold ml-1">{salesOfficer} • {officerJob || 'Sales Officer'}</span>
|
||
</div>
|
||
<div>
|
||
CREATED AT <span className="text-slate-800 font-bold ml-1">{fullDateTimeOrder}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Call Potential Card (Below Orders) */}
|
||
{callPotentialList.length > 0 && (
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-5 shadow-sm flex flex-col gap-3">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className="bg-primary-light-bg p-1.5 rounded-full border border-primary-light-border flex items-center justify-center">
|
||
<TrendingUp size={14} color="currentColor" className="text-primary" />
|
||
</div>
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Potential vs Ordered</h2>
|
||
</div>
|
||
<span className="text-xs font-medium text-slate-800">Ordered vs opportunity</span>
|
||
</div>
|
||
|
||
{(() => {
|
||
const regularPotential = callPotentialList.filter(item => item.actualPotential > 0);
|
||
const extraOrdered = callPotentialList.filter(item => item.actualPotential === 0);
|
||
|
||
const renderTable = (items: any[]) => (
|
||
<div className="flex flex-col gap-2.5">
|
||
{items.map((item, idx) => (
|
||
<div key={idx} className="bg-primary-light-bg border border-primary-light-border rounded-xl px-3 py-2.5 flex flex-col gap-1">
|
||
<div className="flex justify-between items-center">
|
||
<span className="text-primary font-bold text-[13px] uppercase tracking-wide">{item.name}</span>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-center bg-white rounded-lg py-1.5 px-3 border border-primary-light-border mt-0.5">
|
||
<div className="flex flex-col gap-0.5">
|
||
<span className="text-[10px] font-bold text-primary uppercase tracking-wider">Potential</span>
|
||
<span className="text-sm font-semibold text-primary">{item.actualPotential}</span>
|
||
</div>
|
||
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-4">
|
||
<span className="text-[10px] font-bold text-primary uppercase tracking-wider">Ordered</span>
|
||
<span className="text-sm font-semibold text-primary">{item.totalOrdered}</span>
|
||
</div>
|
||
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-4">
|
||
<span className="text-[10px] font-bold text-primary uppercase tracking-wider">Diff</span>
|
||
<span className={`text-[15px] font-black ${item.difference >= 0 ? 'text-emerald-600' : 'text-red-500'}`}>
|
||
{item.difference > 0 ? '+' : ''}{item.difference}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{item.reason && (
|
||
<div className="flex items-center justify-between px-3 py-2 rounded-xl border border-red-200 bg-red-50 mt-1">
|
||
<span className="text-[10px] font-bold text-red-500 uppercase tracking-wider">Reason</span>
|
||
<span className="text-[11px] font-bold text-red-600">{item.reason}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
{regularPotential.length > 0 && renderTable(regularPotential)}
|
||
|
||
{extraOrdered.length > 0 && (
|
||
<div className="flex flex-col gap-3 pt-2 border-t border-slate-100 mt-2">
|
||
<div className="flex items-center gap-2">
|
||
<ShoppingCart className="text-slate-800" size={14} />
|
||
<h3 className="text-[11px] font-bold text-slate-800 uppercase tracking-wider">Ordered Outside Potential</h3>
|
||
</div>
|
||
{renderTable(extraOrdered)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})()}
|
||
|
||
{potentialMiningAction && (
|
||
<div className="flex justify-end border-t border-slate-100 pt-3">
|
||
{potentialMiningAction}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
|
||
|
||
{/* Visit Log Card */}
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<ClipboardList className="text-slate-700" size={18} />
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Visit Log</h2>
|
||
</div>
|
||
<span className="text-xs font-medium text-slate-800">Logged at {time1}</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
|
||
{/* Left Key Value Details */}
|
||
<div className="space-y-3 text-xs">
|
||
<div className="flex justify-between py-1.5 border-b border-slate-100">
|
||
<span className="font-bold text-slate-800 uppercase">DATE OF VISIT</span>
|
||
<span className="font-bold text-slate-800">{dateOfVisit}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1.5 border-b border-slate-100">
|
||
<span className="font-bold text-slate-800 uppercase">TIME OF VISIT</span>
|
||
<span className="font-bold text-slate-800">{timeOfVisit}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1.5 border-b border-slate-100">
|
||
<span className="font-bold text-slate-800 uppercase">STORE STATUS</span>
|
||
<span className="font-bold text-slate-800">{storeStatus}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1.5 border-b border-slate-100">
|
||
<span className="font-bold text-slate-800 uppercase">CHANNEL</span>
|
||
<span className="font-bold text-slate-800">{channel}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Store Proof Photo */}
|
||
<div className="space-y-2">
|
||
<div className="text-[11px] font-bold text-slate-800 uppercase tracking-wider flex items-center gap-1">
|
||
<FileText size={12} /> UPLOADED PROOF
|
||
</div>
|
||
|
||
{uploadedImages.length > 0 ? (
|
||
<div className="space-y-3">
|
||
{uploadedImages.map((file: any, idx: number) => {
|
||
const previewUrl = `${orderBookingClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`;
|
||
return (
|
||
<div key={file.uuid || idx} className="relative w-full rounded-xl overflow-hidden border border-slate-200 bg-slate-100 flex items-center justify-center group shadow-sm">
|
||
<img
|
||
src={previewUrl}
|
||
alt={file.original_name || 'Uploaded Proof'}
|
||
className="w-full h-auto max-h-[300px] object-cover transition-transform group-hover:scale-[1.01]"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
(e.target as HTMLImageElement).parentElement!.innerHTML = `<div class="p-6 text-center text-xs text-slate-800 font-mono">${file.original_name || 'Proof Image'}</div>`;
|
||
}}
|
||
/>
|
||
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="absolute inset-0 z-10" aria-label="View Full Image" />
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div className="bg-primary-light-bg border border-primary-light-border rounded-xl p-6 flex flex-col items-center justify-center text-center gap-2">
|
||
<Camera size={24} color="currentColor" className="text-primary" />
|
||
<span className="text-xs font-semibold text-primary">No proof image uploaded</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{/* SIDEBAR (Now on Left, 1/3 width) */}
|
||
<div className="lg:order-first space-y-6">
|
||
|
||
{/* Store Banner Image Card */}
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 overflow-hidden shadow-sm">
|
||
{storeImages.length > 0 ? (
|
||
<div className="relative w-full h-44 bg-slate-100 flex items-center justify-center overflow-hidden">
|
||
<img
|
||
src={`${orderBookingClient.baseUrl}/app/${APP_ID}/view/files/${storeImages[0].uuid}/preview`}
|
||
alt="Store Front"
|
||
className="w-full h-full object-cover"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
}}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="bg-primary-light-bg border-b border-primary-light-border p-6 flex flex-col items-center justify-center gap-2 h-[160px] text-center">
|
||
<div className="w-12 h-12 rounded-full bg-white border border-primary-light-border flex items-center justify-center shadow-sm">
|
||
<ImageIcon size={22} color="currentColor" className="text-primary" />
|
||
</div>
|
||
<span className="text-xs font-bold text-primary uppercase tracking-wider">{storeName}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Store Info Card */}
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className="bg-primary-light-bg p-1.5 rounded-full border border-primary-light-border flex items-center justify-center">
|
||
<Store size={14} color="currentColor" className="text-primary" />
|
||
</div>
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Store</h2>
|
||
</div>
|
||
<span className="px-2 py-0.5 text-[10px] font-bold text-slate-600 bg-slate-100 rounded border border-slate-200">
|
||
{storeCode}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-3 text-xs">
|
||
<div className="flex items-start justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">BUSINESS</span>
|
||
<span className="font-bold text-slate-900 text-right">{storeName}</span>
|
||
</div>
|
||
<div className="flex items-start justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">OWNER</span>
|
||
<span className="font-bold text-slate-900 text-right">{ownerName}</span>
|
||
</div>
|
||
<div className="flex items-start justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">PHONE</span>
|
||
<span className="font-bold text-slate-900 text-right flex items-center gap-1">
|
||
<Phone size={11} className="text-slate-800" /> {phone}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-start justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">EMAIL</span>
|
||
<span className="font-bold text-slate-900 text-right flex items-center gap-1 break-all">
|
||
<Mail size={11} className="text-slate-800" /> {email}
|
||
</span>
|
||
</div>
|
||
<div className="py-1 border-b border-slate-100 space-y-1">
|
||
<span className="text-slate-800 font-bold uppercase block">ADDRESS</span>
|
||
<span className="font-medium text-slate-800 block leading-relaxed">{address}</span>
|
||
{lat && lng && (
|
||
<a
|
||
href={`https://www.google.com/maps/search/?api=1&query=${lat},${lng}`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="mt-1.5 inline-flex items-center gap-1.5 text-[11px] font-bold text-blue-600 bg-blue-50/50 hover:bg-blue-50 px-2 py-1 rounded transition-colors"
|
||
>
|
||
<MapPin size={12} /> View on Google Maps
|
||
</a>
|
||
)}
|
||
</div>
|
||
<div className="flex items-start justify-between py-1">
|
||
<span className="text-slate-800 font-bold uppercase">ROUTE</span>
|
||
<span className="font-bold text-slate-900 text-right">{route}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Notes Container */}
|
||
<div className="bg-primary-light-bg border border-primary-light-border rounded-xl p-3.5 gap-1.5 flex flex-col">
|
||
<div className="text-[11px] font-bold text-primary uppercase tracking-wider flex items-center gap-1.5">
|
||
<FileText size={12} color="currentColor" className="text-primary" /> NOTES
|
||
</div>
|
||
<p className="text-primary leading-relaxed font-medium m-0 text-xs">
|
||
{storeNotes}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Store Potential Card */}
|
||
{storePotentialList.length > 0 && (
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className="bg-primary-light-bg p-1.5 rounded-full border border-primary-light-border flex items-center justify-center">
|
||
<TrendingUp size={14} color="currentColor" className="text-primary" />
|
||
</div>
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Store Potential</h2>
|
||
</div>
|
||
<span className="text-xs font-medium text-slate-800">Total capacity</span>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-3">
|
||
{storePotentialList.map((item, idx) => (
|
||
<div key={idx} className="flex justify-between items-center bg-primary-light-bg border border-primary-light-border rounded-3xl px-5 py-3.5">
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-primary font-semibold text-sm">{item.name}</span>
|
||
</div>
|
||
<div className="text-right whitespace-nowrap">
|
||
<span className="text-primary font-bold text-sm">{item.actualPotential} KG Potential</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Distributor Card */}
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-4">
|
||
<div className="flex items-center gap-2 border-b border-slate-100 pb-3">
|
||
<div className="bg-primary-light-bg p-1.5 rounded-full border border-primary-light-border flex items-center justify-center">
|
||
<Truck size={14} color="currentColor" className="text-primary" />
|
||
</div>
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Distributor</h2>
|
||
</div>
|
||
|
||
<div className="space-y-3 text-xs">
|
||
<div className="flex justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">COMPANY</span>
|
||
<span className="font-bold text-slate-900">{distCompany}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">CONTACT</span>
|
||
<span className="font-bold text-slate-900">{distContact}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">PHONE</span>
|
||
<span className="font-bold text-slate-900 flex items-center gap-1">
|
||
<Phone size={11} className="text-slate-800" /> {distPhone}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between py-1">
|
||
<span className="text-slate-800 font-bold uppercase">EMAIL</span>
|
||
<span className="font-bold text-slate-900 flex items-center gap-1">
|
||
<Mail size={11} className="text-slate-800" /> {distEmail}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Performed By Card */}
|
||
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-4">
|
||
<div className="flex items-center gap-2 border-b border-slate-100 pb-3">
|
||
<User className="text-slate-700" size={18} />
|
||
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Performed by</h2>
|
||
</div>
|
||
|
||
<div className="space-y-3 text-xs">
|
||
<div className="flex justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">NAME</span>
|
||
<span className="font-bold text-slate-900">{salesOfficer}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1 border-b border-slate-100">
|
||
<span className="text-slate-800 font-bold uppercase">ROLE</span>
|
||
<span className="font-bold text-slate-900">{officerJob || 'Sales Officer'}</span>
|
||
</div>
|
||
<div className="flex justify-between py-1">
|
||
<span className="text-slate-800 font-bold uppercase">EMAIL</span>
|
||
<span className="font-bold text-slate-900 flex items-center gap-1">
|
||
<Mail size={11} className="text-slate-800" /> {officerEmail}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
}
|