cards design polished

This commit is contained in:
suryacp23 2026-07-14 16:21:37 +05:30
parent 3eb445d506
commit aced981fae
10 changed files with 516 additions and 16 deletions

View File

@ -0,0 +1,148 @@
import { formatValue } from '../../lib/format';
export function CallCard({ row }: { row: Record<string, any> }) {
const instanceId = row.instance_id || '—';
// 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');
// Store Details
const storeName = formatValue(row.store || row.store_name || row.customer_name || 'Unknown Store');
const storeCode = formatValue(row.store_code || '—');
// Grid Details
const totalWeight = formatValue(row.total_kgs || row.total_weight || row.weight || 0);
const totalQuantity = formatValue(row.total_bags || row.total_quantity || row.quantity || 0);
const totalOrders = formatValue(row.order_count || row.total_orders || 0);
// Route & Assignment
const routeName = formatValue(row.route_name || row.route || '—');
const areaName = formatValue(row.area || row.area_name || '—');
// User
const userName = formatValue(row.sales_officer || row.user_name || '—');
const userEmail = formatValue(row.performed_by_email || row.user_email || '—');
const initials = userName !== '—' ? String(userName).substring(0, 2).toUpperCase() : 'SO';
// Date & Time
let dateStr = '—';
let timeStr = '—';
if (row.visit_date) {
try {
const d = new Date(row.visit_date as string);
if (!isNaN(d.getTime())) dateStr = d.toISOString().split('T')[0];
} catch {
dateStr = String(row.visit_date).split('T')[0];
}
}
if (row.visit_time) {
try {
const t = String(row.visit_time);
if (t.includes('T')) {
timeStr = t.split('T')[1].replace('Z', '').split('.')[0];
} else {
timeStr = t;
}
} catch {
timeStr = String(row.visit_time);
}
}
// Choose styling based on status
const statusBg = isProductive ? 'bg-emerald-50' : 'bg-amber-50';
const statusBorder = isProductive ? 'border-emerald-100' : 'border-amber-100';
const statusDot = isProductive ? 'bg-emerald-500 animate-pulse' : 'bg-amber-500';
const statusText = isProductive ? 'text-emerald-700' : 'text-amber-700';
return (
<div className="w-full bg-white rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 overflow-hidden font-sans transition-all hover:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)]">
{/* Header Status Bar */}
<div className={`${statusBg} px-5 py-3.5 border-b ${statusBorder} flex justify-between items-center`}>
<div className="flex items-center gap-2">
<span className={`w-2.5 h-2.5 rounded-full ${statusDot}`}></span>
<span className={`text-[13px] font-bold ${statusText} tracking-wide uppercase`}>{stateName}</span>
</div>
<span className="text-[11px] font-mono bg-white border border-slate-200 text-slate-600 px-2.5 py-1 rounded-md font-medium shadow-sm">
ID: {instanceId}
</span>
</div>
{/* Main Content */}
<div className="p-5 space-y-5">
{/* Store Details */}
<div>
<span className="text-[10px] font-bold text-indigo-600 tracking-wider uppercase">Store Details</span>
<h2 className="text-[17px] font-bold text-slate-800 mt-0.5 leading-tight">{storeName}</h2>
{storeCode !== '—' && (
<p className="text-[13px] text-slate-500 mt-0.5">
Code: <span className="font-mono font-medium text-slate-700">{storeCode}</span>
</p>
)}
</div>
{/* Logistics Grid */}
<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>
<p className="text-[17px] font-bold text-slate-800 mt-0.5 leading-none">
{totalWeight} <span className="text-[12px] font-medium text-slate-500">Kgs</span>
</p>
</div>
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Total Quantity</p>
<p className="text-[17px] font-bold text-slate-800 mt-0.5 leading-none">
{totalQuantity} <span className="text-[12px] font-medium text-slate-500">Bags</span>
</p>
</div>
<div className="col-span-2 pt-3 border-t border-slate-200/60 flex justify-between items-center">
<span className="text-[12px] font-medium text-slate-500">Total Orders</span>
<span className="bg-indigo-50 text-indigo-700 border border-indigo-100/50 text-[11px] font-bold px-2.5 py-0.5 rounded-full">
{totalOrders} {Number(totalOrders) === 1 ? 'Order' : 'Orders'}
</span>
</div>
</div>
{/* Route & Assignment */}
<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>
<p className="font-semibold text-slate-700 mt-0.5 text-[13px]">{routeName}</p>
</div>
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Area</p>
<p className="font-semibold text-slate-700 mt-0.5 text-[13px]">{areaName}</p>
</div>
<div className="col-span-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Sales Officer</p>
<div className="flex items-center gap-2 mt-1.5">
<div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center font-bold text-[11px] text-slate-600 uppercase shadow-inner">
{initials}
</div>
<div className="flex flex-col min-w-0">
<p className="font-semibold text-[13px] text-slate-800 leading-tight truncate">{userName}</p>
{userEmail !== '—' && (
<p className="text-[11px] text-slate-500 font-mono truncate">{userEmail}</p>
)}
</div>
</div>
</div>
</div>
</div>
{/* Footer Timeline */}
<div className="bg-slate-50/70 px-5 py-3 border-t border-slate-100 flex justify-between text-[11px] text-slate-500 font-medium">
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<span>{dateStr}</span>
</div>
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>{timeStr}</span>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,140 @@
import { formatValue } from '../../lib/format';
import { FileImage } from 'lucide-react';
export function DailyLogCard({ row, onPunchOut }: { row: Record<string, any>; onPunchOut?: (row: any) => void }) {
const stateName = String(row.status || row.current_state_name || 'Logged');
// User details
const userObj = (row.sales_officer_name || row.user_id || row.user) as Record<string, any> | null;
const userName = formatValue(userObj?.name || row.user_name || 'Unknown User');
const userEmail = formatValue(userObj?.email || row.user_email || '—');
const initials = userName !== 'Unknown User' && userName !== '—' ? String(userName).substring(0, 2).toUpperCase() : 'US';
// Punch Details
let dateStr = '—';
let timeStr = '—';
if (row.date) dateStr = String(row.date).split('T')[0];
else if (row.created_at) dateStr = String(row.created_at).split('T')[0];
if (row.time) timeStr = String(row.time).split('T')[1]?.split('.')[0] || String(row.time);
else if (row.created_at) timeStr = String(row.created_at).split('T')[1]?.split('.')[0] || '—';
const routeCode = formatValue(row.route_code || row.route || '—');
const dayPlanNotes = formatValue(row.day_plan_notes || row.notes || row.plan || 'No notes provided.');
// Store Image / Verification Media
const imageArray = row.store_image || row.image || row.verification_media;
const imgData = Array.isArray(imageArray) && imageArray.length > 0 ? imageArray[0] : null;
let imgName = 'None attached';
let imgSize = '—';
let imgMime = '—';
if (imgData && typeof imgData === 'object') {
imgName = imgData.original_name || imgData.name || 'attachment.png';
const sizeBytes = imgData.size_bytes || imgData.size || 0;
imgSize = sizeBytes > 0 ? `${(sizeBytes / 1024).toFixed(1)} KB` : 'Unknown size';
imgMime = imgData.mime_type || imgData.type || 'image/jpeg';
}
const hasImage = imgName !== 'None attached';
// Dynamic styling based on status
const isPunchedIn = stateName.toLowerCase().includes('in') || stateName.toLowerCase().includes('active');
const statusBg = isPunchedIn ? 'bg-amber-50' : 'bg-slate-50';
const statusBorder = isPunchedIn ? 'border-amber-100' : 'border-slate-200';
const statusDot = isPunchedIn ? 'bg-amber-500 animate-pulse' : 'bg-slate-400';
const statusText = isPunchedIn ? 'text-amber-800' : 'text-slate-600';
return (
<div className="w-full bg-white rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 overflow-hidden font-sans transition-all duration-300 hover:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)]">
{/* Attendance Status Header */}
<div className={`${statusBg} px-6 py-4 border-b ${statusBorder} flex justify-between items-center`}>
<div className="flex items-center gap-2">
<span className={`w-2.5 h-2.5 rounded-full ${statusDot}`}></span>
<span className={`text-sm font-semibold ${statusText} tracking-wide uppercase`}>{stateName}</span>
</div>
</div>
<div className="p-6 space-y-6">
{/* Profile and Account Info */}
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-indigo-50 rounded-full flex items-center justify-center font-bold text-base text-indigo-600 uppercase border border-indigo-100 shadow-inner">
{initials}
</div>
<div className="flex flex-col min-w-0">
<h3 className="text-lg font-bold text-slate-800 leading-tight truncate">{userName}</h3>
{userEmail !== '—' && (
<p className="text-xs text-slate-400 font-mono mt-0.5 truncate">{userEmail}</p>
)}
</div>
</div>
{/* Punch Details Grid */}
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-xl border border-slate-100/80 text-sm">
<div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Punch Date</span>
<p className="font-semibold text-slate-800 mt-1">{dateStr}</p>
</div>
<div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Punch Time</span>
<p className="font-semibold text-slate-800 mt-1">{timeStr}</p>
</div>
<div className="col-span-2 pt-2.5 border-t border-slate-200/60 flex justify-between items-center">
<span className="text-xs font-semibold text-slate-500 tracking-wide">Route Assigned</span>
<span className="bg-slate-200/70 text-slate-700 text-xs font-bold px-2.5 py-0.5 rounded-full uppercase">
Route: {routeCode}
</span>
</div>
</div>
{/* Day Plan & Notes */}
<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">
<p className="text-sm font-medium text-indigo-900 italic leading-relaxed">
"{dayPlanNotes}"
</p>
</div>
</div>
{/* Attached Verification Media */}
{hasImage && (
<div className="space-y-2">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block">Verification Image</span>
<div className="flex items-center justify-between p-3 rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex items-center gap-2.5">
<div className="text-indigo-500 bg-indigo-50 p-1.5 rounded-lg border border-indigo-100/50">
<FileImage size={24} />
</div>
<div className="flex flex-col min-w-0">
<p className="text-xs font-bold text-slate-800 truncate max-w-[180px]">{imgName}</p>
<p className="text-[10px] text-slate-400 font-mono mt-0.5">{imgSize}</p>
</div>
</div>
<span className="text-[10px] font-mono text-slate-400 bg-slate-50 px-2 py-1 rounded border border-slate-100 uppercase">
MIME: {imgMime.split('/')[1] || 'Media'}
</span>
</div>
</div>
)}
{/* Inline Punch Out Action */}
{onPunchOut && isPunchedIn && (
<div className="pt-2">
<button
onClick={(e) => { e.stopPropagation(); onPunchOut(row); }}
className="w-full bg-slate-800 text-white text-[13px] font-bold py-2.5 rounded-xl hover:bg-slate-700 transition-colors shadow-sm flex justify-center items-center gap-2 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2"
>
Punch Out
</button>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,202 @@
import { formatValue } from '../../lib/format';
import { Edit2 } from 'lucide-react';
export function StoreCard({ row, fields, onEdit }: { row: Record<string, any>; fields: any[]; onEdit?: (row: any) => void }) {
// Status
const stateName = String(row.current_state_name || row.status || 'Active');
const isActive = stateName.toLowerCase().includes('active') || stateName.toLowerCase().includes('created') || stateName.toLowerCase().includes('approved');
// Store Details
const storeName = formatValue(row.business_name || row.store_name || row.name || 'Unknown Store');
const storeCode = formatValue(row.store_code || row.code || '—');
const ownerName = formatValue(row.owner_name || row.contact_person || '—');
const phoneObj = row.phone_number as Record<string, unknown> | null;
const phone = formatValue(phoneObj?.phone_with_dial_code || phoneObj?.phone || row.phone || row.mobile || '—');
// Route Info
const routeName = formatValue(row.route_name || row.route || '—');
const subRouteName = formatValue(row.sub_route || row.sub_route_name || '—');
const routeCode = formatValue(row.route_code || routeName.charAt(0) || 'R');
// Distributor Info
const distName = formatValue(row.distributor_name || '—');
const distContact = formatValue(row.distributor_owner_name || '—');
const distPhoneObj = row.distributor_phone_number as Record<string, unknown> | null;
const distPhone = formatValue(distPhoneObj?.phone_with_dial_code || distPhoneObj?.phone || row.distributor_phone || '—');
// Address
const address = formatValue(row.complete_address || row.address || '—');
const pin = formatValue(row.pin_code || row.pincode || '—');
const locObj = row.store_location as Record<string, unknown> | null;
const lat = formatValue(locObj?.latitude || row.latitude || '—');
const lng = formatValue(locObj?.longitude || row.longitude || '—');
const areaName = formatValue(row.area || row.area_name || '—');
// User
const userKey = Object.keys(row).find(k => k.includes('user_id') || k.includes('created_by') || k.includes('sales_officer'));
const userObj = userKey ? (typeof row[userKey] === 'object' ? row[userKey] : null) : null;
const userName = formatValue(userObj?.name || row.sales_officer || row.user_name || '—');
const userEmail = formatValue(userObj?.email || row.user_email || '—');
const initials = userName !== '—' ? String(userName).substring(0, 2).toUpperCase() : 'SO';
// Date
let dateStr = '—';
const dateKey = Object.keys(row).find(k => k.includes('created_at'));
const rawDate = dateKey ? row[dateKey] : (row.created_at || row.date || row.registration_date);
if (rawDate) {
try {
const d = new Date(rawDate as string);
if (!isNaN(d.getTime())) dateStr = d.toISOString().split('T')[0];
else dateStr = String(rawDate).split('T')[0];
} catch {
dateStr = String(rawDate).split('T')[0];
}
}
// Potential (grid)
const potentialKey = Object.keys(row).find(k => k.includes('potential') || k.includes('inventory'));
let potentials: any[] = [];
if (potentialKey && Array.isArray(row[potentialKey])) {
potentials = row[potentialKey];
} else {
// try to find any grid
const gridField = fields.find(f => f.data_type === 'grid');
if (gridField && Array.isArray(row[gridField.field_key])) {
potentials = row[gridField.field_key];
}
}
const statusBg = isActive ? 'bg-blue-50' : 'bg-slate-50';
const statusBorder = isActive ? 'border-blue-100' : 'border-slate-200';
const statusDot = isActive ? 'bg-blue-500 animate-pulse' : 'bg-slate-400';
const statusText = isActive ? 'text-blue-700' : 'text-slate-600';
return (
<div className="w-full bg-white rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 overflow-hidden font-sans transition-all hover:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)]">
{/* Header Status Bar */}
<div className={`${statusBg} px-5 py-3.5 border-b ${statusBorder} flex justify-between items-center`}>
<div className="flex items-center gap-2">
<span className={`w-2.5 h-2.5 rounded-full ${statusDot}`}></span>
<span className={`text-[13px] font-bold ${statusText} tracking-wide uppercase`}>{storeCode}</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
onEdit?.(row);
}}
className={`w-7 h-7 rounded-full flex items-center justify-center bg-white border border-slate-200 shadow-sm ${statusText} hover:bg-slate-50 transition-colors focus:outline-none focus:ring-2 focus:ring-slate-200 focus:ring-offset-1`}
>
<Edit2 size={14} />
</button>
</div>
{/* Main Content */}
<div className="p-5 space-y-5">
{/* Store Identity */}
<div>
<span className="text-[10px] font-bold text-indigo-600 tracking-wider uppercase">Store Details</span>
<h2 className="text-[17px] font-bold text-slate-800 mt-0.5 leading-tight break-words w-full">{storeName}</h2>
{/* Status Badge for Route/Sub-route */}
{(routeName !== '—' || subRouteName !== '—') && (
<div className="mt-2">
<span className="inline-block bg-slate-100 text-slate-700 text-[11px] font-bold px-2.5 py-1 rounded-md">
R: {routeName} <span className="uppercase opacity-50">({String(routeCode).substring(0, 1)})</span>
{subRouteName !== '—' && ` • Sub: ${subRouteName}`}
</span>
</div>
)}
{/* Contact Info */}
{(ownerName !== '—' || phone !== '—') && (
<div className="mt-3 space-y-1.5 text-[13px] text-slate-600">
{ownerName !== '—' && (
<p className="flex items-center gap-2">
<span className="font-bold text-slate-400 text-[10px] w-12 uppercase tracking-wider">Owner</span>
<span className="text-slate-700 font-semibold truncate">{ownerName}</span>
</p>
)}
{phone !== '—' && (
<p className="flex items-center gap-2">
<span className="font-bold text-slate-400 text-[10px] w-12 uppercase tracking-wider">Phone</span>
<span className="font-mono text-slate-700">{phone}</span>
</p>
)}
</div>
)}
</div>
{/* Fulfillment / Distributor */}
{distName !== '—' && (
<div className="bg-slate-50 p-3.5 rounded-xl border border-slate-100 space-y-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Assigned Distributor</p>
<div>
<h4 className="text-[14px] font-bold text-slate-800 leading-tight">{distName}</h4>
{distContact !== '—' && <p className="text-[12px] font-medium text-slate-500 mt-0.5">Contact: {distContact}</p>}
{distPhone !== '—' && <p className="text-[12px] font-mono text-slate-600 mt-0.5">{distPhone}</p>}
</div>
</div>
)}
{/* Product Potential Breakdown */}
{potentials.length > 0 && (
<div>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Inventory Potential</p>
<div className="space-y-1.5">
{potentials.map((item, idx) => {
const prodName = formatValue(item.product_name || item.product_category || item.product || `Item ${idx+1}`);
const qty = formatValue(item.quantity || item.qty || item.potential || item.bags || 0);
return (
<div key={idx} className="flex justify-between items-center text-[13px] px-2.5 py-1.5 rounded-lg bg-indigo-50/50 border border-indigo-100/40">
<span className="font-semibold text-slate-700 truncate mr-2">{prodName}</span>
<span className="font-bold text-indigo-700 shrink-0">{qty} Qty</span>
</div>
);
})}
</div>
</div>
)}
{/* Logistics & Address */}
<div className="grid grid-cols-2 gap-4 text-sm border-t border-slate-100 pt-4">
<div className="col-span-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Address</p>
<p className="text-slate-700 mt-0.5 font-medium text-[13px] leading-tight">{address}</p>
{(pin !== '—' || lat !== '—') && (
<p className="text-[11px] font-mono text-slate-400 mt-1">
{pin !== '—' && `PIN: ${pin}`} {pin !== '—' && lat !== '—' && '|'} {lat !== '—' && `Loc: ${lat}, ${lng}`}
</p>
)}
</div>
<div className="col-span-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Created By</p>
<div className="flex items-center gap-2 mt-1.5">
<div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center font-bold text-[11px] text-slate-600 uppercase shadow-inner">
{initials}
</div>
<div className="flex flex-col min-w-0">
<p className="font-semibold text-[13px] text-slate-800 leading-tight truncate">{userName}</p>
{userEmail !== '—' && (
<p className="text-[11px] text-slate-500 font-mono truncate">{userEmail}</p>
)}
</div>
</div>
</div>
</div>
</div>
{/* Footer Timestamp */}
<div className="bg-slate-50/70 px-5 py-3 border-t border-slate-100 flex justify-between items-center text-[11px] text-slate-500 font-medium">
<div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<span>Registered: {dateStr}</span>
</div>
{areaName !== '—' && (
<span className="text-[10px] text-slate-400 uppercase font-mono tracking-wider truncate max-w-[120px] text-right">
Area: {areaName}
</span>
)}
</div>
</div>
);
}

View File

@ -2,6 +2,7 @@ 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 type { WiredRecordViewProps } from './OrdersView'; import type { WiredRecordViewProps } from './OrdersView';
import { CallCard } from '../cards/CallCard';
/** Calls record view (Calls/Visits workflow). */ /** Calls record view (Calls/Visits workflow). */
export function CallsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) { export function CallsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
@ -15,6 +16,9 @@ export function CallsView({ onRowClick, pageSize, headerActions, rowActions, ref
headerActions={headerActions} headerActions={headerActions}
rowActions={rowActions} rowActions={rowActions}
refreshKey={refreshKey} refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row) => <CallCard row={row} />}
/> />
); );
} }

