details completed

This commit is contained in:
suryacp23 2026-07-27 13:03:20 +05:30
parent e0d79ffbc1
commit e03a5a483c
13 changed files with 2083 additions and 245 deletions

View File

@ -240,11 +240,15 @@ export function CallCard({ row, isDetailView = false }: { row: Record<string, an
<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 bags = formatValue(prod.bags_3 || 0);
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' }}>{bags} Bags</span>
<span style={{ fontWeight: 800, color: '#3730a3' }}>{bagsStr} Bags{kgsStr}</span>
</div>
);
})}

View File

@ -101,12 +101,16 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record<s
};
const productName = formatValue(getVal('product_name') || getVal('product_category') || 'Unknown Product');
const bags = formatValue(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
const bagsStr = formatValue(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
const bagsNum = Number(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
const skuNum = Number(getVal('sku') || 0);
const kgsNum = skuNum > 0 ? skuNum * bagsNum : Number(getVal('row_kgs') || getVal('kgs') || getVal('weight') || 0);
const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : '';
return (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', background: '#f8fafc', padding: '8px 12px', borderRadius: '8px' }}>
<span style={{ fontWeight: 600, color: '#334155' }}>{productName}</span>
<span style={{ fontWeight: 700, color: '#4f46e5' }}>{bags} Bags</span>
<span style={{ fontWeight: 700, color: '#4f46e5' }}>{bagsStr} Bags{kgsStr}</span>
</div>
);
})}
@ -182,12 +186,16 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record<s
};
const productName = formatValue(getVal('product_name') || getVal('product_category') || 'Unknown Product');
const bags = formatValue(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
const bagsStr = formatValue(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
const bagsNum = Number(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
const skuNum = Number(getVal('sku') || 0);
const kgsNum = skuNum > 0 ? skuNum * bagsNum : Number(getVal('row_kgs') || getVal('kgs') || getVal('weight') || 0);
const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : '';
return (
<div key={i} 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' }}>{bags} Bags</span>
<span style={{ fontWeight: 800, color: '#3730a3' }}>{bagsStr} Bags{kgsStr}</span>
</div>
);
})}

View File

