krishna_sales/src/components/dv/StoreDetail.tsx
2026-07-21 12:10:23 +05:30

320 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="Couldnt load record" hint={error} />
</Card>
);
}
if (loading) {
return (
<Card title="Store Details">
<div className="py-8 flex justify-center"><Spinner label="Loading details…" /></div>
</Card>
);
}
if (!data || Object.keys(data).length === 0) {
return (
<Card title="Store Details">
<EmptyState title="No details found" />
</Card>
);
}
const remainingData = { ...data };
const extract = (key: string, obj: any = data) => {
if (obj && key in obj) {
const val = obj[key];
delete obj[key];
return val;
}
return '-';
};
// 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>
);
}