View File

@ -2,9 +2,10 @@ import { dailyReportsClient } from '../../api/clients';
import { DAILY_REPORTS } from '../../api/config'; import { DAILY_REPORTS } from '../../api/config';
import { RecordView } from './RecordView'; import { RecordView } from './RecordView';
import type { WiredRecordViewProps } from './OrdersView'; import type { WiredRecordViewProps } from './OrdersView';
import { DailyLogCard } from '../cards/DailyLogCard';
/** Daily Logs record view (Daily Reports workflow). */ /** Daily Logs record view (Daily Reports workflow). */
export function DailyLogsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) { export function DailyLogsView({ onRowClick, onPunchOutRow, pageSize, headerActions, refreshKey }: WiredRecordViewProps & { onPunchOutRow?: (row: any) => void }) {
return ( return (
<RecordView <RecordView
client={dailyReportsClient} client={dailyReportsClient}
@ -13,8 +14,10 @@ export function DailyLogsView({ onRowClick, pageSize, headerActions, rowActions,
onRowClick={onRowClick} onRowClick={onRowClick}
pageSize={pageSize} pageSize={pageSize}
headerActions={headerActions} headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey} refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row) => <DailyLogCard row={row} onPunchOut={onPunchOutRow} />}
/> />
); );
} }