@ -1,28 +1,934 @@
import { useEffect } from 'react';
import { useEffect, useState, type ReactNode } from 'react';
import { orderBookingClient } from '../../api/clients';
import { ORDER_BOOKING } from '../../api/config';
import type { WiredDetailViewProps } from './OrderDetail';
import { useDetailViewData } from './useDetailViewData';
import { ORDER_BOOKING, APP_ID } from '../../api/config';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { CallCard } from '../cards/CallCard';
import {
ShoppingCart,
Store,
User,
ClipboardList,
TrendingUp,
Edit3,
MapPin,
Clock,
Phone,
Mail,
Truck,
FileText,
Camera,
Image as ImageIcon,
ArrowLeft,
} from 'lucide-react';
export function CallDetail({ instanceId, onDataLoad }: WiredDetailViewProps) {
const { data, loading, error } = useDetailViewData(orderBookingClient, ORDER_BOOKING.detailViews.CALLS, instanceId);
export interface CallDetailProps {
instanceId: number | string;
selectedRow?: Record<string, any>;
potentialMiningAction?: ReactNode;
placeOrderAction?: ReactNode;
refreshKey?: number;
onBack?: () => void;
onDataLoad?: (data: Record<string, unknown>) => void;
}
export function CallDetail({
instanceId,
selectedRow,
potentialMiningAction,
placeOrderAction,
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(() => {
if (data && onDataLoad) {
onDataLoad(data);
}
}, [data, onDataLoad]);
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 || {});
if (error) return <EmptyState title="Couldn't load record" hint={error} />;
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Call Details..." /></div>;
if (!data) return <EmptyState title="No details found" />;
const storeCode = (r.data?.select_store as any)?.store_code || (r.data?.select_store as any)?.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="Couldnt 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 || 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="w-full space-y-6 pb-24">
<CallCard row={data} isDetailView={true} />
<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-400 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-400">
{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-500 font-medium">
<span className="flex items-center gap-1">
<MapPin size={13} className="text-slate-400" /> {areaLoc}
</span>
<span></span>
<span>{route}</span>
<span></span>
<span className="flex items-center gap-1">
<Clock size={13} className="text-slate-400" /> {fullDateTimeOrder}
</span>
</div>
</div>
</div>
{/* Top Right KPI Grid */}
<div className="flex flex-nowrap items-center gap-3 w-full lg:w-auto overflow-x-auto pb-2 min-w-0">
<div className="bg-slate-50 border border-slate-200/60 rounded-xl p-3 text-center min-w-[100px] flex-1">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">TOTAL BAGS</div>
<div className="text-xl font-bold text-slate-900">{totalBags.toLocaleString()}</div>
</div>
<div className="bg-slate-50 border border-slate-200/60 rounded-xl p-3 text-center min-w-[100px] flex-1">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">TOTAL KGS</div>
<div className="text-xl font-bold text-slate-900">{totalKgs.toLocaleString()}</div>
</div>
<div className="bg-slate-50 border border-slate-200/60 rounded-xl p-3 text-center min-w-[100px] flex-1">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">LINE ITEMS</div>
<div className="text-xl font-bold text-slate-900">{lineItemsCount}</div>
</div>
<div className="bg-slate-50 border border-slate-200/60 rounded-xl p-3 text-center min-w-[110px] flex-1">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">SALES OFFICER</div>
<div className="text-sm font-bold text-slate-900 truncate mt-1">{salesOfficer}</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-400 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-400'}`}>Visit logged</div>
<div className="text-slate-400 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-400 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-400'}`}>Productive call</div>
<div className="text-slate-400 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-400 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-400'}`}>Potential Mining</div>
<div className="text-slate-400 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-400 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-400'}`}>Ordered</div>
<div className="text-slate-400 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">
<ShoppingCart className="text-slate-700" size={18} />
<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-500">
{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) => {
let codeBadgeClass = "bg-blue-50 text-blue-700 border-blue-200";
if (item.skuCode?.includes('50')) codeBadgeClass = "bg-indigo-50 text-indigo-700 border-indigo-200";
if (item.skuCode?.includes('MAP')) codeBadgeClass = "bg-emerald-50 text-emerald-700 border-emerald-200";
return (
<div key={idx} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm flex flex-col gap-3 transition-shadow hover:shadow-md">
<div className="flex justify-between items-start gap-4 border-b border-slate-50 pb-3">
<div className="flex flex-col gap-1">
<span className="font-bold text-slate-900 text-sm">{item.name}</span>
<span className="text-[11px] text-slate-400 font-medium uppercase tracking-wide">{item.category}</span>
</div>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold border uppercase tracking-wider whitespace-nowrap ${codeBadgeClass}`}>
{item.skuCode}
</span>
</div>
<div className="flex items-center justify-between p-3 rounded-lg border border-slate-100 bg-white">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Bags</span>
<span className="text-lg font-black text-slate-900">{item.bags}</span>
</div>
<div className="flex flex-col gap-0.5 text-right">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Total Kgs</span>
<span className="text-lg font-black text-emerald-600">{item.totalKgs.toLocaleString()}</span>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-2 bg-slate-900 text-white rounded-xl p-4 flex items-center justify-between shadow-sm">
<span className="text-xs font-bold uppercase tracking-wider text-slate-400">Total Order</span>
<div className="flex items-center gap-6">
<div className="flex flex-col items-end">
<span className="text-[10px] font-medium text-slate-400 uppercase">Bags</span>
<span className="text-lg font-black">{totalBags}</span>
</div>
<div className="flex flex-col items-end">
<span className="text-[10px] font-medium text-slate-400 uppercase">Total Kgs</span>
<span className="text-lg font-black text-emerald-400">{totalKgs.toLocaleString()}</span>
</div>
</div>
</div>
</>
) : (
<div className="py-8 text-center bg-slate-50/50 rounded-xl border border-slate-100">
<ShoppingCart className="mx-auto text-slate-300 mb-2" size={24} />
<span className="text-sm font-semibold text-slate-500">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-400 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-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">
<TrendingUp className="text-slate-700" size={18} />
<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-400">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-3">
{items.map((item, idx) => (
<div key={idx} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm flex flex-col gap-3 transition-shadow hover:shadow-md">
<div className="flex justify-between items-start gap-4 border-b border-slate-50 pb-3">
<span className="font-bold text-slate-900 text-sm">{item.name}</span>
</div>
<div className="grid grid-cols-3 gap-2 bg-slate-50 p-3 rounded-lg border border-slate-100">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Potential</span>
<span className="text-sm font-semibold text-slate-700">{item.actualPotential}</span>
</div>
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-3">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ordered</span>
<span className="text-sm font-semibold text-slate-700">{item.totalOrdered}</span>
</div>
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-3">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Difference</span>
<span className={`text-base font-black ${item.difference < 0 ? 'text-rose-600' : 'text-emerald-600'}`}>
{item.difference > 0 ? '+' : ''}{item.difference}
</span>
</div>
</div>
{item.reason && (
<div className="flex items-center justify-between p-3 rounded-lg border border-slate-100 bg-white">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Reason</span>
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold border uppercase tracking-wider bg-amber-50 text-amber-700 border-amber-200">
{item.reason}
</span>
</div>
)}
</div>
))}
</div>
);
return (
<div className="space-y-6">
{regularPotential.length > 0 && renderTable(regularPotential)}
{extraOrdered.length > 0 && (
<div className="space-y-4 pt-2 border-t border-slate-100 mt-4">
<div className="flex items-center gap-2">
<ShoppingCart className="text-slate-400" size={14} />
<h3 className="text-[11px] font-bold text-slate-500 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-400">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-400 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-400 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-400 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-400 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-400 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-400 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="rounded-xl border border-slate-200/80 bg-slate-50 p-6 flex flex-col items-center justify-center text-center gap-2">
<Camera className="text-slate-400" size={24} />
<span className="text-xs font-semibold text-slate-500">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="h-40 bg-gradient-to-br from-slate-100 to-slate-200/70 p-6 flex flex-col items-center justify-center text-center gap-2 border-b border-slate-200/50">
<div className="w-12 h-12 rounded-full bg-[var(--tiles-card-bg)] shadow-sm flex items-center justify-center text-slate-500">
<ImageIcon size={22} />
</div>
<span className="text-xs font-bold text-slate-600 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">
<Store className="text-slate-700" size={18} />
<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-400 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-400 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-400 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-400" /> {phone}
</span>
</div>
<div className="flex items-start justify-between py-1 border-b border-slate-100">
<span className="text-slate-400 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-400" /> {email}
</span>
</div>
<div className="py-1 border-b border-slate-100 space-y-1">
<span className="text-slate-400 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-400 font-bold uppercase">ROUTE</span>
<span className="font-bold text-slate-900 text-right">{route}</span>
</div>
</div>
{/* Notes Container */}
<div className="bg-slate-50/80 border border-slate-200/80 rounded-xl p-3.5 space-y-1.5 text-xs">
<div className="font-bold text-slate-600 flex items-center gap-1.5 text-[11px] uppercase tracking-wider">
<FileText size={12} /> NOTES
</div>
<p className="text-slate-700 leading-relaxed font-medium">
{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">
<TrendingUp className="text-slate-700" size={18} />
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Store Potential</h2>
</div>
<span className="text-xs font-medium text-slate-400">Total capacity</span>
</div>
<div className="flex flex-col gap-3">
{storePotentialList.map((item, idx) => (
<div key={idx} className="bg-slate-50 rounded-xl border border-slate-100 p-4 flex items-center justify-between transition-colors hover:bg-white">
<span className="font-bold text-slate-900 text-sm">{item.name}</span>
<div className="flex flex-col items-end">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Potential</span>
<span className="text-sm font-semibold text-emerald-600">{item.actualPotential} KG</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">
<Truck className="text-slate-700" size={18} />
<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-400 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-400 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-400 font-bold uppercase">PHONE</span>
<span className="font-bold text-slate-900 flex items-center gap-1">
<Phone size={11} className="text-slate-400" /> {distPhone}
</span>
</div>
<div className="flex justify-between py-1">
<span className="text-slate-400 font-bold uppercase">EMAIL</span>
<span className="font-bold text-slate-900 flex items-center gap-1">
<Mail size={11} className="text-slate-400" /> {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-400 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-400 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-400 font-bold uppercase">EMAIL</span>
<span className="font-bold text-slate-900 flex items-center gap-1">
<Mail size={11} className="text-slate-400" /> {officerEmail}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,57 @@
import { Pencil, Trash2 } from 'lucide-react';
import type { FormScreenField } from '../../api/types';
export interface GridTableProps {
data: Record<string, unknown>[];
columns: FormScreenField[];
onEdit?: (idx: number) => void;
onDelete?: (idx: number) => void;
}
export function GridTable({ data, columns, onEdit, onDelete }: GridTableProps) {
return (
<div className="flex flex-col gap-4">
{data.map((row, rowIdx) => (
<div key={rowIdx} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm flex flex-col gap-3 transition-shadow hover:shadow-md">
{columns.map((col, colIdx) => {
const val = row[col.id];
let displayVal = String(val ?? '-');
if (col.data_type === 'select' || col.data_type === 'multiselect') {
const opt = col.properties?.options?.find((o: any) => String(o.value) === String(val));
if (opt) displayVal = opt.label;
}
return (
<div key={col.id || colIdx} className="flex justify-between items-center gap-4 border-b border-slate-50 pb-2 last:border-0 last:pb-0">
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">{col.name}</span>
<span className="text-sm font-semibold text-slate-800 text-right">{displayVal}</span>
</div>
);
})}
{(onEdit || onDelete) && (
<div className="border-t border-slate-100 pt-3 mt-1 flex items-center justify-end gap-3">
{onEdit && (
<button
type="button"
onClick={() => onEdit(rowIdx)}
className="flex items-center gap-1.5 px-3 py-1.5 text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg font-medium text-xs border border-blue-100 transition-colors shadow-sm"
>
<Pencil size={14} /> Edit
</button>
)}
{onDelete && (
<button
type="button"
onClick={() => onDelete(rowIdx)}
className="flex items-center gap-1.5 px-3 py-1.5 text-red-600 bg-red-50 hover:bg-red-100 rounded-lg font-medium text-xs border border-red-100 transition-colors shadow-sm"
>
<Trash2 size={14} /> Delete
</button>
)}
</div>
)}
</div>
))}
</div>
);
}

View File

@ -1,35 +1,297 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { orderBookingClient } from '../../api/clients';
import { ORDER_BOOKING } from '../../api/config';
import { useDetailViewData } from './useDetailViewData';
import { OrderCard } from '../cards/OrderCard';
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: string | number;
instanceId: number | string;
columns?: 1 | 2 | 3;
onDataLoad?: (data: any) => void;
onDataLoad?: (data: Record<string, unknown>) => void;
onBack?: () => void;
}
export function OrderDetail({ instanceId, onDataLoad }: WiredDetailViewProps) {
const { data, config, loading, error } = useDetailViewData(
orderBookingClient,
ORDER_BOOKING.detailViews.ORDERS,
instanceId
);
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(() => {
if (data && onDataLoad) {
onDataLoad(data);
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 || {});
}
}, [data, onDataLoad]);
} catch (e) {
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
} finally {
if (live) setLoading(false);
}
}
run();
return () => { live = false; };
}, [instanceId]);
if (loading) return <div className="p-4 text-gray-500">Loading Order Details...</div>;
if (error) return <div className="p-4 text-red-500">{error}</div>;
if (!data) return <div className="p-4 text-gray-500">No data found.</div>;
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="pb-24">
<OrderCard row={data} fields={config?.fields || []} isDetailView={true} />
<div className="flex flex-col gap-4 bg-[var(--tiles-card-bg)] 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 items-center gap-3 mb-1">
{onBack && (
<button onClick={onBack} className="p-1.5 text-slate-400 hover:text-slate-800 hover:bg-slate-100 rounded-full transition-colors">
<ArrowLeft size={18} strokeWidth={2.5} />
</button>
)}
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Order ID</div>
</div>
<div className="text-2xl font-black text-slate-900 mb-2">{String(orderId)}</div>
<div className="flex items-center gap-3 text-xs text-slate-500 font-medium pl-1">
<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 className="border border-slate-100 bg-slate-50 rounded-xl p-4 flex flex-col gap-1 min-w-[200px]">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
<Store size={12} /> Store
</div>
<div className="text-sm font-bold text-slate-900">{String(storeName)}</div>
<div className="flex items-center gap-2 text-[10px] text-slate-500 mt-1">
<span className="flex items-center gap-1"><MapPin size={12} /> {String(routeCode)} - {String(routeName)}</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-slate-500 mt-0.5">
<span className="flex items-center gap-1"><User size={12} /> {String(distributorName)}</span>
</div>
</div>
</div>
{/* Middle Stats Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="border border-slate-200 rounded-xl p-5 bg-[var(--tiles-card-bg)] shadow-sm flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">
<Package size={14} className="text-emerald-500" /> Total Bags
</div>
<div className="text-2xl font-bold text-slate-900">{String(totalBags)}</div>
</div>
<div className="border border-slate-200 rounded-xl p-5 bg-[var(--tiles-card-bg)] shadow-sm flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">
<Weight size={14} className="text-blue-500" /> Total Kilograms
</div>
<div className="text-2xl font-bold text-slate-900">{Number(totalKgs).toLocaleString()}</div>
</div>
<div className="border border-slate-200 rounded-xl p-5 bg-[var(--tiles-card-bg)] shadow-sm flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">
<List size={14} className="text-slate-500" /> Line Items
</div>
<div className="text-2xl font-bold text-slate-900">{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">
<div className="p-4 border-b border-slate-100 flex items-center gap-2 bg-slate-50">
<Package size={14} className="text-slate-500" />
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">Order Items</span>
</div>
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-3 bg-slate-50">
{itemsArray.map((item: any, idx: number) => {
const product = item.product_name_3 || '-';
const category = item.product_category_3 || '-';
const sku = item.sku_code_3 || '-';
const bags = item.bags_3 || '0';
const brCode = item.br_code_3 || '-';
const weight = item.sku_3 ? `${item.sku_3} kg` : '-';
return (
<div key={idx} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm flex flex-col gap-3 transition-shadow hover:shadow-md">
<div className="flex justify-between items-start gap-4 border-b border-slate-50 pb-3">
<div className="flex flex-col gap-1">
<span className="font-bold text-slate-900 text-sm">{String(product)}</span>
<span className="text-[11px] text-slate-400 font-medium uppercase tracking-wide">{String(category)}</span>
</div>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold border uppercase tracking-wider whitespace-nowrap bg-blue-50 text-blue-700 border-blue-200`}>
{String(sku)}
</span>
</div>
<div className="grid grid-cols-2 gap-3 bg-slate-50 p-3 rounded-lg border border-slate-100">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Weight</span>
<span className="text-sm font-semibold text-slate-700">{String(weight)}</span>
</div>
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-3">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">BR Code</span>
<span className="text-sm font-semibold text-slate-700">{String(brCode)}</span>
</div>
</div>
<div className="flex items-center justify-between p-3 rounded-lg border border-slate-100 bg-white">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Bags</span>
<span className="text-lg font-black text-slate-900">{String(bags)}</span>
</div>
</div>
</div>
);
})}
<div className="mt-2 bg-slate-900 text-white rounded-xl p-4 flex items-center justify-between shadow-sm">
<span className="text-xs font-bold uppercase tracking-wider text-slate-400">Total Order</span>
<div className="flex flex-col items-end">
<span className="text-[10px] font-medium text-slate-400 uppercase">Total Bags</span>
<span className="text-lg font-black text-emerald-400">{String(totalBags)}</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-400 uppercase tracking-wider mb-4">
<User size={14} className="text-slate-500" /> 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-500">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-500" />
</div>
<div className="overflow-hidden">
<div className="text-[10px] font-bold text-slate-400 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-500" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 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-400 uppercase tracking-wider mb-4">
<Hash size={14} className="text-slate-500" /> Order Meta
</div>
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-500">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-500">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-500">Date of Order</span>
<span className="font-bold text-slate-900">{String(dateOfOrder)}</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,103 @@
import { ArrowUpRight, ArrowDownRight, Pencil, Trash2 } from 'lucide-react';
import { cn } from '../../lib/cn';
export interface MiningItem {
id: string;
productName: string;
badge?: {
label: string;
type: 'error' | 'success' | 'info' | 'warning' | 'default';
};
status?: string;
potentialKgs: number;
orderedKgs: number;
}
export interface PotentialMiningTableProps {
data: MiningItem[];
onEdit?: (item: MiningItem) => void;
onDelete?: (item: MiningItem) => void;
hideOrderDetails?: boolean;
}
export function PotentialMiningTable({ data, onEdit, onDelete, hideOrderDetails }: PotentialMiningTableProps) {
return (
<div className="flex flex-col gap-4">
{data.map((item) => {
const difference = item.orderedKgs - item.potentialKgs;
const isSurplus = difference >= 0;
const absDiff = Math.abs(difference);
return (
<div key={item.id} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm flex flex-col gap-4 transition-shadow hover:shadow-md">
<div className="flex justify-between items-start gap-4">
<div className="flex flex-col gap-1.5">
<span className="font-bold text-slate-900 text-[15px]">{item.productName}</span>
{item.badge && (
<span className={cn(
"px-2 py-0.5 rounded text-[10px] font-semibold self-start",
item.badge.type === 'error' && "bg-red-50 text-red-500",
item.badge.type === 'success' && "bg-emerald-50 text-emerald-600",
item.badge.type === 'info' && "bg-blue-50 text-blue-500",
item.badge.type === 'warning' && "bg-amber-50 text-amber-600",
item.badge.type === 'default' && "bg-slate-100 text-slate-600"
)}>
{item.badge.label}
</span>
)}
</div>
</div>
<div className="grid grid-cols-3 gap-2 bg-slate-50 p-3 rounded-lg border border-slate-100">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Potential</span>
<span className="text-sm font-semibold text-slate-800">{item.potentialKgs}</span>
</div>
{!hideOrderDetails ? (
<>
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-3">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ordered</span>
<span className="text-sm font-semibold text-slate-800">{item.orderedKgs}</span>
</div>
<div className="flex flex-col gap-0.5 border-l border-slate-200 pl-3">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Diff</span>
<span className={`text-base font-black ${isSurplus ? 'text-emerald-600' : 'text-red-500'}`}>
{isSurplus ? '+' : '-'}{absDiff}
</span>
</div>
</>
) : (
<div className="col-span-2"></div>
)}
</div>
{(onEdit || onDelete) && (
<div className="border-t border-slate-100 pt-3 mt-1 flex items-center justify-end gap-3">
{onEdit && (
<button
type="button"
onClick={() => onEdit(item)}
className="flex items-center gap-1.5 px-3 py-1.5 text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg font-medium text-xs border border-blue-100 transition-colors shadow-sm"
>
<Pencil size={14} /> Edit
</button>
)}
{onDelete && (
<button
type="button"
onClick={() => onDelete(item)}
className="flex items-center gap-1.5 px-3 py-1.5 text-red-600 bg-red-50 hover:bg-red-100 rounded-lg font-medium text-xs border border-red-100 transition-colors shadow-sm"
>
<Trash2 size={14} /> Delete
</button>
)}
</div>
)}
</div>
);
})}
</div>
);
}

