card design done

This commit is contained in:
suryacp23 2026-07-16 16:11:47 +05:30
parent 7eaf1d7742
commit 7f88b4a7e4
7 changed files with 269 additions and 427 deletions

View File

@ -1,158 +1,119 @@
import { formatValue } from '../../lib/format';
import { FileImage } from 'lucide-react';
import { BASE_URL, APP_ID } from '../../api/config';
import { User as UserIcon, Clock, LogOut } from 'lucide-react';
import { Button } from '../buttons/Button';
import './card.css';
export function DailyLogCard({ row, isDetailView = false, onPunchOut }: { row: Record<string, any>; isDetailView?: boolean; onPunchOut?: (row: any) => void }) {
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
// Date
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];
try {
if (dateStr !== '—') {
const d = new Date(dateStr);
if (!isNaN(d.getTime())) {
dateStr = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-');
}
}
} catch (e) {}
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 formatTimeStr = (t: string | undefined | null) => {
if (!t) return '—';
const str = String(t);
if (str === '—') return str;
try {
let timeParts = str.split(':');
if (str.includes('T')) {
timeParts = str.split('T')[1].replace('Z', '').split('.')[0].split(':');
}
if (timeParts.length >= 2) {
let h = parseInt(timeParts[0]);
const m = timeParts[1];
const ampm = h >= 12 ? 'PM' : 'AM';
h = h % 12;
h = h ? h : 12;
const hStr = h < 10 ? '0' + h : h;
return `${hStr}:${m} ${ampm}`;
}
return str.split('.')[0];
} catch {
return str.split('.')[0];
}
};
const routeCode = formatValue(row.route_code || row.route || '—');
const dayPlanNotes = formatValue(row.day_plan_notes || row.notes || row.plan || 'No notes provided.');
// Checkin time
let checkInTimeRaw = row.time || row.created_at || row.punch_in_time;
let checkInTime = formatTimeStr(checkInTimeRaw);
// 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';
// Check out time
let checkOutTimeRaw = row.check_out_time || row.punch_out_time;
if (!checkOutTimeRaw && stateName.toLowerCase().includes('out') && row.time) {
checkOutTimeRaw = row.time;
}
let checkOutTime = checkOutTimeRaw ? formatTimeStr(checkOutTimeRaw) : null;
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';
let statusClass = 'z-card-status--neutral';
if (isPunchedIn) statusClass = 'z-card-status--success';
else if (stateName.toLowerCase().includes('out')) statusClass = 'z-card-status--primary';
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 className="z-card">
<div className="z-card-header">
<div className="z-card-header-icon">
<UserIcon size={16} className="text-strong" />
<span className="text-strong">{userName}</span>
</div>
<span className={`z-card-status ${statusClass}`}>
{stateName}
</span>
</div>
<div className="p-6 space-y-6">
<div className="z-card-divider"></div>
{/* 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}
<div className="z-card-footer" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: '16px' }}>
<div className="z-card-footer-item" style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '4px' }}>
<div className="z-card-footer-label" style={{ whiteSpace: 'nowrap' }}><Clock size={14} /> PUNCH-IN TIME</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' }}>
<span className="z-card-footer-value" style={{ display: 'flex', gap: '6px', fontSize: '13px' }}>
<span>{dateStr}</span>
<span>{checkInTime}</span>
</span>
</div>
</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">
<p className="text-sm font-medium text-indigo-900 italic leading-relaxed">
"{dayPlanNotes}"
</p>
</div>
</div>
)}
{/* Attached Verification Media */}
{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 ? (
<div className="w-full h-48 rounded-xl overflow-hidden border border-slate-200 shadow-sm bg-slate-100 relative group">
<img
src={`${BASE_URL}/app/${APP_ID}/view/files/${imgData.uuid}/preview`}
alt={imgName}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).parentElement!.innerHTML = `<div class="flex items-center justify-center h-full text-xs text-slate-400 break-all px-4 text-center">${imgName}</div>`;
}}
/>
<a href={`${BASE_URL}/app/${APP_ID}/view/files/${imgData.uuid}/preview`} target="_blank" rel="noopener noreferrer" className="absolute inset-0 z-10"></a>
</div>
) : (
<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>
)}
{checkOutTime && (
<div className="z-card-footer-item" style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '4px' }}>
<div className="z-card-footer-label" style={{ whiteSpace: 'nowrap' }}><Clock size={14} /> PUNCH-OUT TIME</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' }}>
<span className="z-card-footer-value" style={{ display: 'flex', gap: '6px', fontSize: '13px' }}>
<span>{dateStr}</span>
<span>{checkOutTime}</span>
</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>
{isPunchedIn && onPunchOut && (
<div style={{ padding: '0 6px 0px 6px' }}>
<Button
variant="danger"
full
iconLeft={<LogOut size={16} />}
onClick={(e) => { e.stopPropagation(); onPunchOut(row); }}
>
Punch Out
</Button>
</div>
)}
</div>
);
}

