467 lines
25 KiB
TypeScript
467 lines
25 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 { ArrowLeft, Store, User, Truck, MapPin, TrendingUp, Phone, Navigation2, Edit3, Navigation, Mail, FileText, Hash } from 'lucide-react';
|
||
import { Button } from '../buttons/Button';
|
||
|
||
export interface StoreDetailProps {
|
||
instanceId: string | number;
|
||
onBack?: () => void;
|
||
onEdit?: () => void;
|
||
}
|
||
|
||
export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) {
|
||
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
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 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)]">
|
||
<table className="w-full text-sm text-left">
|
||
<thead className="bg-slate-50 border-b border-slate-100">
|
||
<tr>
|
||
<th className="py-3 px-5 font-bold text-slate-500 text-xs uppercase tracking-wider">Product Category</th>
|
||
<th className="py-3 px-5 font-bold text-slate-500 text-xs uppercase tracking-wider text-right">Quantity</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{potential.map((p: any, idx: number) => {
|
||
if (!p.product_category && !p.quantity) return null;
|
||
return (
|
||
<tr key={idx} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/50">
|
||
<td className="py-3 px-5 font-semibold text-slate-900">{p.product_category ? String(p.product_category) : '-'}</td>
|
||
<td className="py-3 px-5 text-slate-700 text-right">{p.quantity ? String(p.quantity) : '0'}</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
<tr className="bg-slate-50 border-t border-slate-200 font-bold">
|
||
<td className="py-3 px-5 text-slate-900">Total</td>
|
||
<td className="py-3 px-5 text-slate-900 text-right">{totalPotential}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</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>
|
||
);
|
||
}
|