View File

@ -1,39 +1,461 @@
import { useEffect, useState } from 'react';
import { storeClient } from '../../api/clients';
import { STORE } from '../../api/config';
import type { WiredDetailViewProps } from './OrderDetail';
import { useDetailViewData } from './useDetailViewData';
import { STORE, APP_ID } from '../../api/config';
import { Card } from '../reusable/Card';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { StoreCard } from '../cards/StoreCard';
import { MapPin } from 'lucide-react';
import { ArrowLeft, Store, User, Truck, MapPin, TrendingUp, Phone, Navigation2, Edit3, Navigation, Mail, FileText, Hash } from 'lucide-react';
import { Button } from '../buttons/Button';
export function StoreDetail({ instanceId }: WiredDetailViewProps) {
const { data, loading, error } = useDetailViewData(storeClient, STORE.detailViews.STORE, instanceId);
export interface StoreDetailProps {
instanceId: string | number;
onBack?: () => void;
onEdit?: () => void;
}
if (error) return <EmptyState title="Couldn't load record" hint={error} />;
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Store..." /></div>;
if (!data) return <EmptyState title="No details found" />;
export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) {
const [data, setData] = useState<Record<string, unknown> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const locObj = data.store_location as Record<string, unknown> | null;
const lat = locObj?.latitude || data.latitude;
const lng = locObj?.longitude || data.longitude;
const hasLocation = lat && lng && lat !== '—' && lng !== '—';
useEffect(() => {
let live = true;
async function run() {
setLoading(true);
setError(null);
try {
const r = await storeClient.detailView(STORE.detailViews.STORE, instanceId);
if (live) setData(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="Store Details">
<EmptyState title="Couldnt load record" hint={error} />
</Card>
);
}
if (loading) {
return (
<Card title="Store Details">
<div className="py-8 flex justify-center"><Spinner label="Loading details…" /></div>
</Card>
);
}
if (!data || Object.keys(data).length === 0) {
return (
<Card title="Store Details">
<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) || '-';
delete remainingData.current_state_id;
const isSuccess = ['created', 'active', 'approve'].some(s => String(stateName).toLowerCase().includes(s));
const badgeClass = isSuccess ? 'bg-emerald-100 text-emerald-700' : 'bg-blue-100 text-blue-700';
// Store Overview
const storeCode = extract('store_code', remainingData) || '-';
const businessName = extract('business_name', remainingData) || '-';
const area = extract('area', remainingData);
const completeAddress = extract('complete_address', remainingData);
const pinCode = extract('pin_code', remainingData);
// Owner Info
const ownerName = extract('owner_name', remainingData) || '-';
const email = extract('email', remainingData);
let phoneNumber = extract('phone_number', remainingData);
let dialPhone = '';
if (typeof phoneNumber === 'object' && phoneNumber !== null) {
dialPhone = (phoneNumber as any).phone_with_dial_code || (phoneNumber as any).phone || '';
phoneNumber = (phoneNumber as any).phone_with_dial_code || (phoneNumber as any).phone || '-';
} else {
phoneNumber = phoneNumber || '-';
dialPhone = String(phoneNumber);
}
// Distributor Info
const distributorName = extract('distributor_name', remainingData);
const distributorOwnerName = extract('distributor_owner_name', remainingData);
const distributorEmail = extract('distributor_email', remainingData);
let distributorPhone = extract('distributor_phone_number', remainingData);
if (typeof distributorPhone === 'object' && distributorPhone !== null) {
distributorPhone = (distributorPhone as any).phone_with_dial_code || (distributorPhone as any).phone || '-';
} else {
distributorPhone = distributorPhone || '-';
}
// Route Info
const routeCode = extract('route_code', remainingData);
const routeName = extract('route_name', remainingData);
const subRoute = extract('sub_route', remainingData);
// Location
const storeLocation = extract('store_location', remainingData);
let lat = null;
let lng = null;
if (storeLocation) {
let locObj = storeLocation;
if (typeof locObj === 'string') {
try { locObj = JSON.parse(locObj); } catch (e) { }
}
if (locObj && typeof locObj === 'object') {
lat = (locObj as any).latitude;
lng = (locObj as any).longitude;
}
}
// Potential
const potential = extract('potential', remainingData);
let totalPotential = 0;
if (Array.isArray(potential)) {
potential.forEach((p: any) => {
totalPotential += (Number(p.quantity) || 0);
});
}
// Image
const storeImage = extract('store_image', remainingData);
let imageUrl = '';
if (Array.isArray(storeImage) && storeImage.length > 0) {
imageUrl = `${storeClient.baseUrl}/app/${APP_ID}/view/files/${storeImage[0].uuid}/preview`;
}
// Meta
const createdAtKey = Object.keys(remainingData).find(k => k.endsWith('__created_at'));
const createdAt = createdAtKey ? extract(createdAtKey, remainingData) : '-';
const userIdKey = Object.keys(remainingData).find(k => k.endsWith('__user_id'));
const userObj = userIdKey ? extract(userIdKey, remainingData) : null;
const userName = userObj && typeof userObj === 'object' ? (userObj as any).name || (userObj as any).email : '-';
return (
<div className="w-full space-y-6">
<StoreCard row={data} isDetailView={true} />
{hasLocation && (
<a
href={`https://www.google.com/maps/search/?api=1&query=${lat},${lng}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full bg-blue-50 hover:bg-blue-100 text-blue-700 font-bold py-3.5 px-4 rounded-xl border border-blue-200 transition-colors shadow-sm"
<div className="flex flex-col gap-6 w-full max-w-full">
{/* Top Bar */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 p-3">
<div className="flex items-center gap-2 text-sm font-medium">
{onBack && (
<button
onClick={onBack}
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-500 hover:text-slate-900 transition-colors mr-1"
>
<MapPin size={18} />
<span>View on Google Maps</span>
</a>
<ArrowLeft size={18} />
</button>
)}
<div className="w-8 h-8 rounded-lg bg-slate-900 text-white flex items-center justify-center">
<Store size={16} />
</div>
<span className="text-slate-800 font-bold">{String(storeCode)}</span>
</div>
<div className="flex items-center gap-3">
{dialPhone && dialPhone !== '-' && (
<Button size="sm" variant="secondary" iconLeft={<Phone size={14} />} onClick={() => window.open(`tel:${dialPhone}`)}>Call</Button>
)}
{lat && lng && (
<Button size="sm" variant="primary" className="bg-slate-900 hover:bg-slate-800 text-white border-transparent" iconLeft={<Navigation size={14} />} onClick={() => window.open(`https://maps.google.com/maps?daddr=${lat},${lng}`, '_blank')}>Directions</Button>
)}
{onEdit && (
<Button size="sm" variant="secondary" iconLeft={<Edit3 size={14} />} onClick={onEdit}>Edit Store</Button>
)}
</div>
</div>
{/* Hero Card */}
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200 shadow-sm overflow-hidden flex flex-col md:flex-row">
{/* Image Section */}
{imageUrl ? (
<div className="md:w-1/3 lg:w-1/4 h-48 md:h-auto relative bg-slate-100">
<img src={imageUrl} alt={String(businessName)} className="w-full h-full object-cover" />
</div>
) : (
<div className="md:w-1/3 lg:w-1/4 h-48 md:h-auto flex items-center justify-center bg-slate-100 border-r border-slate-200">
<Store size={48} className="text-slate-300" />
</div>
)}
{/* Content Section */}
<div className="flex-1 flex flex-col">
<div className="p-6 flex-1">
<div className="flex items-center gap-3 mb-4">
<div className={`px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider flex items-center gap-1.5 ${badgeClass}`}>
<span className="w-1.5 h-1.5 rounded-full bg-current opacity-75"></span>
{String(stateName)}
</div>
<div className="px-2.5 py-1 rounded-full bg-slate-100 text-slate-600 text-[10px] font-bold uppercase tracking-wider flex items-center gap-1.5">
<Hash size={12} />
{String(storeCode)}
</div>
</div>
<h1 className="text-2xl md:text-3xl font-black text-slate-900 tracking-tight mb-2">{String(businessName)}</h1>
<div className="flex items-start gap-2 text-slate-500 text-sm">
<MapPin size={16} className="mt-0.5 shrink-0" />
<span>
{[completeAddress, area].filter(Boolean).join(', ')}
{pinCode ? `${pinCode}` : ''}
</span>
</div>
</div>
{/* Bottom Stats Row */}
<div className="grid grid-cols-3 border-t border-slate-100 bg-slate-50/50">
<div className="p-4 border-r border-slate-100">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Owner</div>
<div className="text-sm font-semibold text-slate-900 truncate">{String(ownerName)}</div>
</div>
<div className="p-4 border-r border-slate-100">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Route</div>
<div className="text-sm font-semibold text-slate-900 truncate">{String(routeName || '-')}{subRoute ? ` · ${subRoute}` : ''}</div>
</div>
<div className="p-4">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Potential</div>
<div className="text-sm font-semibold text-slate-900 truncate">
{totalPotential} <span className="text-slate-500 font-normal">units</span>
</div>
</div>
</div>
</div>
</div>
{/* Grid Layout */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Left Column (Spans 2) */}
<div className="xl:col-span-2 flex flex-col gap-6">
{/* Contact Card */}
<Card pad={false} className="overflow-hidden border-slate-200 shadow-sm">
<div className="flex items-center gap-2 p-4 border-b border-slate-100 bg-[var(--tiles-card-bg)]">
<User size={16} className="text-slate-400" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider m-0">Contact</h3>
</div>
<div className="p-5 grid grid-cols-1 sm:grid-cols-2 gap-y-6 gap-x-8 bg-[var(--tiles-card-bg)]">
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 flex items-center justify-center shrink-0 border border-slate-100">
<User size={14} className="text-slate-400" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Owner</div>
<div className="text-sm font-semibold text-slate-900">{String(ownerName)}</div>
</div>
</div>
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 flex items-center justify-center shrink-0 border border-slate-100">
<Phone size={14} className="text-slate-400" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Phone</div>
<div className="text-sm font-semibold text-slate-900">{String(phoneNumber)}</div>
</div>
</div>
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 flex items-center justify-center shrink-0 border border-slate-100">
<Mail size={14} className="text-slate-400" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Email</div>
<div className="text-sm font-semibold text-slate-900 break-all">{email ? String(email) : '-'}</div>
</div>
</div>
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 flex items-center justify-center shrink-0 border border-slate-100">
<MapPin size={14} className="text-slate-400" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Address</div>
<div className="text-sm font-semibold text-slate-900">
{[completeAddress, area, pinCode].filter(Boolean).join(', ')}
</div>
</div>
</div>
</div>
</Card>
{/* Route Assignment Card */}
<Card pad={false} className="overflow-hidden border-slate-200 shadow-sm">
<div className="flex items-center gap-2 p-4 border-b border-slate-100 bg-[var(--tiles-card-bg)]">
<TrendingUp size={16} className="text-slate-400" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider m-0">Route Assignment</h3>
</div>
<div className="p-5 grid grid-cols-1 sm:grid-cols-3 gap-4 bg-[var(--tiles-card-bg)]">
<div className="p-4 rounded-xl border border-slate-100 bg-slate-50">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Route</div>
<div className="text-base font-bold text-slate-900">{routeName ? String(routeName) : '-'}</div>
<div className="text-xs text-slate-500 mt-1">Code {routeCode ? String(routeCode) : '-'}</div>
</div>
<div className="p-4 rounded-xl border border-slate-100 bg-slate-50">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Sub Route</div>
<div className="text-base font-bold text-slate-900">{subRoute ? String(subRoute) : '-'}</div>
</div>
<div className="p-4 rounded-xl border border-slate-100 bg-slate-50">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Area</div>
<div className="text-base font-bold text-slate-900">{area ? String(area) : '-'}</div>
<div className="text-xs text-slate-500 mt-1">PIN {pinCode ? String(pinCode) : '-'}</div>
</div>
</div>
</Card>
{/* Order Potential Card */}
{Array.isArray(potential) && potential.length > 0 && (
<Card pad={false} className="overflow-hidden border-slate-200 shadow-sm">
<div className="flex items-center gap-2 p-4 border-b border-slate-100 bg-[var(--tiles-card-bg)]">
<FileText size={16} className="text-slate-400" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider m-0">Order Potential</h3>
</div>
<div className="bg-[var(--tiles-card-bg)] p-4">
<div className="flex flex-col gap-3">
{potential.map((p: any, idx: number) => {
if (!p.product_category && !p.quantity) return null;
return (
<div key={idx} className="bg-slate-50 rounded-xl border border-slate-100 p-4 flex items-center justify-between transition-colors hover:bg-white">
<span className="font-bold text-slate-900 text-sm">{p.product_category ? String(p.product_category) : '-'}</span>
<div className="flex flex-col items-end">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Potential</span>
<span className="text-sm font-semibold text-emerald-600">{p.quantity ? String(p.quantity) : '0'}</span>
</div>
</div>
);
})}
</div>
<div className="mt-4 bg-slate-900 text-white rounded-xl p-4 flex items-center justify-between shadow-sm">
<span className="text-xs font-bold uppercase tracking-wider text-slate-400">Total Potential</span>
<span className="text-lg font-black text-emerald-400">{totalPotential}</span>
</div>
</div>
</Card>
)}
{/* Distributor Card */}
<Card pad={false} className="overflow-hidden border-slate-200 shadow-sm">
<div className="flex items-center gap-2 p-4 border-b border-slate-100 bg-[var(--tiles-card-bg)]">
<Truck size={16} className="text-slate-400" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider m-0">Distributor</h3>
</div>
<div className="p-5 bg-[var(--tiles-card-bg)]">
<div className="p-4 rounded-xl border border-slate-100 bg-slate-50">
<div className="text-sm font-bold text-slate-900 mb-1">{distributorName ? String(distributorName) : '-'}</div>
<div className="text-xs text-slate-500 mb-4">Owner · {distributorOwnerName ? String(distributorOwnerName) : '-'}</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-8">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-[var(--tiles-card-bg)] flex items-center justify-center border border-slate-200">
<Phone size={12} className="text-slate-400" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Phone</div>
<div className="text-xs font-semibold text-slate-900">{String(distributorPhone)}</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-[var(--tiles-card-bg)] flex items-center justify-center border border-slate-200">
<Mail size={12} className="text-slate-400" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Email</div>
<div className="text-xs font-semibold text-slate-900">{distributorEmail ? String(distributorEmail) : '-'}</div>
</div>
</div>
</div>
</div>
</div>
</Card>
</div>
{/* Right Column (Spans 1) */}
<div className="xl:col-span-1 flex flex-col gap-6">
{/* Location Map */}
<Card pad={false} className="overflow-hidden border-slate-200 shadow-sm flex flex-col">
<div className="flex items-center gap-2 p-4 border-b border-slate-100 bg-[var(--tiles-card-bg)]">
<MapPin size={16} className="text-slate-400" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider m-0">Location</h3>
</div>
<div className="bg-slate-100 flex-1 relative min-h-[250px]">
{lat && lng ? (
<>
<iframe
title="Store Location Map"
width="100%"
height="100%"
style={{ border: 0, position: 'absolute', inset: 0 }}
loading="lazy"
allowFullScreen
src={`https://maps.google.com/maps?q=${lat},${lng}&hl=en&z=15&output=embed`}
></iframe>
<div
className="absolute bottom-3 right-3 w-8 h-8 bg-[var(--tiles-card-bg)]/90 backdrop-blur-sm rounded shadow flex items-center justify-center cursor-pointer hover:bg-[var(--tiles-card-bg)] transition-colors"
onClick={() => window.open(`https://maps.google.com/maps?q=${lat},${lng}`, '_blank')}
>
<Navigation2 size={16} className="text-slate-700" />
</div>
</>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center text-slate-400">
<MapPin size={32} className="mb-2 opacity-50" />
<span className="text-sm font-medium">No location data</span>
</div>
)}
</div>
{lat && lng && (
<div className="p-3 bg-[var(--tiles-card-bg)] text-[10px] text-slate-500 text-center border-t border-slate-100">
Tap map icon to open in Google Maps.
</div>
)}
</Card>
{/* Meta */}
<Card pad={false} className="overflow-hidden border-slate-200 shadow-sm">
<div className="flex items-center gap-2 p-4 border-b border-slate-100 bg-[var(--tiles-card-bg)]">
<Hash size={16} className="text-slate-400" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider m-0">Meta</h3>
</div>
<div className="p-5 flex flex-col gap-3 bg-[var(--tiles-card-bg)]">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500">Store Code</span>
<span className="text-xs font-bold text-slate-900">{String(storeCode)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500">Instance</span>
<span className="text-xs font-bold text-slate-900">#{instanceId}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500">Created by</span>
<span className="text-xs font-bold text-slate-900">{String(userName)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500">Created at</span>
<span className="text-xs font-bold text-slate-900">{createdAt !== '-' ? new Date(createdAt as string).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '-'}</span>
</div>
</div>
</Card>
</div>
</div>
</div>
);
}

View File

@ -5,3 +5,7 @@ export type { WiredDetailViewProps } from './OrderDetail';
export { CallDetail } from './CallDetail';
export { StoreDetail } from './StoreDetail';
export { DailyLogDetail } from './DailyLogDetail';
export { PotentialMiningTable } from './PotentialMiningTable';
export { GridTable } from './GridTable';
export type { GridTableProps } from './GridTable';
export type { MiningItem, PotentialMiningTableProps } from './PotentialMiningTable';

View File

@ -16,7 +16,7 @@ export function SmartGridField({
value: Record<string, unknown>[];
onChange: (val: Record<string, unknown>[]) => void;
}) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(true);
const formRef = useRef<HTMLDivElement>(null);
const [newRow, setNewRow] = useState<Record<string, unknown>>({});
const [editingIdx, setEditingIdx] = useState<number | null>(null);
@ -47,15 +47,6 @@ export function SmartGridField({
setIsModalOpen(true);
};
const startAdd = () => {
setEditingIdx(null);
setNewRow({});
setIsModalOpen(true);
setTimeout(() => {
formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 150);
};
const updateNewRowField = (fieldId: string, val: unknown) => {
let row = { ...newRow, [fieldId]: val };
setErrors(prev => ({ ...prev, [fieldId]: '' }));
@ -114,12 +105,24 @@ export function SmartGridField({
const catColId = columns.find(c => c.id === 'product_category' || c.name === 'Product Category' || c.mapped_workflow_field === 'product_category' || getBaseId(c.id) === 'product_category')?.id;
const isCatSelected = catColId ? !!newRow[catColId] : false;
const isOrderDetails = label.toLowerCase().includes('order');
const hasAnyValue = visibleColumns.some(col => newRow[col.id] !== undefined && newRow[col.id] !== null && String(newRow[col.id]).trim() !== '');
for (const col of visibleColumns) {
const isCategory = col.id === 'product_category' || col.name === 'Product Category' || col.mapped_workflow_field === 'product_category' || getBaseId(col.id) === 'product_category';
const isProductName = col.id === 'product_name' || col.name === 'Product Name' || col.mapped_workflow_field === 'product_name' || getBaseId(col.id) === 'product_name';
const isBags = col.id === 'bags' || col.name === 'Bags' || col.mapped_workflow_field === 'bags' || getBaseId(col.id) === 'bags' || col.id.toLowerCase().includes('quantity') || col.name.toLowerCase().includes('quantity') || col.mapped_workflow_field?.toLowerCase().includes('quantity');
const isRequired = col.mandatory || (isCatSelected && (isProductName || isBags));
if (isRequired && !newRow[col.id]) {
let isRequired = col.mandatory || (isCatSelected && (isProductName || isBags));
if (isOrderDetails && (isCategory || isProductName || isBags)) {
isRequired = true;
}
if (!hasAnyValue) {
isRequired = true; // if completely empty, require fields to prevent empty row addition
}
if (isRequired && (!newRow[col.id] || String(newRow[col.id]).trim() === '')) {
newErrors[col.id] = 'This field is required';
}
}
@ -224,13 +227,29 @@ export function SmartGridField({
}
});
const bagsNum = Number(bags) || 0;
const getBaseId = (id: string) => id.replace(/_\d+$/, '');
const skuCol = columns.find(c => c.id === 'sku' || c.mapped_workflow_field === 'sku' || getBaseId(c.id) === 'sku');
const rowKgsCol = columns.find(c => c.id === 'row_kgs' || c.mapped_workflow_field === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || getBaseId(c.id) === 'rowkgs');
const skuVal = skuCol ? Number(row[skuCol.id]) : Number(row['sku'] || 0);
const rowKgsVal = rowKgsCol ? Number(row[rowKgsCol.id]) : Number(row['row_kgs'] || 0);
let kgsNum = 0;
if (skuVal > 0 && bagsNum > 0) {
kgsNum = skuVal * bagsNum;
} else if (rowKgsVal > 0) {
kgsNum = rowKgsVal;
}
const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : '';
return (
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 shadow-sm flex items-center justify-between">
<div className="flex flex-col min-w-0 pr-4 gap-1.5">
<span className="font-bold text-slate-800 text-[14px] truncate">{productName}</span>
<div className="flex flex-wrap items-center gap-2">
{(!isPotentialMining || hasBagsCol) && (
<span className="font-extrabold text-indigo-600 text-[13px]">{bags} {isPotentialMining ? 'Kgs' : 'Bags'}</span>
<span className="font-extrabold text-indigo-600 text-[13px]">{bags} {isPotentialMining ? 'Kgs' : 'Bags'}{!isPotentialMining ? kgsStr : ''}</span>
)}
{isPotentialMining && diffVal !== null && !isNaN(diffVal) && diffVal !== 0 && (
@ -261,18 +280,7 @@ export function SmartGridField({
</div>
)}
<div className="flex justify-end mt-2">
<Button
type="button"
variant="primary"
size="sm"
onClick={startAdd}
className={`transition-all duration-300 ease-in-out font-bold text-lg leading-none ${isModalOpen || editingIdx !== null ? 'opacity-0 pointer-events-none scale-95 w-0 h-0 p-0 m-0 overflow-hidden' : 'opacity-100 scale-100 rounded-full w-10 h-10 flex items-center justify-center shadow-md'}`}
title="Add Row"
>
+
</Button>
</div>
<div
ref={formRef}

View File

@ -3,8 +3,8 @@ import { Modal } from '../components/reusable';
import { CallsView } from '../components/rv';
import { CallDetail } from '../components/dv';
import { useState } from 'react';
import { Button } from '../components/buttons/Button';
import { Phone, Plus } from 'lucide-react';
import { Button } from '../components/buttons/Button';
import { DynamicForm } from '../components/forms/DynamicForm';
import { ORDER_BOOKING } from '../api/config';
import { orderBookingClient } from '../api/clients';
@ -67,53 +67,14 @@ export function CallsPage() {
}
};
if (instanceId != null) {
return (
<>
<CallsView
refreshKey={refreshKey}
onRowClick={(row) => {
setSelectedRow(row);
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/calls/${id}`);
}}
/>
<div className="flex flex-col h-full bg-slate-50 w-full relative">
<div className="flex-1 pb-24 w-full overflow-y-auto">
<CallDetail instanceId={instanceId} onDataLoad={setSelectedRow} onBack={() => { navigate(`/calls`); setIsFabOpen(false); }} />
</div>
{/* Global Floating Action Button for Add Call */}
<button
onClick={() => { setIsCreating(true); setCreateTitle("Log Visit"); }}
className="fixed bottom-24 right-6 w-[60px] h-[60px] bg-[var(--z-bg-primary-400)] rounded-3xl flex items-center justify-center shadow-[0_4px_12px_rgba(var(--z-bg-primary-400-rgb),0.4)] text-white z-40 hover:opacity-90 transition-transform hover:scale-105 active:scale-95"
>
<Phone size={20} className="text-white" />
</button>
<Modal
open={isCreating}
onClose={() => setIsCreating(false)}
title={createTitle}
width="md"
>
<DynamicForm
client={orderBookingClient}
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
onActivityChange={(name) => setCreateTitle(name)}
/>
</Modal>
<Modal
open={instanceId != null}
onClose={() => { navigate(`/calls`); setIsFabOpen(false); }}
title={instanceId != null ? `Call #${instanceId}` : undefined}
width="lg"
>
{instanceId != null && (
<>
<CallDetail instanceId={instanceId} onDataLoad={setSelectedRow} />
<div className="absolute bottom-6 right-6 flex flex-col items-end gap-3 z-50">
<div className="fixed bottom-24 right-6 flex flex-col items-end gap-3 z-50">
{isFabOpen && (
<div className="flex flex-col gap-2 bg-white p-3 rounded-sm shadow-xl border border-border-subtle animate-in fade-in slide-in-from-bottom-2">
<Button size="sm" variant="secondary" onClick={handlePotentialMiningClick} disabled={miningLoading}>
@ -166,9 +127,69 @@ export function CallsPage() {
<Plus size={24} className={`transition-transform duration-200 ${isFabOpen ? "rotate-45" : ""}`} />
</Button>
</div>
</>
<Modal
open={activeActivity != null}
onClose={() => setActiveActivity(null)}
title={activeActivity?.name}
width="md"
>
{activeActivity && (
<DynamicForm
client={orderBookingClient}
activityId={activeActivity.id}
instanceId={instanceId}
ignorePrefill={activeActivity.id === ORDER_BOOKING.activities.PLACE_ORDER.uid}
customPrefillData={activeActivity.id === ORDER_BOOKING.activities.POTENTIAL_MINING.uid ? miningPrefill : undefined}
onSuccess={() => {
setActiveActivity(null);
setRefreshKey(k => k + 1);
}}
onCancel={() => setActiveActivity(null)}
onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)}
/>
)}
</Modal>
</div>
);
}
return (
<>
<CallsView
refreshKey={refreshKey}
onRowClick={(row) => {
setSelectedRow(row);
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/calls/${id}`);
}}
/>
{/* Global Floating Action Button for Add Call */}
<button
onClick={() => { setIsCreating(true); setCreateTitle("Log Visit"); }}
className="fixed bottom-24 right-6 w-[60px] h-[60px] bg-[var(--z-bg-primary-400)] rounded-3xl flex items-center justify-center shadow-[0_4px_12px_rgba(var(--z-bg-primary-400-rgb),0.4)] text-white z-40 hover:opacity-90 transition-transform hover:scale-105 active:scale-95"
>
<Phone size={20} className="text-white" />
</button>
<Modal
open={isCreating}
onClose={() => setIsCreating(false)}
title={createTitle}
width="md"
>
<DynamicForm
client={orderBookingClient}
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
onActivityChange={(name) => setCreateTitle(name)}
/>
</Modal>
<Modal
open={activeActivity != null}

View File

@ -45,7 +45,7 @@ export function ConsoleLayout() {
</header>
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
<div className="max-w-[720px] w-full mx-auto flex flex-col gap-5">
<div className="w-full max-w-[1200px] mx-auto flex flex-col gap-5">
<Outlet />
</div>
</main>

View File

@ -4,7 +4,7 @@ import { OrdersView } from '../components/rv';
import { OrderDetail } from '../components/dv';
import { useState } from 'react';
import { ShoppingBag, Plus } from 'lucide-react';
import { Plus, ShoppingBag } from 'lucide-react';
import { Button } from '../components/buttons/Button';
import { DynamicForm } from '../components/forms/DynamicForm';
import { ORDER_BOOKING } from '../api/config';
@ -69,53 +69,14 @@ export function OrdersPage() {
}
};
if (instanceId != null) {
return (
<>
<OrdersView
refreshKey={refreshKey}
onRowClick={(row) => {
setSelectedRow(row);
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/orders/${id}`);
}}
/>
<div className="flex flex-col h-full bg-slate-50 w-full relative">
<div className="flex-1 pb-24 w-full overflow-y-auto">
<OrderDetail instanceId={instanceId} onDataLoad={setSelectedRow} onBack={() => { navigate(`/orders`); setIsFabOpen(false); }} />
</div>
{/* Global Floating Action Button for Place Order */}
<button
onClick={() => { setIsCreating(true); setCreateTitle("Place Order"); }}
className="fixed bottom-24 right-6 w-[60px] h-[60px] bg-[var(--z-bg-primary-400)] rounded-3xl flex items-center justify-center shadow-[0_4px_12px_rgba(var(--z-bg-primary-400-rgb),0.4)] text-white z-40 hover:opacity-90 transition-transform hover:scale-105 active:scale-95 cursor-pointer"
>
<ShoppingBag size={24} className="text-white" />
</button>
<Modal
open={isCreating}
onClose={() => setIsCreating(false)}
title={createTitle}
width="md"
>
<DynamicForm
client={orderBookingClient}
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
onActivityChange={(name) => setCreateTitle(name)}
/>
</Modal>
<Modal
open={instanceId != null}
onClose={() => { navigate(`/orders`); setIsFabOpen(false); }}
title={instanceId != null ? `Order #${instanceId}` : undefined}
width="lg"
>
{instanceId != null && (
<>
<OrderDetail instanceId={instanceId} onDataLoad={setSelectedRow} />
<div className="absolute bottom-6 right-6 flex flex-col items-end gap-3 z-50">
<div className="fixed bottom-24 right-6 flex flex-col items-end gap-3 z-50">
{isFabOpen && (
<div className="flex flex-col gap-2 bg-white p-3 rounded-sm shadow-xl border border-border-subtle animate-in fade-in slide-in-from-bottom-2">
<Button size="sm" variant="secondary" onClick={handlePotentialMiningClick} disabled={miningLoading}>
@ -168,9 +129,68 @@ export function OrdersPage() {
<Plus size={24} className={`transition-transform duration-200 ${isFabOpen ? "rotate-45" : ""}`} />
</Button>
</div>
</>
<Modal
open={activeActivity != null}
onClose={() => setActiveActivity(null)}
title={activeActivity?.name}
width="md"
>
{activeActivity && instanceId != null && (
<DynamicForm
client={orderBookingClient}
activityId={activeActivity.id}
instanceId={instanceId}
ignorePrefill={activeActivity.id === ORDER_BOOKING.activities.PLACE_ORDER.uid}
customPrefillData={activeActivity.id === ORDER_BOOKING.activities.POTENTIAL_MINING.uid ? miningPrefill : undefined}
onSuccess={() => {
setActiveActivity(null);
setRefreshKey(k => k + 1);
}}
onCancel={() => setActiveActivity(null)}
onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)}
/>
)}
</Modal>
</div>
);
}
return (
<>
<OrdersView
refreshKey={refreshKey}
onRowClick={(row) => {
setSelectedRow(row);
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/orders/${id}`);
}}
/>
<button
onClick={() => { setIsCreating(true); setCreateTitle("Place Order"); }}
className="fixed bottom-24 right-6 w-[60px] h-[60px] bg-[var(--z-bg-primary-400)] rounded-3xl flex items-center justify-center shadow-[0_4px_12px_rgba(var(--z-bg-primary-400-rgb),0.4)] text-white z-40 hover:opacity-90 transition-transform hover:scale-105 active:scale-95 cursor-pointer"
>
<ShoppingBag size={24} className="text-white" />
</button>
<Modal
open={isCreating}
onClose={() => setIsCreating(false)}
title={createTitle}
width="md"
>
<DynamicForm
client={orderBookingClient}
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
onActivityChange={(name) => setCreateTitle(name)}
/>
</Modal>
<Modal
open={activeActivity != null}

View File

@ -19,6 +19,38 @@ export function StoresPage() {
const [editTitle, setEditTitle] = useState("Edit Store");
const [refreshKey, setRefreshKey] = useState(0);
if (instanceId != null && !isCreating) {
return (
<div className="flex flex-col h-full bg-slate-50 w-full relative">
<div className="flex-1 pb-24 w-full overflow-y-auto">
<StoreDetail instanceId={instanceId} onBack={() => navigate(`/stores`)} />
</div>
{/* Edit store modal can still open over the detail view if needed */}
<Modal
open={editingInstanceId != null}
onClose={() => setEditingInstanceId(null)}
title={editTitle}
width="md"
>
{editingInstanceId != null && (
<DynamicForm
client={storeClient}
activityId={STORE.activities.EDIT_STORE.uid}
instanceId={editingInstanceId}
onSuccess={() => {
setEditingInstanceId(null);
setRefreshKey(k => k + 1);
}}
onCancel={() => setEditingInstanceId(null)}
onActivityChange={(name) => setEditTitle(name)}
/>
)}
</Modal>
</div>
);
}
return (
<>
<StoresView
@ -79,15 +111,6 @@ export function StoresPage() {
/>
)}
</Modal>
<Modal
open={instanceId != null && !isCreating}
onClose={() => navigate(`/stores`)}
title={instanceId != null ? `Store #${instanceId}` : undefined}
width="lg"
>
{instanceId != null && <StoreDetail instanceId={instanceId} />}
</Modal>
</>
);
}