View File

@ -23,6 +23,8 @@ export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, re
headerActions={headerActions} headerActions={headerActions}
rowActions={rowActions} rowActions={rowActions}
refreshKey={refreshKey} refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />} renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
/> />
); );

View File

@ -34,6 +34,10 @@ export interface RecordViewProps {
refreshKey?: number; refreshKey?: number;
/** Custom render function for the entire row card body */ /** Custom render function for the entire row card body */
renderItem?: (row: Record<string, unknown>, fields: RecordViewField[]) => React.ReactNode; renderItem?: (row: Record<string, unknown>, fields: RecordViewField[]) => React.ReactNode;
/** Sort by column key */
sortBy?: string;
/** Sort direction */
sortDir?: 'asc' | 'desc';
} }
/** /**
@ -53,6 +57,8 @@ export function RecordView({
rowActions, rowActions,
refreshKey, refreshKey,
renderItem, renderItem,
sortBy,
sortDir,
}: RecordViewProps) { }: RecordViewProps) {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -91,7 +97,7 @@ export function RecordView({
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const r = await client.recordView(rvUid, { page, limit: pageSize, search: debounced, filters: filtersParam }); const r = await client.recordView(rvUid, { page, limit: pageSize, search: debounced, filters: filtersParam, sortBy, sortDir });
if (live) setResp(r); if (live) setResp(r);
} catch (e) { } catch (e) {
if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
@ -103,7 +109,7 @@ export function RecordView({
return () => { return () => {
live = false; live = false;
}; };
}, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey]); }, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]);
const fields: RecordViewField[] = useMemo(() => { const fields: RecordViewField[] = useMemo(() => {
const all = resp?.config.fields ?? []; const all = resp?.config.fields ?? [];

View File

@ -2,9 +2,10 @@ import { storeClient } from '../../api/clients';
import { STORE } from '../../api/config'; import { STORE } from '../../api/config';
import { RecordView } from './RecordView'; import { RecordView } from './RecordView';
import type { WiredRecordViewProps } from './OrdersView'; import type { WiredRecordViewProps } from './OrdersView';
import { StoreCard } from '../cards/StoreCard';
/** Store record view (Store workflow). */ /** Store record view (Store workflow). */
export function StoresView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps & { headerActions?: React.ReactNode }) { export function StoresView({ onRowClick, onEditRow, pageSize, headerActions, refreshKey }: WiredRecordViewProps & { headerActions?: React.ReactNode; onEditRow?: (row: any) => void }) {
return ( return (
<RecordView <RecordView
client={storeClient} client={storeClient}
@ -13,8 +14,10 @@ export function StoresView({ onRowClick, pageSize, headerActions, rowActions, re
onRowClick={onRowClick} onRowClick={onRowClick}
pageSize={pageSize} pageSize={pageSize}
headerActions={headerActions} headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey} refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row, fields) => <StoreCard row={row} fields={fields} onEdit={onEditRow} />}
/> />
); );
} }

View File

@ -30,11 +30,7 @@ export function DailyLogsPage() {
Punch In Punch In
</Button> </Button>
} }
rowActions={(row) => ( onPunchOutRow={(row) => setPunchOutInstanceId(row.instance_id as string | number)}
<Button size="sm" variant="secondary" onClick={() => setPunchOutInstanceId(row.instance_id as string | number)}>
Punch Out
</Button>
)}
/> />
<Modal <Modal

View File

@ -30,11 +30,7 @@ export function StoresPage() {
Create Store Create Store
</Button> </Button>
} }
rowActions={(row) => ( onEditRow={(row) => setEditingInstanceId(row.instance_id as string | number)}
<Button size="sm" variant="secondary" onClick={() => setEditingInstanceId(row.instance_id as string | number)}>
Edit
</Button>
)}
/> />
<Modal <Modal