basic detail view polishing done

This commit is contained in:
suryacp23 2026-07-14 19:16:44 +05:30
parent 5273ff589c
commit 408e0c2888
9 changed files with 198 additions and 92 deletions

View File

@ -1,5 +1,5 @@
import { formatValue } from '../../lib/format'; import { formatValue } from '../../lib/format';
import { BASE_URL } from '../../api/config'; import { BASE_URL, APP_ID } from '../../api/config';
export function CallCard({ row }: { row: Record<string, any> }) { export function CallCard({ row }: { row: Record<string, any> }) {
// Status // Status
@ -89,10 +89,10 @@ export function CallCard({ row }: { row: Record<string, any> }) {
)} )}
</div> </div>
{imageObj && imageObj.blob_path && ( {imageObj && imageObj.uuid && (
<div className="w-16 h-16 shrink-0 rounded-lg overflow-hidden border border-slate-200 shadow-sm bg-slate-100 flex items-center justify-center"> <div className="w-16 h-16 shrink-0 rounded-lg overflow-hidden border border-slate-200 shadow-sm bg-slate-100 flex items-center justify-center">
<img <img
src={`${BASE_URL}/${imageObj.blob_path}`} src={`${BASE_URL}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`}
alt="Store" alt="Store"
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => { onError={(e) => {

View File

@ -1,5 +1,6 @@
import { formatValue } from '../../lib/format'; import { formatValue } from '../../lib/format';
import { FileImage } from 'lucide-react'; import { FileImage } from 'lucide-react';
import { BASE_URL, APP_ID } from '../../api/config';
export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; onPunchOut?: (row: any) => void }) { export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; onPunchOut?: (row: any) => void }) {
const stateName = String(row.status || row.current_state_name || 'Logged'); const stateName = String(row.status || row.current_state_name || 'Logged');
@ -105,6 +106,20 @@ export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; on
{hasImage && ( {hasImage && (
<div className="space-y-2"> <div className="space-y-2">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Verification Image</span> <span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Verification Image</span>
{imgData.uuid ? (
<div className="w-full h-48 rounded-xl overflow-hidden border border-slate-200 shadow-sm bg-slate-100 relative group">
<img
src={`${BASE_URL}/app/${APP_ID}/view/files/${imgData.uuid}/preview`}
alt={imgName}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).parentElement!.innerHTML = `<div class="flex items-center justify-center h-full text-xs text-slate-400 break-all px-4 text-center">${imgName}</div>`;
}}
/>
<a href={`${BASE_URL}/app/${APP_ID}/view/files/${imgData.uuid}/preview`} target="_blank" rel="noopener noreferrer" className="absolute inset-0 z-10"></a>
</div>
) : (
<div className="flex items-center justify-between p-3 rounded-xl border border-slate-200 bg-white shadow-sm"> <div className="flex items-center justify-between p-3 rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="text-indigo-500 bg-indigo-50 p-1.5 rounded-lg border border-indigo-100/50"> <div className="text-indigo-500 bg-indigo-50 p-1.5 rounded-lg border border-indigo-100/50">
@ -119,6 +134,7 @@ export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; on
MIME: {imgMime.split('/')[1] || 'Media'} MIME: {imgMime.split('/')[1] || 'Media'}
</span> </span>
</div> </div>
)}
</div> </div>
)} )}

View File

@ -1,17 +1,29 @@
import { formatValue } from '../../lib/format'; import { formatValue } from '../../lib/format';
import { Edit2 } from 'lucide-react'; import { Edit2 } from 'lucide-react';
import { BASE_URL, APP_ID } from '../../api/config';
export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; fields: any[]; onEdit?: (row: any) => void }) { export function StoreCard({ row, fields, isDetailView = false, onEdit }: { row: Record<string, any>; fields: any[]; isDetailView?: boolean; onEdit?: (row: any) => void }) {
// Status // Status
const stateName = String(row.current_state_name || row.status || 'Active'); const stateName = String(row.current_state_name || row.status || 'Active');
const isActive = stateName.toLowerCase().includes('active') || stateName.toLowerCase().includes('created') || stateName.toLowerCase().includes('approved'); const isActive = stateName.toLowerCase().includes('active') || stateName.toLowerCase().includes('created') || stateName.toLowerCase().includes('approved');
// Image
let imageObj = null;
const imgData = row.store_image || row.image || row.upload_image || row.store_photo;
if (Array.isArray(imgData) && imgData.length > 0) {
imageObj = imgData[0];
} else if (imgData && typeof imgData === 'object' && imgData.uuid) {
imageObj = imgData;
}
// Store Details // Store Details
const storeName = formatValue(row.business_name || row.store_name || row.name || 'Unknown Store'); const storeName = formatValue(row.business_name || row.store_name || row.name || 'Unknown Store');
const storeCode = formatValue(row.store_code || row.code || '—'); const storeCode = formatValue(row.store_code || row.code || '—');
const ownerName = formatValue(row.owner_name || row.contact_person || '—'); const ownerName = formatValue(row.owner_name || row.contact_person || '—');
const phoneObj = row.phone_number as Record<string, unknown> | null; const phoneObj = row.phone_number as Record<string, unknown> | null;
const phone = formatValue(phoneObj?.phone_with_dial_code || phoneObj?.phone || row.phone || row.mobile || '—'); const phone = formatValue(phoneObj?.phone_with_dial_code || phoneObj?.phone || row.phone || row.mobile || '—');
const emailObj = row.email_address as Record<string, unknown> | null;
const email = formatValue(emailObj?.email || row.email || row.store_email || row.business_email || '—');
// Route Info // Route Info
const routeName = formatValue(row.route_name || row.route || '—'); const routeName = formatValue(row.route_name || row.route || '—');
@ -23,6 +35,8 @@ export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; f
const distContact = formatValue(row.distributor_owner_name || '—'); const distContact = formatValue(row.distributor_owner_name || '—');
const distPhoneObj = row.distributor_phone_number as Record<string, unknown> | null; const distPhoneObj = row.distributor_phone_number as Record<string, unknown> | null;
const distPhone = formatValue(distPhoneObj?.phone_with_dial_code || distPhoneObj?.phone || row.distributor_phone || '—'); const distPhone = formatValue(distPhoneObj?.phone_with_dial_code || distPhoneObj?.phone || row.distributor_phone || '—');
const distEmailObj = row.distributor_email_address as Record<string, unknown> | null;
const distEmail = formatValue(distEmailObj?.email || row.distributor_email || '—');
// Address // Address
const address = formatValue(row.complete_address || row.address || '—'); const address = formatValue(row.complete_address || row.address || '—');
@ -93,7 +107,8 @@ export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; f
{/* Main Content */} {/* Main Content */}
<div className="p-5 space-y-5"> <div className="p-5 space-y-5">
{/* Store Identity */} {/* Store Identity */}
<div> <div className="flex justify-between items-start gap-4">
<div className="flex-1 min-w-0">
<span className="text-[10px] font-bold text-indigo-600 tracking-wider uppercase">Store Details</span> <span className="text-[10px] font-bold text-indigo-600 tracking-wider uppercase">Store Details</span>
<h2 className="text-[17px] font-bold text-slate-800 mt-0.5 leading-tight break-words w-full">{storeName}</h2> <h2 className="text-[17px] font-bold text-slate-800 mt-0.5 leading-tight break-words w-full">{storeName}</h2>
@ -106,9 +121,23 @@ export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; f
</span> </span>
</div> </div>
)} )}
</div>
{imageObj && imageObj.uuid && (
<div className="w-16 h-16 shrink-0 rounded-lg overflow-hidden border border-slate-200 shadow-sm bg-slate-100 flex items-center justify-center">
<img
src={`${BASE_URL}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`}
alt="Store"
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
</div>
{/* Contact Info */} {/* Contact Info */}
{(ownerName !== '—' || phone !== '—') && ( {(ownerName !== '—' || phone !== '—' || email !== '—') && (
<div className="mt-3 space-y-1.5 text-[13px] text-slate-600"> <div className="mt-3 space-y-1.5 text-[13px] text-slate-600">
{ownerName !== '—' && ( {ownerName !== '—' && (
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
@ -122,24 +151,30 @@ export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; f
<span className="font-mono text-slate-700">{phone}</span> <span className="font-mono text-slate-700">{phone}</span>
</p> </p>
)} )}
</div> {email !== '—' && (
<p className="flex items-center gap-2">
<span className="font-bold text-slate-400 text-[10px] w-12 uppercase tracking-wider">Email</span>
<span className="font-mono text-slate-700 truncate">{email}</span>
</p>
)} )}
</div> </div>
)}
{/* Fulfillment / Distributor */} {/* Fulfillment / Distributor */}
{distName !== '—' && ( {isDetailView && distName !== '—' && (
<div className="bg-slate-50 p-3.5 rounded-xl border border-slate-100 space-y-2"> <div className="bg-slate-50 p-3.5 rounded-xl border border-slate-100 space-y-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Assigned Distributor</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Assigned Distributor</p>
<div> <div>
<h4 className="text-[14px] font-bold text-slate-800 leading-tight">{distName}</h4> <h4 className="text-[14px] font-bold text-slate-800 leading-tight">{distName}</h4>
{distContact !== '—' && <p className="text-[12px] font-medium text-slate-500 mt-0.5">Contact: {distContact}</p>} {distContact !== '—' && <p className="text-[12px] font-medium text-slate-500 mt-0.5">Contact: {distContact}</p>}
{distPhone !== '—' && <p className="text-[12px] font-mono text-slate-600 mt-0.5">{distPhone}</p>} {distPhone !== '—' && <p className="text-[12px] font-mono text-slate-600 mt-0.5">Phone: {distPhone}</p>}
{distEmail !== '—' && <p className="text-[12px] font-mono text-slate-600 mt-0.5">Email: {distEmail}</p>}
</div> </div>
</div> </div>
)} )}
{/* Product Potential Breakdown */} {/* Product Potential Breakdown */}
{potentials.length > 0 && ( {isDetailView && potentials.length > 0 && (
<div> <div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Inventory Potential</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Inventory Potential</p>
<div className="space-y-1.5"> <div className="space-y-1.5">
@ -158,6 +193,7 @@ export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; f
)} )}
{/* Logistics & Address */} {/* Logistics & Address */}
{isDetailView && (
<div className="grid grid-cols-2 gap-4 text-sm border-t border-slate-100 pt-4"> <div className="grid grid-cols-2 gap-4 text-sm border-t border-slate-100 pt-4">
<div className="col-span-2"> <div className="col-span-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Address</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Address</p>
@ -183,6 +219,7 @@ export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; f
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
{/* Footer Timestamp */} {/* Footer Timestamp */}

View File

@ -3,14 +3,13 @@ import { ORDER_BOOKING } from '../../api/config';
import { DetailView } from './DetailView'; import { DetailView } from './DetailView';
import type { WiredDetailViewProps } from './OrderDetail'; import type { WiredDetailViewProps } from './OrderDetail';
/** Call detail view (Order Booking workflow). */ export function CallDetail({ instanceId, columns = 2 }: WiredDetailViewProps) {
export function CallDetail({ instanceId, columns }: WiredDetailViewProps) {
return ( return (
<DetailView <DetailView
client={orderBookingClient} client={orderBookingClient}
dvUid={ORDER_BOOKING.detailViews.CALLS} dvUid={ORDER_BOOKING.detailViews.CALLS}
instanceId={instanceId} instanceId={instanceId}
title="Call" title="Call Details"
columns={columns} columns={columns}
/> />
); );

View File

@ -1,17 +1,21 @@
import { dailyReportsClient } from '../../api/clients'; import { dailyReportsClient } from '../../api/clients';
import { DAILY_REPORTS } from '../../api/config'; import { DAILY_REPORTS } from '../../api/config';
import { DetailView } from './DetailView';
import type { WiredDetailViewProps } from './OrderDetail'; import type { WiredDetailViewProps } from './OrderDetail';
import { useDetailViewData } from './useDetailViewData';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { DailyLogCard } from '../cards/DailyLogCard';
export function DailyLogDetail({ instanceId }: WiredDetailViewProps) {
const { data, loading, error } = useDetailViewData(dailyReportsClient, DAILY_REPORTS.detailViews.DAILY_LOGS, instanceId);
if (error) return <EmptyState title="Couldn't load record" hint={error} />;
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Daily Log..." /></div>;
if (!data) return <EmptyState title="No details found" />;
/** Daily Log detail view (Daily Reports workflow). */
export function DailyLogDetail({ instanceId, columns }: WiredDetailViewProps) {
return ( return (
<DetailView <div className="w-full">
client={dailyReportsClient} <DailyLogCard row={data} />
dvUid={DAILY_REPORTS.detailViews.DAILY_LOGS} </div>
instanceId={instanceId}
title="Daily Log"
columns={columns}
/>
); );
} }

View File

@ -44,7 +44,7 @@ function getActivityName(uid: string): string | undefined {
* Generic Zino detail view. Fetches `GET /app/{id}/view/audit` and * Generic Zino detail view. Fetches `GET /app/{id}/view/audit` and
* renders the audit trail as a labeled definition grid. * renders the audit trail as a labeled definition grid.
*/ */
export function DetailView({ client, dvUid, instanceId, title, columns = 2 }: DetailViewProps) { export function DetailView({ client, instanceId, title, columns = 2 }: DetailViewProps) {
const [entries, setEntries] = useState<AuditEntry[]>([]); const [entries, setEntries] = useState<AuditEntry[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -55,15 +55,9 @@ export function DetailView({ client, dvUid, instanceId, title, columns = 2 }: De
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
if (!dvUid) throw new Error("dvUid is required to fetch detail view data."); const r = await client.audit(instanceId);
const r = await client.detailView(dvUid, instanceId);
if (live) { if (live) {
setEntries([{ setEntries(r || []);
id: Number(instanceId),
activity_id: '',
data: r.data || {},
created_at: (r as any).created_at || new Date().toISOString()
} as AuditEntry]);
} }
} catch (e) { } catch (e) {
if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); if (live) setError((e as { message?: string })?.message ?? 'Failed to load');

View File

@ -3,18 +3,17 @@ import { ORDER_BOOKING } from '../../api/config';
import { DetailView } from './DetailView'; import { DetailView } from './DetailView';
export interface WiredDetailViewProps { export interface WiredDetailViewProps {
instanceId: number | string; instanceId: string | number;
columns?: 1 | 2 | 3; columns?: 1 | 2 | 3;
} }
/** Order detail view (Order Booking workflow). */ export function OrderDetail({ instanceId, columns = 2 }: WiredDetailViewProps) {
export function OrderDetail({ instanceId, columns }: WiredDetailViewProps) {
return ( return (
<DetailView <DetailView
client={orderBookingClient} client={orderBookingClient}
dvUid={ORDER_BOOKING.detailViews.ORDERS} dvUid={ORDER_BOOKING.detailViews.ORDERS}
instanceId={instanceId} instanceId={instanceId}
title="Order" title="Order Details"
columns={columns} columns={columns}
/> />
); );

View File

@ -1,17 +1,39 @@
import { storeClient } from '../../api/clients'; import { storeClient } from '../../api/clients';
import { STORE } from '../../api/config'; import { STORE } from '../../api/config';
import { DetailView } from './DetailView';
import type { WiredDetailViewProps } from './OrderDetail'; import type { WiredDetailViewProps } from './OrderDetail';
import { useDetailViewData } from './useDetailViewData';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { StoreCard } from '../cards/StoreCard';
import { MapPin } from 'lucide-react';
export function StoreDetail({ instanceId }: WiredDetailViewProps) {
const { data, config, loading, error } = useDetailViewData(storeClient, STORE.detailViews.STORE, instanceId);
if (error) return <EmptyState title="Couldn't load record" hint={error} />;
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Store..." /></div>;
if (!data) return <EmptyState title="No details found" />;
const locObj = data.store_location as Record<string, unknown> | null;
const lat = locObj?.latitude || data.latitude;
const lng = locObj?.longitude || data.longitude;
const hasLocation = lat && lng && lat !== '—' && lng !== '—';
/** Store detail view (Store workflow). */
export function StoreDetail({ instanceId, columns }: WiredDetailViewProps) {
return ( return (
<DetailView <div className="w-full space-y-6">
client={storeClient} <StoreCard row={data} fields={config?.fields || []} isDetailView={true} />
dvUid={STORE.detailViews.STORE}
instanceId={instanceId} {hasLocation && (
title="Store" <a
columns={columns} href={`https://www.google.com/maps/search/?api=1&query=${lat},${lng}`}
/> target="_blank"
rel="noopener noreferrer"
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"
>
<MapPin size={18} />
<span>View on Google Maps</span>
</a>
)}
</div>
); );
} }

View File

@ -0,0 +1,35 @@
import { useState, useEffect } from 'react';
import type { ZinoClient } from '../../api/client';
export function useDetailViewData(client: ZinoClient, dvUid: string | undefined, instanceId: string | number) {
const [data, setData] = useState<Record<string, any> | null>(null);
const [config, setConfig] = useState<any>(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 {
if (!dvUid) throw new Error("dvUid is required to fetch detail view data.");
const r = await client.detailView(dvUid, instanceId);
if (live) {
setData(r.data || {});
setConfig(r.config || {});
}
} catch (e) {
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
} finally {
if (live) setLoading(false);
}
}
run();
return () => {
live = false;
};
}, [client, dvUid, instanceId]);
return { data, config, loading, error };
}