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 { formatValue } from '../../lib/format';
import { BASE_URL, APP_ID } from '../../api/config'; 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 // 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');
@ -89,7 +89,7 @@ export function CallCard({ row }: { row: Record<string, any> }) {
)} )}
</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"> <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}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`} src={`${BASE_URL}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`}
@ -104,6 +104,7 @@ export function CallCard({ row }: { row: Record<string, any> }) {
</div> </div>
{/* Logistics Grid */} {/* Logistics Grid */}
{isDetailView && (
<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>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Total Weight</p> <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> </span>
</div> </div>
</div> </div>
)}
{/* Route & Assignment */} {/* Route & Assignment */}
{isDetailView && (
<div className="grid grid-cols-2 gap-y-4 text-sm border-t border-slate-100 pt-4"> <div className="grid grid-cols-2 gap-y-4 text-sm border-t border-slate-100 pt-4">
<div> <div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Route</p> <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>
</div> </div>
)}
</div> </div>
{/* Footer Timeline */} {/* Footer Timeline */}

View File

@ -2,7 +2,7 @@ import { formatValue } from '../../lib/format';
import { FileImage } from 'lucide-react'; import { FileImage } from 'lucide-react';
import { BASE_URL, APP_ID } from '../../api/config'; 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'); const stateName = String(row.status || row.current_state_name || 'Logged');
// User details // User details
@ -93,6 +93,7 @@ export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; on
</div> </div>
{/* Day Plan & Notes */} {/* Day Plan & Notes */}
{isDetailView && (
<div className="space-y-1.5"> <div className="space-y-1.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Day Plan Summary</span> <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"> <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> </p>
</div> </div>
</div> </div>
)}
{/* Attached Verification Media */} {/* Attached Verification Media */}
{hasImage && ( {isDetailView && 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 ? ( {imgData.uuid ? (

View File

@ -122,7 +122,7 @@ export function StoreCard({ row, fields, isDetailView = false, onEdit }: { row:
</div> </div>
)} )}
</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"> <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}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`} 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> </div>
{/* Contact Info */} {/* Contact Info */}
{(ownerName !== '—' || phone !== '—' || email !== '—') && ( {isDetailView && (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">

View File

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

View File

@ -1,7 +1,10 @@
import { useEffect, useState } from 'react';
import { orderBookingClient } from '../../api/clients'; import { orderBookingClient } from '../../api/clients';
import { ORDER_BOOKING } from '../../api/config'; import { ORDER_BOOKING } from '../../api/config';
import { RecordView } from './RecordView'; import { RecordView } from './RecordView';
import { OrderCard } from '../cards/OrderCard'; import { OrderCard } from '../cards/OrderCard';
import { StatsTiles } from '../reusable/StatsTiles';
import type { TileItem } from '../../api/types';
export interface WiredRecordViewProps { export interface WiredRecordViewProps {
onRowClick?: (row: Record<string, unknown>, index: number) => void; onRowClick?: (row: Record<string, unknown>, index: number) => void;
@ -13,19 +16,45 @@ export interface WiredRecordViewProps {
/** Orders record view (Order Booking workflow). */ /** Orders record view (Order Booking workflow). */
export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) { 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 ( return (
<RecordView <div className="flex flex-col gap-5 w-full">
client={orderBookingClient} {analyticsTiles.length > 0 && <StatsTiles tiles={analyticsTiles} />}
rvUid={ORDER_BOOKING.recordViews.ORDERS} <RecordView
title="Orders" client={orderBookingClient}
onRowClick={onRowClick} rvUid={ORDER_BOOKING.recordViews.ORDERS}
pageSize={pageSize} title="Orders"
headerActions={headerActions} onRowClick={onRowClick}
rowActions={rowActions} pageSize={pageSize}
refreshKey={refreshKey} headerActions={headerActions}
sortBy="instance_id" rowActions={rowActions}
sortDir="desc" refreshKey={refreshKey}
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />} 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 { useEffect, useMemo, useRef, useState } from 'react';
import { Search } from 'lucide-react'; import { Search, Filter } from 'lucide-react';
import { cn } from '../../lib/cn'; 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';
@ -10,6 +10,8 @@ import { EmptyState } from '../reusable/EmptyState';
import { StatsTiles } from '../reusable/StatsTiles'; import { StatsTiles } from '../reusable/StatsTiles';
import { AnalyticsChart } from '../reusable/AnalyticsChart'; import { AnalyticsChart } from '../reusable/AnalyticsChart';
import { Select } from '../reusable/Select'; import { Select } from '../reusable/Select';
import { Input } from '../reusable/Input';
import { Modal } from '../reusable/Modal';
export interface RecordViewProps { export interface RecordViewProps {
/** Workflow-bound client (see api/clients.ts). */ /** Workflow-bound client (see api/clients.ts). */
@ -64,6 +66,8 @@ export function RecordView({
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debounced, setDebounced] = useState(''); const [debounced, setDebounced] = useState('');
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({}); 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 [resp, setResp] = useState<RecordViewResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -143,35 +147,111 @@ export function RecordView({
</div> </div>
<div className="flex flex-col gap-2.5"> <div className="flex flex-col gap-2.5">
<div className="relative focus-ring rounded-pill border border-border-subtle bg-card shadow-sm"> <div className="flex items-center gap-2">
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-faint pointer-events-none" /> <div className="relative flex-1 focus-ring rounded-pill border border-border-subtle bg-card shadow-sm">
<input <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
value={search} <input
onChange={(e) => setSearch(e.target.value)} value={search}
placeholder="Search…" onChange={(e) => setSearch(e.target.value)}
className="w-full h-11 rounded-pill bg-transparent pl-11 pr-4 font-sans text-sm text-strong outline-none placeholder:text-faint" 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> </div>
{filterEntries.length > 0 && ( {filterEntries.length > 0 && (
<div className="flex gap-2 overflow-x-auto scrollbar-slim -mx-1 px-1 pb-1"> <Modal
{filterEntries.map(([key, options]) => { open={showFilters}
const fieldDef = resp?.config.fields.find(f => f.field_key === key); onClose={() => setShowFilters(false)}
const label = fieldDef?.output_label || key; title="Filters"
const opts = options!.map(o => ({ value: o, label: o })); width="sm"
return ( actions={
<Select Object.values(pendingFilters).some(Boolean) && (
key={key} <button
value={activeFilters[key] || ''} onClick={() => {
onChange={(e) => { setPendingFilters({});
setActiveFilters(prev => ({ ...prev, [key]: e.target.value })); setActiveFilters({});
setPage(1); setPage(1);
}} setShowFilters(false);
options={[{ value: '', label: `All ${label}` }, ...opts]} }}
className="w-40 shrink-0" 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> )
}
>
<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> </div>