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

202 lines
8.1 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 { cn } from '../../lib/cn';
import { formatValue } from '../../lib/format';
import type { ZinoClient } from '../../api/client';
import { APP_ID } from '../../api/config';
import { Card } from '../reusable/Card';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
export interface DetailViewProps {
/** Workflow-bound client (see api/clients.ts). */
client: ZinoClient;
/** detailview template uid. */
dvUid?: string;
/** Instance to render. */
instanceId: number | string;
/** Card header title. */
title?: string;
/** Restrict/order visible fields by field_key. Defaults to all. */
fields?: string[];
/** Columns in the definition grid. @default 2 */
columns?: 1 | 2 | 3;
}
/**
* Generic Zino detail view. Fetches `GET /app/{id}/view/detailview/{dvUid}` and
* renders the data as a labeled definition grid.
*/
export function DetailView({ client, dvUid, instanceId, title, fields, columns = 2 }: DetailViewProps) {
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 {
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 || {});
}
} 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]);
const gridCols = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-3' }[columns];
if (error) {
return (
<Card title={title}>
<EmptyState title="Couldnt load record" hint={error} />
</Card>
);
}
if (loading) {
return (
<Card title={title}>
<div className="py-8 flex justify-center">
<Spinner label="Loading…" />
</div>
</Card>
);
}
if (!data || Object.keys(data).length === 0) {
return (
<Card title={title}>
<EmptyState title="No details found" />
</Card>
);
}
return (
<div className="flex flex-col gap-4">
<Card
title={title || 'Details'}
className="border-t-[4px] border-[var(--z-bg-primary)] shadow-md"
bodyClassName="bg-slate-50/30"
>
<dl className={cn('grid gap-x-6 gap-y-6', gridCols)}>
{Object.entries(data).map(([key, value]) => {
if (fields && !fields.includes(key)) return null;
const lowerKey = key.toLowerCase();
if (lowerKey.includes('uuid') || /^\d+$/.test(key)) return null;
const isGridArray =
Array.isArray(value) &&
value.length > 0 &&
typeof value[0] === 'object' &&
value[0] !== null &&
!('original_name' in value[0]) &&
!('url' in value[0]);
if (isGridArray) {
const rows = value as Record<string, unknown>[];
// Use all unique keys found across all rows just in case they differ slightly
const headers = Array.from(new Set(rows.flatMap(r => Object.keys(r))));
return (
<div key={key} className="flex flex-col gap-2 min-w-0 col-span-full mt-2 mb-4">
<dt className="text-[11px] font-bold uppercase tracking-[0.08em] text-[var(--z-bg-primary)] opacity-80">
{key.replace(/_/g, ' ')}
</dt>
<dd className="m-0 text-sm text-strong font-medium overflow-x-auto rounded border border-border-subtle shadow-sm">
<table className="min-w-full divide-y divide-border-subtle text-left bg-[var(--tiles-card-bg)]">
<thead className="bg-slate-50">
<tr>
{headers.map(h => (
<th key={h} className="px-4 py-2 text-[10px] font-bold uppercase text-muted tracking-wide whitespace-nowrap">
{h.replace(/_/g, ' ')}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border-subtle">
{rows.map((row, idx) => (
<tr key={idx} className="hover:bg-slate-50/50">
{headers.map(h => (
<td key={h} className="px-4 py-2 whitespace-nowrap text-sm text-strong">
{formatValue(row[h])}
</td>
))}
</tr>
))}
</tbody>
</table>
</dd>
</div>
);
}
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-[11px] font-bold uppercase tracking-[0.08em] text-[var(--z-bg-primary)] opacity-80">
{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 (
<div key={key} className="flex flex-col gap-1.5 min-w-0">
<dt className="text-[11px] font-bold uppercase tracking-[0.08em] text-[var(--z-bg-primary)] opacity-80">
{key.replace(/_/g, ' ')}
</dt>
<dd className="m-0 text-sm text-strong font-medium break-words">
{formatValue(value)}
</dd>
</div>
);
})}
</dl>
</Card>
</div>
);
}