fix:cards minimal information
This commit is contained in:
parent
3dce9d655a
commit
e391056b82
@ -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 */}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -15,7 +15,7 @@ export function DailyLogDetail({ instanceId }: WiredDetailViewProps) {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<DailyLogCard row={data} />
|
||||
<DailyLogCard row={data} isDetailView={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user