320 lines
13 KiB
TypeScript
320 lines
13 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { storeClient } from '../../api/clients';
|
||
import { STORE, APP_ID } from '../../api/config';
|
||
import { Card } from '../reusable/Card';
|
||
import { Spinner } from '../reusable/Spinner';
|
||
import { EmptyState } from '../reusable/EmptyState';
|
||
import { formatValue } from '../../lib/format';
|
||
import { Store, User, Truck, MapPin, TrendingUp, Camera, ClipboardList } from 'lucide-react';
|
||
import { GridTable } from './GridTable';
|
||
import type { WiredDetailViewProps } from './OrderDetail';
|
||
|
||
export function StoreDetail({ instanceId }: WiredDetailViewProps) {
|
||
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
let live = true;
|
||
async function run() {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const r = await storeClient.detailView(STORE.detailViews.STORE, instanceId);
|
||
if (live) setData(r.data || {});
|
||
} catch (e) {
|
||
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
||
} finally {
|
||
if (live) setLoading(false);
|
||
}
|
||
}
|
||
run();
|
||
return () => { live = false; };
|
||
}, [instanceId]);
|
||
|
||
if (error) {
|
||
return (
|
||
<Card title="Store Details">
|
||
<EmptyState title="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 '-';
|
||
};
|
||
|
||
// State
|
||
const stateName = extract('current_state_name', remainingData);
|
||
delete remainingData.current_state_id;
|
||
const isSuccess = ['created', 'active', 'approve'].some(s => stateName.toString().toLowerCase().includes(s));
|
||
const colorClass = isSuccess ? 'text-emerald-500' : 'text-blue-500';
|
||
|
||
// 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);
|
||
const notes = extract('notes', remainingData);
|
||
|
||
// Owner Info
|
||
const ownerName = extract('owner_name', remainingData);
|
||
const email = extract('email', remainingData);
|
||
let phoneNumber = extract('phone_number', remainingData);
|
||
if (typeof phoneNumber === 'object' && phoneNumber !== null) {
|
||
phoneNumber = (phoneNumber as any).phone_with_dial_code || (phoneNumber as any).phone || '-';
|
||
}
|
||
|
||
// 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 || '-';
|
||
}
|
||
|
||
// 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);
|
||
|
||
// Image
|
||
const storeImage = extract('store_image', remainingData);
|
||
|
||
// 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 : '-';
|
||
|
||
// Helper Row
|
||
const Row = ({ label, value }: { label: string, value: any }) => {
|
||
let finalValue = value;
|
||
if (finalValue && typeof finalValue === 'object') {
|
||
try { finalValue = JSON.stringify(finalValue); } catch (e) { finalValue = '-'; }
|
||
}
|
||
return (
|
||
<div className="flex flex-col sm:flex-row sm:items-center py-2 border-b border-border-subtle last:border-0">
|
||
<span className="text-sm text-muted sm:w-1/3">{label}:</span>
|
||
<span className="text-sm font-bold text-slate-800 sm:w-2/3">{formatValue(finalValue)}</span>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
|
||
{/* 1. Status Card */}
|
||
<Card pad={false} className="border-t-[4px] border-[var(--z-bg-primary)] shadow-md">
|
||
<div className="p-5 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-slate-50/50">
|
||
<div>
|
||
<div className="text-xs font-bold text-muted uppercase tracking-wider mb-1">Status</div>
|
||
<div className={`text-2xl font-black uppercase tracking-tight ${colorClass}`}>
|
||
{stateName}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 2. Store Overview Card */}
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<Store className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Store Overview</h3>
|
||
</div>
|
||
<div className="p-5 flex flex-col">
|
||
<Row label="Store Code" value={storeCode} />
|
||
<Row label="Business Name" value={businessName} />
|
||
<Row label="Area" value={area} />
|
||
<Row label="Complete Address" value={completeAddress} />
|
||
<Row label="PIN Code" value={pinCode} />
|
||
<Row label="Notes" value={notes} />
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 3. Owner Information Card */}
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<User className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Owner Information</h3>
|
||
</div>
|
||
<div className="p-5 flex flex-col">
|
||
<Row label="Owner Name" value={ownerName} />
|
||
<Row label="Phone Number" value={phoneNumber} />
|
||
<Row label="Email" value={email} />
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 4. Distributor Details Card */}
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<Truck className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Distributor Details</h3>
|
||
</div>
|
||
<div className="p-5 flex flex-col">
|
||
<Row label="Distributor Name" value={distributorName} />
|
||
<Row label="Distributor Owner Name" value={distributorOwnerName} />
|
||
<Row label="Phone Number" value={distributorPhone} />
|
||
<Row label="Email" value={distributorEmail} />
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 5. Route Details Card */}
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<MapPin className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Route Details</h3>
|
||
</div>
|
||
<div className="p-5 flex flex-col">
|
||
<Row label="Route Name" value={routeName} />
|
||
<Row label="Route Code" value={routeCode} />
|
||
<Row label="Sub Route" value={subRoute} />
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 6. Potential Card */}
|
||
{Array.isArray(potential) && potential.length > 0 && (
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<TrendingUp className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Potential</h3>
|
||
</div>
|
||
<div className="p-0 sm:p-5">
|
||
<GridTable
|
||
data={potential}
|
||
columns={[
|
||
{ id: 'product_category', uid: '', name: 'Product Category', data_type: 'text' },
|
||
{ id: 'quantity', uid: '', name: 'Quantity (Kgs)', data_type: 'number' }
|
||
]}
|
||
/>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 7. Store Image Card */}
|
||
{Array.isArray(storeImage) && storeImage.length > 0 && (
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<Camera className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Store Image</h3>
|
||
</div>
|
||
<div className="p-5">
|
||
<div className="flex flex-wrap gap-4">
|
||
{storeImage.map((file: any, idx: number) => {
|
||
const previewUrl = `${storeClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`;
|
||
return (
|
||
<div key={file.uuid || idx} className="relative w-full rounded-lg border border-border-subtle overflow-hidden bg-slate-100 flex items-center justify-center group shadow-sm">
|
||
<img
|
||
src={previewUrl}
|
||
alt={file.original_name || 'Store'}
|
||
className="w-full h-auto max-h-[400px] object-contain transition-transform group-hover:scale-[1.02]"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
(e.target as HTMLImageElement).parentElement!.innerHTML = `<span class="text-[10px] text-faint text-center px-2 break-all font-mono">${file.original_name || 'File'}</span>`;
|
||
}}
|
||
/>
|
||
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="absolute inset-0 z-10"></a>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 7.5 Store Location Map */}
|
||
{lat && lng && (
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<MapPin className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Store Location</h3>
|
||
</div>
|
||
<div className="p-0">
|
||
<iframe
|
||
title="Store Location Map"
|
||
width="100%"
|
||
height="350"
|
||
style={{ border: 0 }}
|
||
loading="lazy"
|
||
allowFullScreen
|
||
src={`https://maps.google.com/maps?q=${lat},${lng}&hl=en&z=15&output=embed`}
|
||
></iframe>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 8. Log Visit Details Fallback & Meta */}
|
||
{(Object.keys(remainingData).filter(k => !k.includes('uuid') && !/^\d+$/.test(k)).length > 0 || userName !== '-') && (
|
||
<Card pad={false} className="shadow-md">
|
||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||
<ClipboardList className="text-[var(--z-bg-primary)]" size={18} />
|
||
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Other Details</h3>
|
||
</div>
|
||
<div className="p-5 flex flex-col">
|
||
<Row label="Created By" value={userName} />
|
||
<Row label="Created At" value={createdAt} />
|
||
{Object.entries(remainingData).map(([key, value]) => {
|
||
const lowerKey = key.toLowerCase();
|
||
if (lowerKey.includes('uuid') || /^\d+$/.test(key) || lowerKey === 'instance_id') return null;
|
||
|
||
const label = key.replace(/_/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase())
|
||
.replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i, '')
|
||
.replace(/^\s+|\s+$/g, '');
|
||
|
||
if (!label) return null;
|
||
|
||
return <Row key={key} label={label} value={value} />;
|
||
})}
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
</div>
|
||
);
|
||
}
|