krishna_sales/src/components/dv/StoreDetail.tsx
2026-07-24 19:11:39 +05:30

467 lines
25 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 { 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="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 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>
);
}