fix:cards minimal information
This commit is contained in:
parent
3dce9d655a
commit
e391056b82
@ -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 */}
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,7 +16,32 @@ 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 (
|
||||||
|
<div className="flex flex-col gap-5 w-full">
|
||||||
|
{analyticsTiles.length > 0 && <StatsTiles tiles={analyticsTiles} />}
|
||||||
<RecordView
|
<RecordView
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
rvUid={ORDER_BOOKING.recordViews.ORDERS}
|
rvUid={ORDER_BOOKING.recordViews.ORDERS}
|
||||||
@ -27,5 +55,6 @@ export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, re
|
|||||||
sortDir="desc"
|
sortDir="desc"
|
||||||
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
|
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,7 +147,8 @@ 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">
|
||||||
|
<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" />
|
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
value={search}
|
value={search}
|
||||||
@ -153,25 +158,100 @@ export function RecordView({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{filterEntries.length > 0 && (
|
{filterEntries.length > 0 && (
|
||||||
<div className="flex gap-2 overflow-x-auto scrollbar-slim -mx-1 px-1 pb-1">
|
<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 && (
|
||||||
|
<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]) => {
|
{filterEntries.map(([key, options]) => {
|
||||||
const fieldDef = resp?.config.fields.find(f => f.field_key === key);
|
const fieldDef = resp?.config.fields.find(f => f.field_key === key);
|
||||||
const label = fieldDef?.output_label || key;
|
const label = fieldDef?.output_label || key;
|
||||||
const opts = options!.map(o => ({ value: o, label: o }));
|
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 (
|
return (
|
||||||
<Select
|
<Select
|
||||||
key={key}
|
key={key}
|
||||||
value={activeFilters[key] || ''}
|
label={label.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())}
|
||||||
|
value={pendingFilters[key] || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setActiveFilters(prev => ({ ...prev, [key]: e.target.value }));
|
setPendingFilters(prev => ({ ...prev, [key]: e.target.value }));
|
||||||
setPage(1);
|
|
||||||
}}
|
}}
|
||||||
options={[{ value: '', label: `All ${label}` }, ...opts]}
|
options={[{ value: '', label: 'All' }, ...opts]}
|
||||||
className="w-40 shrink-0"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user