details completed
This commit is contained in:
parent
e0d79ffbc1
commit
e03a5a483c
@ -240,11 +240,15 @@ export function CallCard({ row, isDetailView = false }: { row: Record<string, an
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
{row.order_details_3.map((prod: any, idx: number) => {
|
{row.order_details_3.map((prod: any, idx: number) => {
|
||||||
const productName = formatValue(prod.product_name_3 || prod.product_category_3 || 'Unknown Product');
|
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 (
|
return (
|
||||||
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '14px', background: '#f8fafc', padding: '12px 16px', borderRadius: '8px', border: '1px solid #e2e8f0' }}>
|
<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: 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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -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 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 (
|
return (
|
||||||
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', background: '#f8fafc', padding: '8px 12px', borderRadius: '8px' }}>
|
<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: 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>
|
</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 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 (
|
return (
|
||||||
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '14px', background: '#f8fafc', padding: '12px 16px', borderRadius: '8px', border: '1px solid #e2e8f0' }}>
|
<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: 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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,28 +1,934 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
import { orderBookingClient } from '../../api/clients';
|
import { orderBookingClient } from '../../api/clients';
|
||||||
import { ORDER_BOOKING } from '../../api/config';
|
import { ORDER_BOOKING, APP_ID } from '../../api/config';
|
||||||
import type { WiredDetailViewProps } from './OrderDetail';
|
|
||||||
import { useDetailViewData } from './useDetailViewData';
|
|
||||||
import { Spinner } from '../reusable/Spinner';
|
import { Spinner } from '../reusable/Spinner';
|
||||||
import { EmptyState } from '../reusable/EmptyState';
|
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) {
|
export interface CallDetailProps {
|
||||||
const { data, loading, error } = useDetailViewData(orderBookingClient, ORDER_BOOKING.detailViews.CALLS, instanceId);
|
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(() => {
|
useEffect(() => {
|
||||||
if (data && onDataLoad) {
|
let live = true;
|
||||||
onDataLoad(data);
|
async function run() {
|
||||||
}
|
setLoading(true);
|
||||||
}, [data, onDataLoad]);
|
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} />;
|
const storeCode = (r.data?.select_store as any)?.store_code || (r.data?.select_store as any)?.code;
|
||||||
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Call Details..." /></div>;
|
if (storeCode) {
|
||||||
if (!data) return <EmptyState title="No details found" />;
|
orderBookingClient.request<{ potential: { potential: any[] } }>(
|
||||||
|
'POST',
|
||||||
|
'/api/papi2/potential-mining',
|
||||||
|
{ store_code: storeCode, instance_id: String(instanceId) },
|
||||||
|
{ 'TemplateID': '146' }
|
||||||
|
).then(res => {
|
||||||
|
if (live) setPotentialData(res.potential?.potential || []);
|
||||||
|
}).catch(e => console.warn("Failed to fetch potential mining", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (live) setError((e as { message?: string })?.message ?? 'Failed to load details');
|
||||||
|
} finally {
|
||||||
|
if (live) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
return () => {
|
||||||
|
live = false;
|
||||||
|
};
|
||||||
|
}, [instanceId, refreshKey]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[var(--tiles-card-bg)] p-8 rounded-2xl border border-border-default shadow-sm text-center">
|
||||||
|
<EmptyState title="Couldn’t load record" hint={error} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[var(--tiles-card-bg)] p-12 rounded-2xl border border-border-default shadow-sm flex justify-center items-center">
|
||||||
|
<Spinner label="Loading call details…" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowSrc = selectedRow || data || {};
|
||||||
|
|
||||||
|
// Extract store info with fallbacks matching reference
|
||||||
|
const selectStore = (rowSrc.select_store || data?.select_store || {}) as Record<string, any>;
|
||||||
|
const storeName = selectStore.business_name || selectStore.store_name || rowSrc.store_name || 'No data';
|
||||||
|
const storeCode = selectStore.store_code || 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 (
|
return (
|
||||||
<div className="w-full space-y-6 pb-24">
|
<div className="text-slate-800 py-6 lg:py-8 font-sans space-y-6">
|
||||||
<CallCard row={data} isDetailView={true} />
|
|
||||||
|
{/* 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/components/dv/GridTable.tsx
Normal file
57
src/components/dv/GridTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,35 +1,297 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { orderBookingClient } from '../../api/clients';
|
import { orderBookingClient } from '../../api/clients';
|
||||||
import { ORDER_BOOKING } from '../../api/config';
|
import { ORDER_BOOKING } from '../../api/config';
|
||||||
import { useDetailViewData } from './useDetailViewData';
|
import { Card } from '../reusable/Card';
|
||||||
import { OrderCard } from '../cards/OrderCard';
|
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 {
|
export interface WiredDetailViewProps {
|
||||||
instanceId: string | number;
|
instanceId: number | string;
|
||||||
columns?: 1 | 2 | 3;
|
columns?: 1 | 2 | 3;
|
||||||
onDataLoad?: (data: any) => void;
|
onDataLoad?: (data: Record<string, unknown>) => void;
|
||||||
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OrderDetail({ instanceId, onDataLoad }: WiredDetailViewProps) {
|
export function OrderDetail({ instanceId, onDataLoad, onBack }: WiredDetailViewProps) {
|
||||||
const { data, config, loading, error } = useDetailViewData(
|
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
||||||
orderBookingClient,
|
const [loading, setLoading] = useState(true);
|
||||||
ORDER_BOOKING.detailViews.ORDERS,
|
const [error, setError] = useState<string | null>(null);
|
||||||
instanceId
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && onDataLoad) {
|
let live = true;
|
||||||
onDataLoad(data);
|
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 <div className="p-4 text-gray-500">Loading Order Details...</div>;
|
if (error) {
|
||||||
if (error) return <div className="p-4 text-red-500">{error}</div>;
|
return (
|
||||||
if (!data) return <div className="p-4 text-gray-500">No data found.</div>;
|
<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 (
|
return (
|
||||||
<div className="pb-24">
|
<div className="flex flex-col gap-4 bg-[var(--tiles-card-bg)] p-1">
|
||||||
<OrderCard row={data} fields={config?.fields || []} isDetailView={true} />
|
{/* 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/components/dv/PotentialMiningTable.tsx
Normal file
103
src/components/dv/PotentialMiningTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,39 +1,461 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { storeClient } from '../../api/clients';
|
import { storeClient } from '../../api/clients';
|
||||||
import { STORE } from '../../api/config';
|
import { STORE, APP_ID } from '../../api/config';
|
||||||
import type { WiredDetailViewProps } from './OrderDetail';
|
import { Card } from '../reusable/Card';
|
||||||
import { useDetailViewData } from './useDetailViewData';
|
|
||||||
import { Spinner } from '../reusable/Spinner';
|
import { Spinner } from '../reusable/Spinner';
|
||||||
import { EmptyState } from '../reusable/EmptyState';
|
import { EmptyState } from '../reusable/EmptyState';
|
||||||
import { StoreCard } from '../cards/StoreCard';
|
import { ArrowLeft, Store, User, Truck, MapPin, TrendingUp, Phone, Navigation2, Edit3, Navigation, Mail, FileText, Hash } from 'lucide-react';
|
||||||
import { MapPin } from 'lucide-react';
|
import { Button } from '../buttons/Button';
|
||||||
|
|
||||||
export function StoreDetail({ instanceId }: WiredDetailViewProps) {
|
export interface StoreDetailProps {
|
||||||
const { data, loading, error } = useDetailViewData(storeClient, STORE.detailViews.STORE, instanceId);
|
instanceId: string | number;
|
||||||
|
onBack?: () => void;
|
||||||
if (error) return <EmptyState title="Couldn't load record" hint={error} />;
|
onEdit?: () => void;
|
||||||
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 locObj = data.store_location as Record<string, unknown> | null;
|
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
||||||
const lat = locObj?.latitude || data.latitude;
|
const [loading, setLoading] = useState(true);
|
||||||
const lng = locObj?.longitude || data.longitude;
|
const [error, setError] = useState<string | null>(null);
|
||||||
const hasLocation = lat && lng && lat !== '—' && lng !== '—';
|
|
||||||
|
useEffect(() => {
|
||||||
return (
|
let live = true;
|
||||||
<div className="w-full space-y-6">
|
async function run() {
|
||||||
<StoreCard row={data} isDetailView={true} />
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
{hasLocation && (
|
try {
|
||||||
<a
|
const r = await storeClient.detailView(STORE.detailViews.STORE, instanceId);
|
||||||
href={`https://www.google.com/maps/search/?api=1&query=${lat},${lng}`}
|
if (live) setData(r.data || {});
|
||||||
target="_blank"
|
} catch (e) {
|
||||||
rel="noopener noreferrer"
|
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
||||||
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"
|
} finally {
|
||||||
>
|
if (live) setLoading(false);
|
||||||
<MapPin size={18} />
|
}
|
||||||
<span>View on Google Maps</span>
|
}
|
||||||
</a>
|
run();
|
||||||
)}
|
return () => { live = false; };
|
||||||
</div>
|
}, [instanceId]);
|
||||||
);
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card title="Store Details">
|
||||||
|
<EmptyState title="Couldn’t 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="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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,3 +5,7 @@ export type { WiredDetailViewProps } from './OrderDetail';
|
|||||||
export { CallDetail } from './CallDetail';
|
export { CallDetail } from './CallDetail';
|
||||||
export { StoreDetail } from './StoreDetail';
|
export { StoreDetail } from './StoreDetail';
|
||||||
export { DailyLogDetail } from './DailyLogDetail';
|
export { DailyLogDetail } from './DailyLogDetail';
|
||||||
|
export { PotentialMiningTable } from './PotentialMiningTable';
|
||||||
|
export { GridTable } from './GridTable';
|
||||||
|
export type { GridTableProps } from './GridTable';
|
||||||
|
export type { MiningItem, PotentialMiningTableProps } from './PotentialMiningTable';
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export function SmartGridField({
|
|||||||
value: Record<string, unknown>[];
|
value: Record<string, unknown>[];
|
||||||
onChange: (val: Record<string, unknown>[]) => void;
|
onChange: (val: Record<string, unknown>[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||||
const formRef = useRef<HTMLDivElement>(null);
|
const formRef = useRef<HTMLDivElement>(null);
|
||||||
const [newRow, setNewRow] = useState<Record<string, unknown>>({});
|
const [newRow, setNewRow] = useState<Record<string, unknown>>({});
|
||||||
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||||
@ -47,15 +47,6 @@ export function SmartGridField({
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startAdd = () => {
|
|
||||||
setEditingIdx(null);
|
|
||||||
setNewRow({});
|
|
||||||
setIsModalOpen(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}, 150);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateNewRowField = (fieldId: string, val: unknown) => {
|
const updateNewRowField = (fieldId: string, val: unknown) => {
|
||||||
let row = { ...newRow, [fieldId]: val };
|
let row = { ...newRow, [fieldId]: val };
|
||||||
setErrors(prev => ({ ...prev, [fieldId]: '' }));
|
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 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 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) {
|
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 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 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';
|
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 (
|
return (
|
||||||
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 shadow-sm flex items-center justify-between">
|
<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">
|
<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>
|
<span className="font-bold text-slate-800 text-[14px] truncate">{productName}</span>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{(!isPotentialMining || hasBagsCol) && (
|
{(!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 && (
|
{isPotentialMining && diffVal !== null && !isNaN(diffVal) && diffVal !== 0 && (
|
||||||
@ -261,18 +280,7 @@ export function SmartGridField({
|
|||||||
</div>
|
</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
|
<div
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { Modal } from '../components/reusable';
|
|||||||
import { CallsView } from '../components/rv';
|
import { CallsView } from '../components/rv';
|
||||||
import { CallDetail } from '../components/dv';
|
import { CallDetail } from '../components/dv';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '../components/buttons/Button';
|
|
||||||
import { Phone, Plus } from 'lucide-react';
|
import { Phone, Plus } from 'lucide-react';
|
||||||
|
import { Button } from '../components/buttons/Button';
|
||||||
import { DynamicForm } from '../components/forms/DynamicForm';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { orderBookingClient } from '../api/clients';
|
import { orderBookingClient } from '../api/clients';
|
||||||
@ -67,6 +67,93 @@ export function CallsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (instanceId != null) {
|
||||||
|
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">
|
||||||
|
<CallDetail instanceId={instanceId} onDataLoad={setSelectedRow} onBack={() => { navigate(`/calls`); setIsFabOpen(false); }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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}>
|
||||||
|
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
||||||
|
</Button>
|
||||||
|
{(() => {
|
||||||
|
const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase();
|
||||||
|
if (stateName.includes('ordered') || stateName === 'ordered') {
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' });
|
||||||
|
setIsFabOpen(false);
|
||||||
|
}}>
|
||||||
|
Edit Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else if (stateName.includes('productive')) {
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
||||||
|
setIsFabOpen(false);
|
||||||
|
}}>
|
||||||
|
Place Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
||||||
|
setIsFabOpen(false);
|
||||||
|
}}>
|
||||||
|
Place Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button size='fab'
|
||||||
|
className="rounded-full shadow-xl flex items-center justify-center !p-0"
|
||||||
|
onClick={() => setIsFabOpen(!isFabOpen)}
|
||||||
|
>
|
||||||
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<CallsView
|
<CallsView
|
||||||
@ -104,72 +191,6 @@ export function CallsPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</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">
|
|
||||||
{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}>
|
|
||||||
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
|
||||||
</Button>
|
|
||||||
{(() => {
|
|
||||||
const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase();
|
|
||||||
if (stateName.includes('ordered') || stateName === 'ordered') {
|
|
||||||
return (
|
|
||||||
<Button size="sm" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' });
|
|
||||||
setIsFabOpen(false);
|
|
||||||
}}>
|
|
||||||
Edit Order
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else if (stateName.includes('productive')) {
|
|
||||||
return (
|
|
||||||
<Button size="sm" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
|
||||||
setIsFabOpen(false);
|
|
||||||
}}>
|
|
||||||
Place Order
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button size="sm" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
|
||||||
setIsFabOpen(false);
|
|
||||||
}}>
|
|
||||||
Place Order
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button size='fab'
|
|
||||||
className="rounded-full shadow-xl flex items-center justify-center !p-0"
|
|
||||||
onClick={() => setIsFabOpen(!isFabOpen)}
|
|
||||||
>
|
|
||||||
<Plus size={24} className={`transition-transform duration-200 ${isFabOpen ? "rotate-45" : ""}`} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={activeActivity != null}
|
open={activeActivity != null}
|
||||||
onClose={() => setActiveActivity(null)}
|
onClose={() => setActiveActivity(null)}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export function ConsoleLayout() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
|
<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 />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { OrdersView } from '../components/rv';
|
|||||||
import { OrderDetail } from '../components/dv';
|
import { OrderDetail } from '../components/dv';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ShoppingBag, Plus } from 'lucide-react';
|
import { Plus, ShoppingBag } from 'lucide-react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import { DynamicForm } from '../components/forms/DynamicForm';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
@ -69,6 +69,93 @@ export function OrdersPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (instanceId != null) {
|
||||||
|
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">
|
||||||
|
<OrderDetail instanceId={instanceId} onDataLoad={setSelectedRow} onBack={() => { navigate(`/orders`); setIsFabOpen(false); }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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}>
|
||||||
|
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
||||||
|
</Button>
|
||||||
|
{(() => {
|
||||||
|
const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase();
|
||||||
|
if (stateName.includes('ordered') || stateName === 'ordered') {
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' });
|
||||||
|
setIsFabOpen(false);
|
||||||
|
}}>
|
||||||
|
Edit Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else if (stateName.includes('productive')) {
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
||||||
|
setIsFabOpen(false);
|
||||||
|
}}>
|
||||||
|
Place Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
||||||
|
setIsFabOpen(false);
|
||||||
|
}}>
|
||||||
|
Place Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button size='fab'
|
||||||
|
className="rounded-full shadow-xl flex items-center justify-center !p-0"
|
||||||
|
onClick={() => setIsFabOpen(!isFabOpen)}
|
||||||
|
>
|
||||||
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<OrdersView
|
<OrdersView
|
||||||
@ -80,7 +167,6 @@ export function OrdersPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Global Floating Action Button for Place Order */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => { setIsCreating(true); setCreateTitle("Place Order"); }}
|
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"
|
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"
|
||||||
@ -106,72 +192,6 @@ export function OrdersPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</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">
|
|
||||||
{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}>
|
|
||||||
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
|
||||||
</Button>
|
|
||||||
{(() => {
|
|
||||||
const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase();
|
|
||||||
if (stateName.includes('ordered') || stateName === 'ordered') {
|
|
||||||
return (
|
|
||||||
<Button size="sm" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' });
|
|
||||||
setIsFabOpen(false);
|
|
||||||
}}>
|
|
||||||
Edit Order
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else if (stateName.includes('productive')) {
|
|
||||||
return (
|
|
||||||
<Button size="sm" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
|
||||||
setIsFabOpen(false);
|
|
||||||
}}>
|
|
||||||
Place Order
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else if (stateName.includes('no order') || stateName.includes('non-productive') || stateName.includes('non productive')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button size="sm" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' });
|
|
||||||
setIsFabOpen(false);
|
|
||||||
}}>
|
|
||||||
Place Order
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button size='fab'
|
|
||||||
className="rounded-full shadow-xl flex items-center justify-center !p-0"
|
|
||||||
onClick={() => setIsFabOpen(!isFabOpen)}
|
|
||||||
>
|
|
||||||
<Plus size={24} className={`transition-transform duration-200 ${isFabOpen ? "rotate-45" : ""}`} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={activeActivity != null}
|
open={activeActivity != null}
|
||||||
onClose={() => setActiveActivity(null)}
|
onClose={() => setActiveActivity(null)}
|
||||||
|
|||||||
@ -19,6 +19,38 @@ export function StoresPage() {
|
|||||||
const [editTitle, setEditTitle] = useState("Edit Store");
|
const [editTitle, setEditTitle] = useState("Edit Store");
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<StoresView
|
<StoresView
|
||||||
@ -79,15 +111,6 @@ export function StoresPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={instanceId != null && !isCreating}
|
|
||||||
onClose={() => navigate(`/stores`)}
|
|
||||||
title={instanceId != null ? `Store #${instanceId}` : undefined}
|
|
||||||
width="lg"
|
|
||||||
>
|
|
||||||
{instanceId != null && <StoreDetail instanceId={instanceId} />}
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user