diff --git a/src/components/cards/CallCard.tsx b/src/components/cards/CallCard.tsx index 3b1bb41..cf41366 100644 --- a/src/components/cards/CallCard.tsx +++ b/src/components/cards/CallCard.tsx @@ -240,11 +240,15 @@ export function CallCard({ row, isDetailView = false }: { row: Record {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 (
{productName} - {bags} Bags + {bagsStr} Bags{kgsStr}
); })} diff --git a/src/components/cards/OrderCard.tsx b/src/components/cards/OrderCard.tsx index 3546448..f2cd574 100644 --- a/src/components/cards/OrderCard.tsx +++ b/src/components/cards/OrderCard.tsx @@ -101,12 +101,16 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record 0 ? skuNum * bagsNum : Number(getVal('row_kgs') || getVal('kgs') || getVal('weight') || 0); + const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : ''; return (
{productName} - {bags} Bags + {bagsStr} Bags{kgsStr}
); })} @@ -182,12 +186,16 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record 0 ? skuNum * bagsNum : Number(getVal('row_kgs') || getVal('kgs') || getVal('weight') || 0); + const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : ''; return (
{productName} - {bags} Bags + {bagsStr} Bags{kgsStr}
); })} diff --git a/src/components/dv/CallDetail.tsx b/src/components/dv/CallDetail.tsx index 60925c8..acb4eec 100644 --- a/src/components/dv/CallDetail.tsx +++ b/src/components/dv/CallDetail.tsx @@ -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; + potentialMiningAction?: ReactNode; + placeOrderAction?: ReactNode; + refreshKey?: number; + onBack?: () => void; + onDataLoad?: (data: Record) => void; +} + +export function CallDetail({ + instanceId, + selectedRow, + potentialMiningAction, + placeOrderAction, + refreshKey, + onBack, + onDataLoad, +}: CallDetailProps) { + const [data, setData] = useState | null>(null); + const [potentialData, setPotentialData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ; - if (loading) return
; - if (!data) return ; + 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 ( +
+ +
+ ); + } + + if (loading) { + return ( +
+ +
+ ); + } + + const rowSrc = selectedRow || data || {}; + + // Extract store info with fallbacks matching reference + const selectStore = (rowSrc.select_store || data?.select_store || {}) as Record; + 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 ( -
- +
+ + {/* 1. Primary Summary & Status Banner */} +
+
+ + {/* Title & Badges */} +
+ {onBack && ( + + )} +
+
+ {/* {isPlaceOrderDone && ( + + Ordered + + )} */} + {currentStateName !== 'No data' && ( + + {currentStateName} + + )} + {/* + Productive call + + + On call + */} + {/* + {isPlaceOrderDone ? `Order ${orderId}` : 'Order Not Placed'} + */} +
+ +

+ {storeName} +

+ +
+ + {areaLoc} + + + {route} + + + {fullDateTimeOrder} + +
+
+
+ + {/* Top Right KPI Grid */} +
+
+
TOTAL BAGS
+
{totalBags.toLocaleString()}
+
+
+
TOTAL KGS
+
{totalKgs.toLocaleString()}
+
+
+
LINE ITEMS
+
{lineItemsCount}
+
+
+
SALES OFFICER
+
{salesOfficer}
+
+
+
+ + {/* Workflow Progress Stepper - 3 Timestamps */} +
+
+ + {/* Step 1: Log Visit */} +
+ {isLogVisitDone ? ( +
+ ✓ +
+ ) : ( +
+ +
+ )} +
+
Visit logged
+
+ {isLogVisitDone ? `${time1} • ${date1}` : 'Not performed'} +
+
+
+ + {/* Step 2: Productivity of Visit */} +
+ {isProductivityDone ? ( +
+ ✓ +
+ ) : ( +
+ +
+ )} +
+
Productive call
+
+ {isProductivityDone ? `${time2} • ${date2}` : 'Not performed'} +
+
+
+ + {/* Step 3: Potential Mining */} +
+ {isPotentialMiningDone ? ( +
+ ✓ +
+ ) : ( +
+ +
+ )} +
+
Potential Mining
+
+ {isPotentialMiningDone ? `${timePm} • ${datePm}` : 'Not performed'} +
+
+
+ + {/* Step 4: Place Order */} +
+ {isPlaceOrderDone ? ( +
+ ✓ +
+ ) : ( +
+ +
+ )} +
+
Ordered
+
+ {isPlaceOrderDone ? `${time3} • ${date3}` : 'Not performed'} +
+
+
+ +
+
+
+ + {/* 2. Main Two-Column Layout */} +
+ + {/* MAIN CONTENT (Now on Right, 2/3 width) */} +
+ + {/* Order Details Card */} + {currentStateName.toLowerCase() !== 'no order' && ( +
+
+
+ +

Order Details

+
+
+ + {isPlaceOrderDone ? `${orderId} • ${dateOfOrder}` : 'Not Performed'} + +
+
+ + {/* Table */} +
+ {orderItems.length > 0 ? ( + <> +
+ {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 ( +
+
+
+ {item.name} + {item.category} +
+ + {item.skuCode} + +
+ +
+
+ Bags + {item.bags} +
+
+ Total Kgs + {item.totalKgs.toLocaleString()} +
+
+
+ ); + })} +
+
+ Total Order +
+
+ Bags + {totalBags} +
+
+ Total Kgs + {totalKgs.toLocaleString()} +
+
+
+ + ) : ( +
+ + Order not placed +
+ )} +
+ + {/* Total Summary Row (Moved to table footer) */} + + {/* Meta Footer */} + {isPlaceOrderDone && ( +
+
+ CREATED BY {salesOfficer} • {officerJob || 'Sales Officer'} +
+
+ CREATED AT {fullDateTimeOrder} +
+
+ )} +
+ )} + + {/* Call Potential Card (Below Orders) */} + {callPotentialList.length > 0 && ( +
+
+
+ +

Potential vs Ordered

+
+ Ordered vs opportunity +
+ + {(() => { + const regularPotential = callPotentialList.filter(item => item.actualPotential > 0); + const extraOrdered = callPotentialList.filter(item => item.actualPotential === 0); + + const renderTable = (items: any[]) => ( +
+ {items.map((item, idx) => ( +
+
+ {item.name} +
+
+
+ Potential + {item.actualPotential} +
+
+ Ordered + {item.totalOrdered} +
+
+ Difference + + {item.difference > 0 ? '+' : ''}{item.difference} + +
+
+ {item.reason && ( +
+ Reason + + {item.reason} + +
+ )} +
+ ))} +
+ ); + + return ( +
+ {regularPotential.length > 0 && renderTable(regularPotential)} + + {extraOrdered.length > 0 && ( +
+
+ +

Ordered Outside Potential

+
+ {renderTable(extraOrdered)} +
+ )} +
+ ); + })()} + + {potentialMiningAction && ( +
+ {potentialMiningAction} +
+ )} +
+ )} + + + + {/* Visit Log Card */} +
+
+
+ +

