cards design polished
This commit is contained in:
parent
3eb445d506
commit
aced981fae
148
src/components/cards/CallCard.tsx
Normal file
148
src/components/cards/CallCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
140
src/components/cards/DailyLogCard.tsx
Normal file
140
src/components/cards/DailyLogCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
202
src/components/cards/StoreCard.tsx
Normal file
202
src/components/cards/StoreCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { orderBookingClient } from '../../api/clients';
|
||||
import { ORDER_BOOKING } from '../../api/config';
|
||||
import { RecordView } from './RecordView';
|
||||
import type { WiredRecordViewProps } from './OrdersView';
|
||||
import { CallCard } from '../cards/CallCard';
|
||||
|
||||
/** Calls record view (Calls/Visits workflow). */
|
||||
export function CallsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
|
||||
@ -15,6 +16,9 @@ export function CallsView({ onRowClick, pageSize, headerActions, rowActions, ref
|
||||
headerActions={headerActions}
|
||||
rowActions={rowActions}
|
||||
refreshKey={refreshKey}
|
||||
sortBy="instance_id"
|
||||
sortDir="desc"
|
||||
renderItem={(row) => <CallCard row={row} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@ import { dailyReportsClient } from '../../api/clients';
|
||||
import { DAILY_REPORTS } from '../../api/config';
|
||||
import { RecordView } from './RecordView';
|
||||
import type { WiredRecordViewProps } from './OrdersView';
|
||||
import { DailyLogCard } from '../cards/DailyLogCard';
|
||||
|
||||
/** 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 (
|
||||
<RecordView
|
||||
client={dailyReportsClient}
|
||||
@ -13,8 +14,10 @@ export function DailyLogsView({ onRowClick, pageSize, headerActions, rowActions,
|
||||
onRowClick={onRowClick}
|
||||
pageSize={pageSize}
|
||||
headerActions={headerActions}
|
||||
rowActions={rowActions}
|
||||
refreshKey={refreshKey}
|
||||
sortBy="instance_id"
|
||||
sortDir="desc"
|
||||
renderItem={(row) => <DailyLogCard row={row} onPunchOut={onPunchOutRow} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,6 +23,8 @@ export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, re
|
||||
headerActions={headerActions}
|
||||
rowActions={rowActions}
|
||||
refreshKey={refreshKey}
|
||||
sortBy="instance_id"
|
||||
sortDir="desc"
|
||||
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -34,6 +34,10 @@ export interface RecordViewProps {
|
||||
refreshKey?: number;
|
||||
/** Custom render function for the entire row card body */
|
||||
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,
|
||||
refreshKey,
|
||||
renderItem,
|
||||
sortBy,
|
||||
sortDir,
|
||||
}: RecordViewProps) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState('');
|
||||
@ -91,7 +97,7 @@ export function RecordView({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
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);
|
||||
} catch (e) {
|
||||
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
|
||||
@ -103,7 +109,7 @@ export function RecordView({
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey]);
|
||||
}, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey, sortBy, sortDir]);
|
||||
|
||||
const fields: RecordViewField[] = useMemo(() => {
|
||||
const all = resp?.config.fields ?? [];
|
||||
|
||||
@ -2,9 +2,10 @@ import { storeClient } from '../../api/clients';
|
||||
import { STORE } from '../../api/config';
|
||||
import { RecordView } from './RecordView';
|
||||
import type { WiredRecordViewProps } from './OrdersView';
|
||||
import { StoreCard } from '../cards/StoreCard';
|
||||
|
||||
/** 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 (
|
||||
<RecordView
|
||||
client={storeClient}
|
||||
@ -13,8 +14,10 @@ export function StoresView({ onRowClick, pageSize, headerActions, rowActions, re
|
||||
onRowClick={onRowClick}
|
||||
pageSize={pageSize}
|
||||
headerActions={headerActions}
|
||||
rowActions={rowActions}
|
||||
refreshKey={refreshKey}
|
||||
sortBy="instance_id"
|
||||
sortDir="desc"
|
||||
renderItem={(row, fields) => <StoreCard row={row} fields={fields} onEdit={onEditRow} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -30,11 +30,7 @@ export function DailyLogsPage() {
|
||||
Punch In
|
||||
</Button>
|
||||
}
|
||||
rowActions={(row) => (
|
||||
<Button size="sm" variant="secondary" onClick={() => setPunchOutInstanceId(row.instance_id as string | number)}>
|
||||
Punch Out
|
||||
</Button>
|
||||
)}
|
||||
onPunchOutRow={(row) => setPunchOutInstanceId(row.instance_id as string | number)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
@ -30,11 +30,7 @@ export function StoresPage() {
|
||||
Create Store
|
||||
</Button>
|
||||
}
|
||||
rowActions={(row) => (
|
||||
<Button size="sm" variant="secondary" onClick={() => setEditingInstanceId(row.instance_id as string | number)}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
onEditRow={(row) => setEditingInstanceId(row.instance_id as string | number)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
Loading…
Reference in New Issue
Block a user