fix:cards minimal information

This commit is contained in:
suryacp23 2026-07-15 11:40:46 +05:30
parent 3dce9d655a
commit e391056b82
6 changed files with 163 additions and 48 deletions

View File

@ -1,7 +1,7 @@
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, isDetailView = false }: { row: Record<string, any>; isDetailView?: boolean }) {
// Status
const stateName = String(row.status || row.current_state_name || 'Pending');
const isProductive = stateName.toLowerCase().includes('productive') || stateName.toLowerCase().includes('completed') || stateName.toLowerCase().includes('success');
@ -89,7 +89,7 @@ export function CallCard({ row }: { row: Record<string, any> }) {
)}
</div>
{imageObj && imageObj.uuid && (
{isDetailView && 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`}
@ -104,6 +104,7 @@ export function CallCard({ row }: { row: Record<string, any> }) {
</div>
{/* Logistics Grid */}
{isDetailView && (
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-xl border border-slate-100/80">
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Total Weight</p>
@ -124,8 +125,10 @@ export function CallCard({ row }: { row: Record<string, any> }) {
</span>
</div>
</div>
)}
{/* Route & Assignment */}
{isDetailView && (
<div className="grid grid-cols-2 gap-y-4 text-sm border-t border-slate-100 pt-4">
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Route</p>
@ -150,6 +153,7 @@ export function CallCard({ row }: { row: Record<string, any> }) {
</div>
</div>
</div>
)}
</div>
{/* Footer Timeline */}

View File

@ -2,7 +2,7 @@ import { formatValue } from '../../lib/format';
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, isDetailView = false, onPunchOut }: { row: Record<string, any>; isDetailView?: boolean; onPunchOut?: (row: any) => void }) {
const stateName = String(row.status || row.current_state_name || 'Logged');
// User details
@ -93,6 +93,7 @@ export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; on
</div>
{/* Day Plan & Notes */}
{isDetailView && (
<div className="space-y-1.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Day Plan Summary</span>
<div className="bg-indigo-50/40 border border-indigo-100/40 p-3.5 rounded-xl">
@ -101,9 +102,10 @@ export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; on
</p>
</div>
</div>
)}
{/* Attached Verification Media */}
{hasImage && (
{isDetailView && hasImage && (
<div className="space-y-2">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Verification Image</span>
{imgData.uuid ? (

View File

@ -122,7 +122,7 @@ export function StoreCard({ row, fields, isDetailView = false, onEdit }: { row:
</div>
)}
</div>
{imageObj && imageObj.uuid && (
{isDetailView && 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`}
@ -137,7 +137,7 @@ export function StoreCard({ row, fields, isDetailView = false, onEdit }: { row:
</div>
{/* Contact Info */}
{(ownerName !== '—' || phone !== '—' || email !== '—') && (
{isDetailView && (ownerName !== '—' || phone !== '—' || email !== '—') && (
<div className="mt-3 space-y-1.5 text-[13px] text-slate-600">
{ownerName !== '—' && (
<p className="flex items-center gap-2">

View File

@ -15,7 +15,7 @@ export function DailyLogDetail({ instanceId }: WiredDetailViewProps) {
return (
<div className="w-full">
<DailyLogCard row={data} />
<DailyLogCard row={data} isDetailView={true} />
</div>
);
}

View File

@ -1,7 +1,10 @@
import { useEffect, useState } from 'react';
import { orderBookingClient } from '../../api/clients';
import { ORDER_BOOKING } from '../../api/config';
import { RecordView } from './RecordView';
import { OrderCard } from '../cards/OrderCard';
import { StatsTiles } from '../reusable/StatsTiles';
import type { TileItem } from '../../api/types';
export interface WiredRecordViewProps {
onRowClick?: (row: Record<string, unknown>, index: number) => void;
@ -13,19 +16,45 @@ export interface WiredRecordViewProps {
/** Orders record view (Order Booking workflow). */
export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
const [analyticsTiles, setAnalyticsTiles] = useState<TileItem[]>([]);
useEffect(() => {
let live = true;
// Defer the fetch so it doesn't compete with the main list fetch
const timer = setTimeout(async () => {
try {
const res = await orderBookingClient.recordView(ORDER_BOOKING.recordViews.ANALYTICS_ORDERS, { limit: 0 });
if (live && res.tile_values) {
const filteredTiles = (res.tile_values as TileItem[]).filter(
(t) => t.key !== 'total_orders' && t.key !== 'total_orders_count'
);
setAnalyticsTiles(filteredTiles);
}
} catch (e) {
console.error("Failed to load analytics tiles for orders", e);
}
}, 100);
return () => { live = false; clearTimeout(timer); };
}, [refreshKey]);
return (
<RecordView
client={orderBookingClient}
rvUid={ORDER_BOOKING.recordViews.ORDERS}
title="Orders"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
/>
<div className="flex flex-col gap-5 w-full">
{analyticsTiles.length > 0 && <StatsTiles tiles={analyticsTiles} />}
<RecordView
client={orderBookingClient}
rvUid={ORDER_BOOKING.recordViews.ORDERS}
title="Orders"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
/>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Search } from 'lucide-react';
import { Search, Filter } from 'lucide-react';
import { cn } from '../../lib/cn';
import { formatValue } from '../../lib/format';
import type { ZinoClient } from '../../api/client';
@ -10,6 +10,8 @@ import { EmptyState } from '../reusable/EmptyState';
import { StatsTiles } from '../reusable/StatsTiles';
import { AnalyticsChart } from '../reusable/AnalyticsChart';
import { Select } from '../reusable/Select';
import { Input } from '../reusable/Input';
import { Modal } from '../reusable/Modal';
export interface RecordViewProps {
/** Workflow-bound client (see api/clients.ts). */
@ -64,6 +66,8 @@ export function RecordView({
const [search, setSearch] = useState('');
const [debounced, setDebounced] = useState('');
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
const [pendingFilters, setPendingFilters] = useState<Record<string, string>>({});
const [showFilters, setShowFilters] = useState(false);
const [resp, setResp] = useState<RecordViewResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -143,35 +147,111 @@ export function RecordView({
</div>
<div className="flex flex-col gap-2.5">
<div className="relative focus-ring rounded-pill border border-border-subtle bg-card shadow-sm">
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-full h-11 rounded-pill bg-transparent pl-11 pr-4 font-sans text-sm text-strong outline-none placeholder:text-faint"
/>
<div className="flex items-center gap-2">
<div className="relative flex-1 focus-ring rounded-pill border border-border-subtle bg-card shadow-sm">
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-full h-11 rounded-pill bg-transparent pl-11 pr-4 font-sans text-sm text-strong outline-none placeholder:text-faint"
/>
</div>
{filterEntries.length > 0 && (
<button
onClick={() => {
setPendingFilters(activeFilters);
setShowFilters(true);
}}
className={cn(
"flex items-center justify-center h-11 px-4 rounded-pill border transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-slate-300",
Object.values(activeFilters).some(Boolean)
? "bg-slate-800 text-white border-slate-800"
: "bg-white text-slate-700 border-border-subtle hover:bg-slate-50"
)}
>
<Filter size={16} className={cn("mr-2", Object.values(activeFilters).some(Boolean) ? "text-slate-300" : "text-faint")} />
<span className="font-semibold text-[13px]">Filter</span>
{Object.values(activeFilters).filter(Boolean).length > 0 && (
<span className="ml-1.5 bg-indigo-500 text-white text-[10px] w-4 h-4 flex items-center justify-center rounded-full font-bold">
{Object.values(activeFilters).filter(Boolean).length}
</span>
)}
</button>
)}
</div>
{filterEntries.length > 0 && (
<div className="flex gap-2 overflow-x-auto scrollbar-slim -mx-1 px-1 pb-1">
{filterEntries.map(([key, options]) => {
const fieldDef = resp?.config.fields.find(f => f.field_key === key);
const label = fieldDef?.output_label || key;
const opts = options!.map(o => ({ value: o, label: o }));
return (
<Select
key={key}
value={activeFilters[key] || ''}
onChange={(e) => {
setActiveFilters(prev => ({ ...prev, [key]: e.target.value }));
setPage(1);
}}
options={[{ value: '', label: `All ${label}` }, ...opts]}
className="w-40 shrink-0"
/>
);
})}
</div>
<Modal
open={showFilters}
onClose={() => setShowFilters(false)}
title="Filters"
width="sm"
actions={
Object.values(pendingFilters).some(Boolean) && (
<button
onClick={() => {
setPendingFilters({});
setActiveFilters({});
setPage(1);
setShowFilters(false);
}}
className="text-xs font-semibold text-ruby-600 hover:text-ruby-700 bg-ruby-50 px-3 py-1.5 rounded-md"
>
Clear All
</button>
)
}
>
<div className="flex flex-col gap-5">
{filterEntries.map(([key, options]) => {
const fieldDef = resp?.config.fields.find(f => f.field_key === key);
const label = fieldDef?.output_label || key;
const isDateField = fieldDef?.data_type === 'date' || key.toLowerCase().includes('date');
if (isDateField) {
return (
<Input
key={key}
type="date"
label={label.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())}
value={pendingFilters[key] || ''}
onChange={(e) => {
setPendingFilters(prev => ({ ...prev, [key]: e.target.value }));
}}
/>
);
}
const opts = options!.map(o => ({
value: o,
label: o.replace(/[_\-\s]*\(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\)?/i, '').trim() || o
}));
return (
<Select
key={key}
label={label.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())}
value={pendingFilters[key] || ''}
onChange={(e) => {
setPendingFilters(prev => ({ ...prev, [key]: e.target.value }));
}}
options={[{ value: '', label: 'All' }, ...opts]}
/>
);
})}
<button
onClick={() => {
setActiveFilters(pendingFilters);
setPage(1);
setShowFilters(false);
}}
className="mt-4 w-full h-11 bg-slate-800 text-white font-bold rounded-lg hover:bg-slate-700 transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-800"
>
Apply Filters
</button>
</div>
</Modal>
)}
</div>