View File

@ -1,102 +1,77 @@
import { formatValue } from '../../lib/format';
import { Badge } from '../reusable/Badge';
import { CheckCircle2Icon, Factory, UserIcon } from 'lucide-react';
import { Store, Calendar, Package, ShoppingBag } from 'lucide-react';
import './card.css';
export function OrderCard({ row, fields }: { row: Record<string, any>; fields: any[] }) {
const orderIdStr = row.order_id;
const orderIdStr = row.order_id || '—';
// Extract store lookup object if it exists
const storeObj = (row.select_store || row.store) as Record<string, unknown> | null;
const storeVal = formatValue(storeObj?.business_name || '—');
const routeVal = formatValue(storeObj?.route_name || '—');
const distributorVal = formatValue(storeObj?.distributor_name || '—');
// Extract user/performed_by object
const userKey = Object.keys(row).find(k => k.includes('user_id') || k.includes('created_by'));
const userObj = userKey ? row[userKey] as Record<string, unknown> : null;
const userVal = formatValue(userObj?.name || userObj?.user_name || '—');
const storeVal = formatValue(storeObj?.business_name || row.business_name || row.store_name || '—');
// Extract date
const dateKey = Object.keys(row).find(k => k.includes('date_of_order'));
const dateVal = dateKey && row[dateKey] ? formatValue(row[dateKey]) : '—';
let dateVal = dateKey && row[dateKey] ? formatValue(row[dateKey]) : '—';
// Try to append time if it exists
const timeKey = Object.keys(row).find(k => k.includes('time_of_order'));
if (timeKey && row[timeKey]) {
dateVal += ` ${formatValue(row[timeKey])}`;
}
const stateName = String(row.current_state_name || '');
const isProductive = stateName.toLowerCase().includes('productive') || stateName.toLowerCase().includes('closed') || stateName.toLowerCase().includes('ordered');
const productiveLabel = stateName || 'Pending';
let statusClass = 'z-card-status--neutral';
if (isProductive) statusClass = 'z-card-status--success';
// Order Details Grid
const gridField = fields.find(f => f.data_type === 'grid' || String(f.field_key).includes('order_details'));
const gridValRaw = gridField ? row[gridField.field_key] : (row.order_details || row.order_details_2 || row.order_details_3);
const gridVal = Array.isArray(gridValRaw) ? gridValRaw : [];
// Total Kgs
let totalKgsVal = Number(row.total_kgs || row.total_kgs_2 || row.total_kgs_3 || row.total_weight || row.weight || 0);
if (totalKgsVal === 0 && gridVal.length > 0) {
gridVal.forEach(p => { totalKgsVal += Number(p.row_kgs || p.kgs || p.weight || 0); })
}
const totalKgs = formatValue(totalKgsVal);
return (
<div className="w-full bg-white rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-200/75 p-0 overflow-hidden transition-all hover:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)]">
{/* Top Header Section */}
<div className="p-4 bg-slate-50 border-b border-slate-100">
<div className="flex justify-between items-start mb-4">
<div className="flex flex-col">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-0.5">Order ID</span>
<span className="text-[17px] font-extrabold text-slate-800 tracking-tight">{orderIdStr}</span>
</div>
<Badge tone={isProductive ? 'success' : 'neutral'} className="shadow-sm border-0 font-medium tracking-wide">
{isProductive && <CheckCircle2Icon size={14} className="text-emerald-500 mr-1" />}
{productiveLabel}
</Badge>
<div className="z-card">
<div className="z-card-header">
<div className="z-card-header-icon">
<ShoppingBag size={16} />
<span>{orderIdStr}</span>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Date</span>
<span className="text-[13px] font-semibold text-slate-700">{dateVal}</span>
</div>
<div className="flex flex-col gap-0.5 col-span-2">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Store & Route</span>
<span className="text-[13px] font-semibold text-slate-700 truncate">
{storeVal} <span className="text-slate-400 font-normal ml-1">({routeVal})</span>
</span>
</div>
<span className={`z-card-status ${statusClass}`}>
{productiveLabel}
</span>
</div>
<div className="z-card-store-row">
<Store size={18} className="z-card-store-icon" />
<h2 className="z-card-store-name">{storeVal}</h2>
</div>
<div className="z-card-divider"></div>
<div className="z-card-footer">
<div className="z-card-footer-item">
<div className="z-card-footer-label"><Calendar size={14} /> DATE & TIME</div>
<span className="z-card-footer-value">{dateVal}</span>
</div>
<div className="z-card-footer-item" style={{ textAlign: 'left', width: '96px' }}>
<div className="z-card-footer-label"><Package size={14} /> TOTAL KGS</div>
<span className="z-card-footer-value">{totalKgs} kg</span>
</div>
</div>
{/* Middle Section: Distributor & User */}
<div className="px-4 py-3 bg-slate-50/60 border-b border-slate-100 flex items-center justify-between gap-2">
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0 text-indigo-600">
<Factory size={14} />
</div>
<div className="flex flex-col min-w-0">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">Distributor</span>
<span className="text-[12px] font-semibold text-slate-700 truncate">{distributorVal}</span>
</div>
</div>
<div className="w-px h-8 bg-slate-200 shrink-0 mx-2"></div>
<div className="flex items-center gap-2.5 min-w-0 flex-1 justify-end">
<div className="flex flex-col items-end min-w-0 text-right">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">Placed by</span>
<span className="text-[12px] font-semibold text-slate-700 truncate">{userVal}</span>
</div>
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center shrink-0 text-blue-600">
<UserIcon size={14} />
</div>
</div>
</div>
{/* Bottom Section: Order Items */}
{gridVal.length > 0 && (
<div className="p-4 bg-white">
<div className="flex justify-between items-center mb-3">
<h3 className="text-[11px] font-bold text-slate-500 uppercase tracking-widest flex items-center">
Order Items
<span className="ml-2 bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full text-[10px]">
{gridVal.length}
</span>
</h3>
</div>
<div className="flex flex-col gap-1.5">
<div className="z-card-details" style={{ display: 'block', borderTop: '1px solid #f1f5f9' }}>
<p className="z-card-meta" style={{ marginBottom: '8px' }}>Order Products</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{gridVal.map((item, i) => {
const getVal = (key: string) => {
if (item[key] !== undefined) return item[key];
@ -106,29 +81,12 @@ export function OrderCard({ row, fields }: { row: Record<string, any>; fields: a
};
const productName = formatValue(getVal('product_name') || getVal('product_category') || 'Unknown Product');
const sku = formatValue(getVal('sku_code') || getVal('sku') || 'N/A');
const bags = formatValue(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0);
return (
<div key={i} className="flex items-center justify-between gap-4 p-2.5 rounded-xl bg-slate-50/80 border border-slate-100 transition-colors">
<div className="flex flex-col gap-1 min-w-0">
<div className="flex items-center">
<span className="bg-blue-50 text-blue-700 text-[9px] uppercase font-bold tracking-widest px-2 py-0.5 rounded-md">
{sku}
</span>
</div>
<span className="text-[13px] font-bold text-slate-700 truncate">
{productName}
</span>
</div>
<div className="flex flex-col items-end justify-center shrink-0 pl-2">
<span className="text-[22px] font-black text-slate-800 leading-none tracking-tighter">
{bags}
</span>
<span className="text-[9px] text-slate-400 font-bold uppercase tracking-widest mt-1">
bags
</span>
</div>
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', background: '#f8fafc', padding: '8px 12px', borderRadius: '8px' }}>
<span style={{ fontWeight: 600, color: '#334155' }}>{productName}</span>
<span style={{ fontWeight: 700, color: '#4f46e5' }}>{bags} Bags</span>
</div>
);
})}

View File

@ -1,12 +1,14 @@
import { formatValue } from '../../lib/format';
import { Edit2 } from 'lucide-react';
import { Store, MapPin, User as UserIcon, Edit2, Phone, Mail } from 'lucide-react';
import { BASE_URL, APP_ID } from '../../api/config';
import { Button } from '../buttons/Button';
import './card.css';
export function StoreCard({ row, fields, isDetailView = false, onEdit }: { row: Record<string, any>; fields: any[]; isDetailView?: boolean; onEdit?: (row: any) => void }) {
export function StoreCard({ row, isDetailView = false, onEdit }: { row: Record<string, any>; isDetailView?: boolean; 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');
// Image
let imageObj = null;
const imgData = row.store_image || row.image || row.upload_image || row.store_photo;
@ -24,216 +26,136 @@ export function StoreCard({ row, fields, isDetailView = false, onEdit }: { row:
const phone = formatValue(phoneObj?.phone_with_dial_code || phoneObj?.phone || row.phone || row.mobile || '—');
const emailObj = row.email_address as Record<string, unknown> | null;
const email = formatValue(emailObj?.email || row.email || row.store_email || row.business_email || '—');
// 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 || '—');
const distEmailObj = row.distributor_email_address as Record<string, unknown> | null;
const distEmail = formatValue(distEmailObj?.email || row.distributor_email || '—');
// Address
const address = formatValue(row.complete_address || row.address || '—');
const pin = formatValue(row.pin_code || row.pincode || '—');
// Location
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 latRaw = locObj?.latitude || row.latitude;
const lngRaw = locObj?.longitude || row.longitude;
const lat = formatValue(latRaw || '—');
const lng = formatValue(lngRaw || '—');
const hasLocation = lat !== '—' && lng !== '—' && latRaw !== undefined && lngRaw !== undefined;
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';
let statusClass = 'z-card-status--neutral';
if (isActive) statusClass = 'z-card-status--success';
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 className="z-card">
<div className="z-card-header">
<div className="z-card-header-icon">
<Store size={16} />
<span>{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 className="flex justify-between items-start gap-4">
<div className="flex-1 min-w-0">
<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>
)}
</div>
{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`}
alt="Store"
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
</div>
{/* Contact Info */}
{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">
<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>
)}
{email !== '—' && (
<p className="flex items-center gap-2">
<span className="font-bold text-slate-400 text-[10px] w-12 uppercase tracking-wider">Email</span>
<span className="font-mono text-slate-700 truncate">{email}</span>
</p>
)}
</div>
)}
{/* Fulfillment / Distributor */}
{isDetailView && 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">Phone: {distPhone}</p>}
{distEmail !== '—' && <p className="text-[12px] font-mono text-slate-600 mt-0.5">Email: {distEmail}</p>}
</div>
</div>
)}
{/* Product Potential Breakdown */}
{isDetailView && 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 */}
{isDetailView && (
<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}
{hasLocation ? (
<a
href={`https://maps.google.com/?q=${lat},${lng}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center transition-all hover:opacity-80 active:scale-[0.97]"
style={{ textDecoration: 'none', padding: '4px' }}
onClick={(e) => e.stopPropagation()}
title="Open in Google Maps"
>
<img
src="https://upload.wikimedia.org/wikipedia/commons/a/aa/Google_Maps_icon_%282020%29.svg"
alt="Google Maps"
style={{ width: '20px', height: '20px' }}
/>
</a>
) : (
<span className={`z-card-status ${statusClass}`}>
{stateName}
</span>
)}
</div>
<div className="z-card-divider"></div>
<div >
<h2 style={{ fontSize: '17px', fontWeight: 700, color: 'var(--text-strong)', marginBottom: '12px', lineHeight: '1.3' }}>
{storeName}
</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'max-content 1fr', columnGap: '12px', rowGap: '8px', fontSize: '13px' }}>
{ownerName !== '—' && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--text-muted)' }}>
<UserIcon size={14} />
<span>Owner</span>
</div>
<div style={{ fontWeight: 500, color: 'var(--text-strong)' }}>{ownerName}</div>
</>
)}
{areaName !== '—' && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--text-muted)' }}>
<MapPin size={14} />
<span>Area</span>
</div>
<div style={{ fontWeight: 500, color: 'var(--text-strong)' }}>{areaName}</div>
</>
)}
</div>
</div>
{!isDetailView && (
<div className='mt-4' >
<Button
variant="secondary"
full
iconLeft={<Edit2 size={15} />}
onClick={(e) => {
if (onEdit) {
e.stopPropagation();
onEdit(row);
}
}}
>
Edit Details
</Button>
</div>
)}
{isDetailView && (
<div className="z-card-details" style={{ borderTop: '1px solid var(--border-subtle)', padding: '16px' }}>
{imageObj && imageObj.uuid && (
<div className="z-card-image-box" style={{ marginBottom: '16px' }}>
<img
src={`${BASE_URL}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`}
alt="Store"
className="z-card-image"
style={{ borderRadius: '8px', width: '100%', objectFit: 'cover', maxHeight: '200px' }}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', background: 'var(--surface-sunk)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)' }}>
<Phone size={16} />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Phone</span>
<span style={{ fontSize: '14px', fontWeight: 500, color: 'var(--text-strong)' }}>{phone}</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '32px', height: '32px', borderRadius: '8px', background: 'var(--surface-sunk)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)' }}>
<Mail size={16} />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Email</span>
<span style={{ fontSize: '14px', fontWeight: 500, color: 'var(--text-strong)' }}>{email}</span>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

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

View File

@ -8,7 +8,7 @@ import { StoreCard } from '../cards/StoreCard';
import { MapPin } from 'lucide-react';
export function StoreDetail({ instanceId }: WiredDetailViewProps) {
const { data, config, loading, error } = useDetailViewData(storeClient, STORE.detailViews.STORE, instanceId);
const { data, loading, error } = useDetailViewData(storeClient, STORE.detailViews.STORE, instanceId);
if (error) return <EmptyState title="Couldn't load record" hint={error} />;
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Store..." /></div>;
@ -21,7 +21,7 @@ export function StoreDetail({ instanceId }: WiredDetailViewProps) {
return (
<div className="w-full space-y-6">
<StoreCard row={data} fields={config?.fields || []} isDetailView={true} />
<StoreCard row={data} isDetailView={true} />
{hasLocation && (
<a

View File

@ -17,6 +17,7 @@ export function DailyLogsView({ onRowClick, onPunchOutRow, pageSize, headerActio
refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
hideChart
renderItem={(row) => <DailyLogCard row={row} onPunchOut={onPunchOutRow} />}
/>
);

View File

@ -17,7 +17,7 @@ export function StoresView({ onRowClick, onEditRow, pageSize, headerActions, ref
refreshKey={refreshKey}
sortBy="instance_id"
sortDir="desc"
renderItem={(row, fields) => <StoreCard row={row} fields={fields} onEdit={onEditRow} />}
renderItem={(row) => <StoreCard row={row} onEdit={onEditRow} />}
/>
);
}