Visit Log

+
+ Logged at {time1} +
+ +
+ {/* Left Key Value Details */} +
+
+ DATE OF VISIT + {dateOfVisit} +
+
+ TIME OF VISIT + {timeOfVisit} +
+
+ STORE STATUS + {storeStatus} +
+
+ CHANNEL + {channel} +
+
+ + {/* Right Store Proof Photo */} +
+
+ UPLOADED PROOF +
+ + {uploadedImages.length > 0 ? ( +
+ {uploadedImages.map((file: any, idx: number) => { + const previewUrl = `${orderBookingClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`; + return ( +
+ {file.original_name { + (e.target as HTMLImageElement).style.display = 'none'; + (e.target as HTMLImageElement).parentElement!.innerHTML = `
${file.original_name || 'Proof Image'}
`; + }} + /> + +
+ ); + })} +
+ ) : ( +
+ + No proof image uploaded +
+ )} +
+
+
+ +
+ + {/* SIDEBAR (Now on Left, 1/3 width) */} +
+ + {/* Store Banner Image Card */} +
+ {storeImages.length > 0 ? ( +
+ Store Front { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> +
+ ) : ( +
+
+ +
+ {storeName} +
+ )} +
+ + {/* Store Info Card */} +
+ + {/* Store Potential Card */} + {storePotentialList.length > 0 && ( +
+
+
+ +

Store Potential

+
+ Total capacity +
+ +
+ {storePotentialList.map((item, idx) => ( +
+ {item.name} +
+ Potential + {item.actualPotential} KG +
+
+ ))} +
+
+ )} + + {/* Distributor Card */} +
+
+ +

Distributor

+
+ +
+
+ COMPANY + {distCompany} +
+
+ CONTACT + {distContact} +
+
+ PHONE + + {distPhone} + +
+
+ EMAIL + + {distEmail} + +
+
+
+ + {/* Performed By Card */} +
+
+ +

Performed by

+
+ +
+
+ NAME + {salesOfficer} +
+
+ ROLE + {officerJob || 'Sales Officer'} +
+
+ EMAIL + + {officerEmail} + +
+
+
+ +
+ +
+
); } diff --git a/src/components/dv/GridTable.tsx b/src/components/dv/GridTable.tsx new file mode 100644 index 0000000..e5caca5 --- /dev/null +++ b/src/components/dv/GridTable.tsx @@ -0,0 +1,57 @@ +import { Pencil, Trash2 } from 'lucide-react'; +import type { FormScreenField } from '../../api/types'; + +export interface GridTableProps { + data: Record[]; + columns: FormScreenField[]; + onEdit?: (idx: number) => void; + onDelete?: (idx: number) => void; +} + +export function GridTable({ data, columns, onEdit, onDelete }: GridTableProps) { + return ( +
+ {data.map((row, rowIdx) => ( +
+ {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 ( +
+ {col.name} + {displayVal} +
+ ); + })} + + {(onEdit || onDelete) && ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ )} +
+ ))} +
+ ); +} diff --git a/src/components/dv/OrderDetail.tsx b/src/components/dv/OrderDetail.tsx index b112efd..344de6a 100644 --- a/src/components/dv/OrderDetail.tsx +++ b/src/components/dv/OrderDetail.tsx @@ -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) => 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 | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 || {}); + } + } catch (e) { + if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); + } finally { + if (live) setLoading(false); + } } - }, [data, onDataLoad]); + run(); + return () => { live = false; }; + }, [instanceId]); - if (loading) return
Loading Order Details...
; - if (error) return
{error}
; - if (!data) return
No data found.
; + if (error) { + return ( + + + + ); + } + + if (loading) { + return ( + +
+
+ ); + } + + if (!data || Object.keys(data).length === 0) { + return ( + + + + ); + } + + 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 ( -
- +
+ {/* Top Section */} +
+
+ {onBack && ( + + )} +
Order ID
+
+
{String(orderId)}
+
+ {String(dateOfOrder)} + Instance #{instanceId} +
+
+
+ Store +
+
{String(storeName)}
+
+ {String(routeCode)} - {String(routeName)} +
+
+ {String(distributorName)} +
+
+
+ + {/* Middle Stats Section */} +
+
+
+ Total Bags +
+
{String(totalBags)}
+
+
+
+ Total Kilograms +
+
{Number(totalKgs).toLocaleString()}
+
+
+
+ Line Items +
+
{itemsArray.length}
+
+
+ + {/* Bottom Section */} +
+ {/* Left Table */} +
+
+ + Order Items +
+
+ {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 ( +
+
+
+ {String(product)} + {String(category)} +
+ + {String(sku)} + +
+ +
+
+ Weight + {String(weight)} +
+
+ BR Code + {String(brCode)} +
+
+ +
+
+ Bags + {String(bags)} +
+
+
+ ); + })} +
+ Total Order +
+ Total Bags + {String(totalBags)} +
+
+
+
+ + {/* Right Sidebar */} +
+ {/* SO Card */} +
+
+ Sales Officer +
+
+
+ {soInitials} +
+
+
{String(soName)}
+
Sales Officer - ID {String(soId)}
+
+
+
+
+
+ +
+
+
Email
+
{String(soEmail)}
+
+
+
+
+ +
+
+
User ID
+
{String(soId)}
+
+
+
+
+ + {/* Order Meta */} +
+
+ Order Meta +
+
+
+ Instance + #{instanceId} +
+
+ State + {String(stateName)} +
+
+ Date of Order + {String(dateOfOrder)} +
+
+
+
+
); } diff --git a/src/components/dv/PotentialMiningTable.tsx b/src/components/dv/PotentialMiningTable.tsx new file mode 100644 index 0000000..4ef4881 --- /dev/null +++ b/src/components/dv/PotentialMiningTable.tsx @@ -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 ( +
+ {data.map((item) => { + const difference = item.orderedKgs - item.potentialKgs; + const isSurplus = difference >= 0; + const absDiff = Math.abs(difference); + + return ( +
+
+
+ {item.productName} + {item.badge && ( + + {item.badge.label} + + )} +
+
+ +
+
+ Potential + {item.potentialKgs} +
+ {!hideOrderDetails ? ( + <> +
+ Ordered + {item.orderedKgs} +
+
+ Diff + + {isSurplus ? '+' : '-'}{absDiff} + +
+ + ) : ( +
+ )} +
+ + + + {(onEdit || onDelete) && ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/src/components/dv/StoreDetail.tsx b/src/components/dv/StoreDetail.tsx index b278e0b..32e2fd7 100644 --- a/src/components/dv/StoreDetail.tsx +++ b/src/components/dv/StoreDetail.tsx @@ -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); - - if (error) return ; - if (loading) return
; - if (!data) return ; - - const locObj = data.store_location as Record | null; - const lat = locObj?.latitude || data.latitude; - const lng = locObj?.longitude || data.longitude; - const hasLocation = lat && lng && lat !== '—' && lng !== '—'; - - return ( -
- - - {hasLocation && ( - - - View on Google Maps - - )} -
- ); +export interface StoreDetailProps { + instanceId: string | number; + onBack?: () => void; + onEdit?: () => void; +} + +export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + 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 ( + + + + ); + } + + if (loading) { + return ( + +
+
+ ); + } + + if (!data || Object.keys(data).length === 0) { + return ( + + + + ); + } + + 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 ( +
+ {/* Top Bar */} +
+
+ {onBack && ( + + )} +
+ +
+ {String(storeCode)} +
+
+ {dialPhone && dialPhone !== '-' && ( + + )} + {lat && lng && ( + + )} + {onEdit && ( + + )} +
+
+ + {/* Hero Card */} +
+ {/* Image Section */} + {imageUrl ? ( +
+ {String(businessName)} +
+ ) : ( +
+ +
+ )} + + {/* Content Section */} +
+
+
+
+ + {String(stateName)} +
+
+ + {String(storeCode)} +
+
+ +

{String(businessName)}

+ +
+ + + {[completeAddress, area].filter(Boolean).join(', ')} + {pinCode ? ` — ${pinCode}` : ''} + +
+
+ + {/* Bottom Stats Row */} +
+
+
Owner
+
{String(ownerName)}
+
+
+
Route
+
{String(routeName || '-')}{subRoute ? ` · ${subRoute}` : ''}
+
+
+
Potential
+
+ {totalPotential} units +
+
+
+
+
+ + {/* Grid Layout */} +
+ + {/* Left Column (Spans 2) */} +
+ + {/* Contact Card */} + +
+ +

Contact

+
+
+
+
+ +
+
+
Owner
+
{String(ownerName)}
+
+
+
+
+ +
+
+
Phone
+
{String(phoneNumber)}
+
+
+
+
+ +
+
+
Email
+
{email ? String(email) : '-'}
+
+
+
+
+ +
+
+
Address
+
+ {[completeAddress, area, pinCode].filter(Boolean).join(', ')} +
+
+
+
+
+ + {/* Route Assignment Card */} + +
+ +

Route Assignment

+
+
+
+
Route
+
{routeName ? String(routeName) : '-'}
+
Code {routeCode ? String(routeCode) : '-'}
+
+
+
Sub Route
+
{subRoute ? String(subRoute) : '-'}
+
+
+
Area
+
{area ? String(area) : '-'}
+
PIN {pinCode ? String(pinCode) : '-'}
+
+
+
+ + {/* Order Potential Card */} + {Array.isArray(potential) && potential.length > 0 && ( + +
+ +

Order Potential

+
+
+
+ {potential.map((p: any, idx: number) => { + if (!p.product_category && !p.quantity) return null; + return ( +
+ {p.product_category ? String(p.product_category) : '-'} +
+ Potential + {p.quantity ? String(p.quantity) : '0'} +
+
+ ); + })} +
+
+ Total Potential + {totalPotential} +
+
+
+ )} + + {/* Distributor Card */} + +
+ +

Distributor

+
+
+
+
{distributorName ? String(distributorName) : '-'}
+
Owner · {distributorOwnerName ? String(distributorOwnerName) : '-'}
+ +
+
+
+ +
+
+
Phone
+
{String(distributorPhone)}
+
+
+
+
+ +
+
+
Email
+
{distributorEmail ? String(distributorEmail) : '-'}
+
+
+
+
+
+
+ +
+ + {/* Right Column (Spans 1) */} +
+ + {/* Location Map */} + +
+ +

Location

+
+
+ {lat && lng ? ( + <> + +
window.open(`https://maps.google.com/maps?q=${lat},${lng}`, '_blank')} + > + +
+ + ) : ( +
+ + No location data +
+ )} +
+ {lat && lng && ( +
+ Tap map icon to open in Google Maps. +
+ )} +
+ + {/* Meta */} + +
+ +

Meta

+
+
+
+ Store Code + {String(storeCode)} +
+
+ Instance + #{instanceId} +
+
+ Created by + {String(userName)} +
+
+ Created at + {createdAt !== '-' ? new Date(createdAt as string).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '-'} +
+
+
+ +
+
+ +
+ ); } diff --git a/src/components/dv/index.ts b/src/components/dv/index.ts index f146b7d..519ce23 100644 --- a/src/components/dv/index.ts +++ b/src/components/dv/index.ts @@ -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'; diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx index 6c00754..4db2018 100644 --- a/src/components/forms/fields/SmartGridField.tsx +++ b/src/components/forms/fields/SmartGridField.tsx @@ -16,7 +16,7 @@ export function SmartGridField({ value: Record[]; onChange: (val: Record[]) => void; }) { - const [isModalOpen, setIsModalOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(true); const formRef = useRef(null); const [newRow, setNewRow] = useState>({}); const [editingIdx, setEditingIdx] = useState(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 (
{productName}
{(!isPotentialMining || hasBagsCol) && ( - {bags} {isPotentialMining ? 'Kgs' : 'Bags'} + {bags} {isPotentialMining ? 'Kgs' : 'Bags'}{!isPotentialMining ? kgsStr : ''} )} {isPotentialMining && diffVal !== null && !isNaN(diffVal) && diffVal !== 0 && ( @@ -261,18 +280,7 @@ export function SmartGridField({
)} -
- -
+
+
+ { navigate(`/calls`); setIsFabOpen(false); }} /> +
+ +
+ {isFabOpen && ( +
+ + {(() => { + const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase(); + if (stateName.includes('ordered') || stateName === 'ordered') { + return ( + + ); + } else if (stateName.includes('productive')) { + return ( + + ); + } else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) { + return null; + } + return ( + + ); + })()} +
+ )} + +
+ + setActiveActivity(null)} + title={activeActivity?.name} + width="md" + > + {activeActivity && ( + { + setActiveActivity(null); + setRefreshKey(k => k + 1); + }} + onCancel={() => setActiveActivity(null)} + onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)} + /> + )} + +
+ ); + } + return ( <> - { navigate(`/calls`); setIsFabOpen(false); }} - title={instanceId != null ? `Call #${instanceId}` : undefined} - width="lg" - > - {instanceId != null && ( - <> - -
- {isFabOpen && ( -
- - {(() => { - const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase(); - if (stateName.includes('ordered') || stateName === 'ordered') { - return ( - - ); - } else if (stateName.includes('productive')) { - return ( - - ); - } else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) { - return null; - } - return ( - - ); - })()} -
- )} - -
- - )} -
- setActiveActivity(null)} diff --git a/src/screens/ConsoleLayout.tsx b/src/screens/ConsoleLayout.tsx index cd9062f..017e13c 100644 --- a/src/screens/ConsoleLayout.tsx +++ b/src/screens/ConsoleLayout.tsx @@ -45,7 +45,7 @@ export function ConsoleLayout() {
-
+
diff --git a/src/screens/OrdersPage.tsx b/src/screens/OrdersPage.tsx index 7f60422..5765eab 100644 --- a/src/screens/OrdersPage.tsx +++ b/src/screens/OrdersPage.tsx @@ -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,6 +69,93 @@ export function OrdersPage() { } }; + if (instanceId != null) { + return ( +
+
+ { navigate(`/orders`); setIsFabOpen(false); }} /> +
+ +
+ {isFabOpen && ( +
+ + {(() => { + const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase(); + if (stateName.includes('ordered') || stateName === 'ordered') { + return ( + + ); + } else if (stateName.includes('productive')) { + return ( + + ); + } else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) { + return null; + } + return ( + + ); + })()} +
+ )} + +
+ + setActiveActivity(null)} + title={activeActivity?.name} + width="md" + > + {activeActivity && instanceId != null && ( + { + setActiveActivity(null); + setRefreshKey(k => k + 1); + }} + onCancel={() => setActiveActivity(null)} + onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)} + /> + )} + +
+ ); + } + return ( <> - {/* Global Floating Action Button for Place Order */} - {(() => { - const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase(); - if (stateName.includes('ordered') || stateName === 'ordered') { - return ( - - ); - } else if (stateName.includes('productive')) { - return ( - - ); - } else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) { - return null; - } - return ( - - ); - })()} -
- )} - -
- - )} - - setActiveActivity(null)} diff --git a/src/screens/StoresPage.tsx b/src/screens/StoresPage.tsx index e7992f2..6daba45 100644 --- a/src/screens/StoresPage.tsx +++ b/src/screens/StoresPage.tsx @@ -19,6 +19,38 @@ export function StoresPage() { const [editTitle, setEditTitle] = useState("Edit Store"); const [refreshKey, setRefreshKey] = useState(0); + if (instanceId != null && !isCreating) { + return ( +
+
+ navigate(`/stores`)} /> +
+ + {/* Edit store modal can still open over the detail view if needed */} + setEditingInstanceId(null)} + title={editTitle} + width="md" + > + {editingInstanceId != null && ( + { + setEditingInstanceId(null); + setRefreshKey(k => k + 1); + }} + onCancel={() => setEditingInstanceId(null)} + onActivityChange={(name) => setEditTitle(name)} + /> + )} + +
+ ); + } + return ( <> )}
- - navigate(`/stores`)} - title={instanceId != null ? `Store #${instanceId}` : undefined} - width="lg" - > - {instanceId != null && } - ); }