Compare commits
2 Commits
f35eb87af6
...
408e0c2888
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
408e0c2888 | ||
|
|
5273ff589c |
@ -1,10 +1,20 @@
|
|||||||
import { formatValue } from '../../lib/format';
|
import { formatValue } from '../../lib/format';
|
||||||
|
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
|
||||||
const stateName = String(row.status || row.current_state_name || 'Pending');
|
const stateName = String(row.status || row.current_state_name || 'Pending');
|
||||||
const isProductive = stateName.toLowerCase().includes('productive') || stateName.toLowerCase().includes('completed') || stateName.toLowerCase().includes('success');
|
const isProductive = stateName.toLowerCase().includes('productive') || stateName.toLowerCase().includes('completed') || stateName.toLowerCase().includes('success');
|
||||||
|
|
||||||
|
// Image
|
||||||
|
let imageObj = null;
|
||||||
|
const imgData = row.upload_image || row.store_image || row.image;
|
||||||
|
if (Array.isArray(imgData) && imgData.length > 0) {
|
||||||
|
imageObj = imgData[0];
|
||||||
|
} else if (imgData && typeof imgData === 'object' && imgData.blob_path) {
|
||||||
|
imageObj = imgData;
|
||||||
|
}
|
||||||
|
|
||||||
// Store Details
|
// Store Details
|
||||||
const storeName = formatValue(row.store || row.store_name || row.customer_name || 'Unknown Store');
|
const storeName = formatValue(row.store || row.store_name || row.customer_name || 'Unknown Store');
|
||||||
const storeCode = formatValue(row.store_code || '—');
|
const storeCode = formatValue(row.store_code || '—');
|
||||||
@ -68,6 +78,7 @@ export function CallCard({ row }: { row: Record<string, any> }) {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="p-5 space-y-5">
|
<div className="p-5 space-y-5">
|
||||||
{/* Store Details */}
|
{/* Store Details */}
|
||||||
|
<div className="flex justify-between items-start gap-4">
|
||||||
<div>
|
<div>
|
||||||
<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">{storeName}</h2>
|
<h2 className="text-[17px] font-bold text-slate-800 mt-0.5 leading-tight">{storeName}</h2>
|
||||||
@ -78,6 +89,20 @@ export function CallCard({ row }: { row: Record<string, any> }) {
|
|||||||
)}
|
)}
|
||||||
</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>
|
||||||
|
|
||||||
{/* Logistics Grid */}
|
{/* Logistics Grid */}
|
||||||
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-xl border border-slate-100/80">
|
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-xl border border-slate-100/80">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { cn } from '../../lib/cn';
|
|||||||
import { formatValue } from '../../lib/format';
|
import { formatValue } from '../../lib/format';
|
||||||
import type { ZinoClient } from '../../api/client';
|
import type { ZinoClient } from '../../api/client';
|
||||||
import type { AuditEntry } from '../../api/types';
|
import type { AuditEntry } from '../../api/types';
|
||||||
import { WORKFLOWS } from '../../api/config';
|
import { WORKFLOWS, APP_ID } from '../../api/config';
|
||||||
import { Card } from '../reusable/Card';
|
import { Card } from '../reusable/Card';
|
||||||
import { Spinner } from '../reusable/Spinner';
|
import { Spinner } from '../reusable/Spinner';
|
||||||
import { EmptyState } from '../reusable/EmptyState';
|
import { EmptyState } from '../reusable/EmptyState';
|
||||||
@ -56,7 +56,9 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const r = await client.audit(instanceId);
|
const r = await client.audit(instanceId);
|
||||||
if (live) setEntries(r);
|
if (live) {
|
||||||
|
setEntries(r || []);
|
||||||
|
}
|
||||||
} 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');
|
||||||
} finally {
|
} finally {
|
||||||
@ -158,6 +160,47 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFileArray =
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.length > 0 &&
|
||||||
|
typeof value[0] === 'object' &&
|
||||||
|
value[0] !== null &&
|
||||||
|
('original_name' in value[0] || 'uuid' in value[0]);
|
||||||
|
|
||||||
|
if (isFileArray) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="flex flex-col gap-2 min-w-0 col-span-full mt-2 mb-4">
|
||||||
|
<dt className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint">
|
||||||
|
{key.replace(/_/g, ' ')}
|
||||||
|
</dt>
|
||||||
|
<dd className="m-0 flex flex-wrap gap-4">
|
||||||
|
{(value as any[]).map((file, idx) => {
|
||||||
|
const previewUrl = `${client.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`;
|
||||||
|
return (
|
||||||
|
<div key={file.uuid || idx} className="relative w-32 h-32 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 || 'Attached File'}
|
||||||
|
className="w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-1 min-w-0">
|
<div key={key} className="flex flex-col gap-1 min-w-0">
|
||||||
<dt className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint">
|
<dt className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint">
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/components/dv/useDetailViewData.ts
Normal file
35
src/components/dv/useDetailViewData.ts
Normal 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 };
|
||||||
|
}
|
||||||
@ -160,12 +160,12 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
} else if ((f.data_type === 'image' || f.data_type === 'file') && Array.isArray(val) && val.length > 0 && val[0] instanceof File) {
|
} else if ((f.data_type === 'image' || f.data_type === 'file') && Array.isArray(val) && val.length > 0 && val[0] instanceof File) {
|
||||||
const uploadedFiles = [];
|
const uploadedFiles = [];
|
||||||
for (const file of val) {
|
for (const file of val) {
|
||||||
const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id });
|
const fileMeta = await client.uploadFile(file, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
|
||||||
uploadedFiles.push(fileMeta);
|
uploadedFiles.push(fileMeta);
|
||||||
}
|
}
|
||||||
payload[f.id] = uploadedFiles;
|
payload[f.id] = uploadedFiles;
|
||||||
} else if ((f.data_type === 'image' || f.data_type === 'file') && val instanceof File) {
|
} else if ((f.data_type === 'image' || f.data_type === 'file') && val instanceof File) {
|
||||||
const fileMeta = await client.uploadFile(val, { activityId: currentActivityId, fieldId: f.id });
|
const fileMeta = await client.uploadFile(val, { activityId: currentActivityId, fieldId: f.id, instanceId: currentInstanceId });
|
||||||
payload[f.id] = [fileMeta];
|
payload[f.id] = [fileMeta];
|
||||||
} else {
|
} else {
|
||||||
payload[f.id] = val;
|
payload[f.id] = val;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user