call card finished

This commit is contained in:
suryacp23 2026-07-17 18:02:52 +05:30
parent 3492cdbf9e
commit 2e57c47c7f
3 changed files with 139 additions and 110 deletions

View File

@ -1,58 +1,12 @@
import { Store, MapPin, TrendingUp, Calendar, Clock, Info } from 'lucide-react';
import { Store, MapPin, TrendingUp, Calendar, Clock, Info, Truck } from 'lucide-react';
import { formatValue } from '../../lib/format';
import { BASE_URL, APP_ID } from '../../api/config';
import './card.css';
// {
// "42b7f47d-96f0-4289-a6d9-38f455ad57dd__user_id": {
// "email": "surya.c@getzino.com",
// "job_title": "",
// "name": "Surya C",
// "user_id": 29113
// },
// "current_state_id": "33d7ab1f-ddf4-4549-ac99-7d23172c4ca5",
// "current_state_name": "Productive Call",
// "date_of_visit": "2026-07-15",
// "instance_id": 6592,
// "is_productive_call": "yes",
// "select_store": {
// "area": "Dummy Area",
// "business_name": "Business Name 777",
// "complete_address": "Dummy notes for Complete Address",
// "distributor_email": "test@example.com",
// "distributor_name": "Banashree Multi Millet Flour",
// "distributor_owner_name": "Dummy Distributor Owner Name",
// "distributor_phone_number": {
// "dial_code": "+91",
// "phone": "9876543210",
// "phone_with_dial_code": "+919876543210"
// },
// "email": "test@example.com",
// "instance_id": 6118,
// "notes": "Dummy notes for Notes",
// "owner_name": "Dummy Owner Name",
// "phone_number": {
// "dial_code": "+91",
// "phone": "9876543210",
// "phone_with_dial_code": "+919876543210"
// },
// "pin_code": "100000",
// "potential": "[{\"row_kgs\": 0, \"quantity\": 20, \"product_category\": \"MAIDA SUPER VALUE\"}, {\"row_kgs\": 0, \"quantity\": 30, \"product_category\": \"SUJI\"}, {\"row_kgs\": 0, \"quantity\": 10, \"product_category\": \"FORTIFIED MAIDA\"}, {\"row_kgs\": 0, \"quantity\": 300, \"product_category\": \"ATTA REGULAR\"}]",
// "route_code": "k",
// "route_name": "Krishna",
// "store_code": "STR-00024",
// "store_image": "[{\"uuid\": \"a449f6cb-0dce-4e21-9599-ab4d3e47042b\", \"blob_path\": \"434/a449f6cb-0dce-4e21-9599-ab4d3e47042b.png\", \"mime_type\": \"image/png\", \"size_bytes\": 32174, \"original_name\": \"profile.png\"}]",
// "store_location": "{\"latitude\":12.9716,\"longitude\":77.5946}",
// "sub_route": "A"
// },
// "time_of_visit": "13:02:00",
// "upload_image": null
// }
export function CallCard({ row, isDetailView = false }: { row: Record<string, any>; isDetailView?: boolean }) {
// Status Mapping
const stateName = row.current_state_name;
const lowerState = stateName.toLowerCase();
const showOrderDetails = lowerState.includes('ordered') || lowerState.includes('order closed') || lowerState.includes('close order');
let statusClass = 'z-card-status--neutral';
if (lowerState.includes('productive call')) {
@ -72,26 +26,37 @@ export function CallCard({ row, isDetailView = false }: { row: Record<string, an
const userKey = Object.keys(row).find(k => k.includes('user_id'));
const userObj = userKey ? row[userKey] : null;
// Image
let imageObj = null;
let imgData = row.upload_image || storeObj?.store_image || row.store_image || row.image;
if (typeof imgData === 'string' && imgData.startsWith('[')) {
try { imgData = JSON.parse(imgData); } catch (e) { }
}
if (Array.isArray(imgData) && imgData.length > 0) {
imageObj = imgData[0];
} else if (imgData && typeof imgData === 'object' && imgData.blob_path) {
imageObj = imgData;
}
// Store Details
const storeName = formatValue(storeObj?.business_name || storeObj?.distributor_name || row.store_name || row.customer_name || (typeof row.store === 'string' ? row.store : 'Unknown Store'));
const storeCode = formatValue(storeObj?.store_code || row.store_code || '—');
// Grid Details (Calculate from potential if missing)
let totalWeightVal = Number(row.total_kgs || row.total_weight || row.weight || 0);
let totalQuantityVal = Number(row.total_bags || row.total_quantity || row.quantity || 0);
let totalOrdersVal = Number(row.order_count || row.total_orders || 0);
// Phone and Email
const storeEmail = formatValue(storeObj?.email || row.email || row.store_email || '—');
const storePhoneRaw = storeObj?.phone_number || row.phone_number || row.phone || row.store_phone;
let storePhone = '—';
if (typeof storePhoneRaw === 'object' && storePhoneRaw !== null) {
storePhone = formatValue(storePhoneRaw.phone_with_dial_code || storePhoneRaw.phone || '—');
} else if (typeof storePhoneRaw === 'string') {
storePhone = formatValue(storePhoneRaw);
}
// Distributor Details
const distName = formatValue(storeObj?.distributor_name || '—');
const distOwner = formatValue(storeObj?.distributor_owner_name || '—');
const distEmail = formatValue(storeObj?.distributor_email || '—');
const distPhoneRaw = storeObj?.distributor_phone_number;
let distPhone = '—';
if (typeof distPhoneRaw === 'object' && distPhoneRaw !== null) {
distPhone = formatValue(distPhoneRaw.phone_with_dial_code || distPhoneRaw.phone || '—');
} else if (typeof distPhoneRaw === 'string') {
distPhone = formatValue(distPhoneRaw);
}
const hasDistributor = distName !== '—' || distOwner !== '—' || distEmail !== '—' || distPhone !== '—';
// Grid Details (Calculate from potential or orders if missing)
let totalWeightVal = Number(row.total_kgs_3 || row.total_kgs || row.total_weight || row.weight || 0);
let totalQuantityVal = Number(row.total_bags_3 || row.total_bags || row.total_quantity || row.quantity || 0);
let totalOrdersVal = Number(row.order_count || row.total_orders || (row.order_details_3 ? row.order_details_3.length : 0));
if (totalWeightVal === 0 && totalQuantityVal === 0 && storeObj?.potential) {
try {
@ -209,47 +174,18 @@ export function CallCard({ row, isDetailView = false }: { row: Record<string, an
{/* Detail View Additions */}
{isDetailView && (
<div className="z-card-details">
{storeCode !== '—' && (
{storePhone !== '—' && (
<p className="z-card-meta">
Code: <span className="z-card-meta-code">{storeCode}</span>
Phone: <span className="z-card-meta-val">{storePhone}</span>
</p>
)}
{storeEmail !== '—' && (
<p className="z-card-meta">
Email: <span className="z-card-meta-val lowercase">{storeEmail}</span>
</p>
)}
{imageObj && imageObj.uuid && (
<div className="z-card-image-box">
<img
src={`${BASE_URL}/app/${APP_ID}/view/files/${imageObj.uuid}/preview`}
alt="Store"
className="z-card-image"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
{/* Logistics Grid */}
<div className="z-card-grid">
<div>
<p className="z-card-grid-item-label">Total Weight</p>
<p className="z-card-grid-item-val">
{totalWeight} <span className="z-card-grid-item-unit">Kgs</span>
</p>
</div>
<div>
<p className="z-card-grid-item-label">Total Quantity</p>
<p className="z-card-grid-item-val">
{totalQuantity} <span className="z-card-grid-item-unit">Bags</span>
</p>
</div>
<div className="z-card-grid-footer">
<span className="z-card-grid-footer-label">Total Orders</span>
<span className="z-card-grid-footer-badge">
{totalOrders} {Number(totalOrders) === 1 ? 'Order' : 'Orders'}
</span>
</div>
</div>
{/* Route & Assignment */}
<div className="z-card-route">
@ -277,6 +213,89 @@ export function CallCard({ row, isDetailView = false }: { row: Record<string, an
</div>
</div>
{/* Call Meta Chips */}
<div className="z-card-route" style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{row.order_received_channel && (
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">
Channel: {String(row.order_received_channel).replace(/_/g, ' ').toUpperCase()}
</span>
)}
{row.remarks && (
<div className="w-full mt-2 text-sm text-gray-600 bg-gray-50 p-3 rounded-lg border border-gray-100">
<span className="font-semibold text-gray-800">Remarks: </span>
{row.remarks}
</div>
)}
</div>
{/* Product Cards (Order Details) */}
{showOrderDetails && row.order_details_3 && Array.isArray(row.order_details_3) && row.order_details_3.length > 0 && (
<div className="mt-6 border-t border-gray-100 pt-6">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<p className="z-card-meta" style={{ margin: 0, color: '#334155', fontWeight: 700 }}>Order Products</p>
<span className="text-xs font-bold text-gray-800 bg-gray-100 px-3 py-1 rounded-full border border-gray-200">
{totalOrders} {Number(totalOrders) === 1 ? 'Item' : 'Items'}
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{row.order_details_3.map((prod: any, idx: number) => {
const productName = formatValue(prod.product_name_3 || prod.product_category_3 || 'Unknown Product');
const bags = formatValue(prod.bags_3 || 0);
return (
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '14px', background: '#f8fafc', padding: '12px 16px', borderRadius: '8px', border: '1px solid #e2e8f0' }}>
<span style={{ fontWeight: 700, color: '#1e293b' }}>{productName}</span>
<span style={{ fontWeight: 800, color: '#3730a3' }}>{bags} Bags</span>
</div>
);
})}
</div>
</div>
)}
{/* Logistics Totals at the bottom */}
{showOrderDetails && (Number(totalWeightVal) > 0 || Number(totalQuantityVal) > 0) && (
<div className="mt-4 flex justify-between items-center bg-gray-50 p-4 rounded-xl border border-gray-200 shadow-sm">
<div className="flex flex-col">
<span className="text-[11px] uppercase tracking-wider font-bold text-gray-700">Total Weight</span>
<span className="text-xl font-black text-gray-900 mt-1">{totalWeight} <span className="text-sm font-bold text-gray-600">Kgs</span></span>
</div>
<div className="flex flex-col text-right">
<span className="text-[11px] uppercase tracking-wider font-bold text-gray-700">Total Quantity</span>
<span className="text-xl font-black text-gray-900 mt-1">{totalQuantity} <span className="text-sm font-bold text-gray-600">Bags</span></span>
</div>
</div>
)}
{/* Distributor Details */}
{hasDistributor && (
<div className="mt-6 border-t border-gray-100 pt-6">
<p className="z-card-meta" style={{ marginBottom: '12px', color: '#334155', fontWeight: 700 }}>Distributor Details</p>
{distName !== '—' && (
<div className="z-card-store-row" style={{ marginBottom: '12px' }}>
<Truck size={18} className="z-card-store-icon" />
<h2 className="z-card-store-name">{distName}</h2>
</div>
)}
{distOwner !== '—' && (
<p className="z-card-meta">
Owner: <span className="z-card-meta-val">{distOwner}</span>
</p>
)}
{distPhone !== '—' && (
<p className="z-card-meta">
Phone: <span className="z-card-meta-val">{distPhone}</span>
</p>
)}
{distEmail !== '—' && (
<p className="z-card-meta">
Email: <span className="z-card-meta-val lowercase">{distEmail}</span>
</p>
)}
</div>
)}
</div>
)}
</div>

View File

@ -164,10 +164,15 @@
.z-card-meta {
font-size: 13px;
color: var(--z-text-neutral-500);
color: var(--z-text-neutral-600);
margin-top: 2px;
}
.z-card-meta-val {
font-weight: 700;
color: var(--z-text-neutral-900);
}
.z-card-meta-code {
font-family: var(--font-mono);
font-weight: 500;

View File

@ -1,16 +1,21 @@
import { orderBookingClient } from '../../api/clients';
import { ORDER_BOOKING } from '../../api/config';
import { DetailView } from './DetailView';
import type { WiredDetailViewProps } from './OrderDetail';
import { useDetailViewData } from './useDetailViewData';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { CallCard } from '../cards/CallCard';
export function CallDetail({ instanceId }: WiredDetailViewProps) {
const { data, loading, error } = useDetailViewData(orderBookingClient, ORDER_BOOKING.detailViews.CALLS, 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 Call Details..." /></div>;
if (!data) return <EmptyState title="No details found" />;
export function CallDetail({ instanceId, columns = 2 }: WiredDetailViewProps) {
return (
<DetailView
client={orderBookingClient}
dvUid={ORDER_BOOKING.detailViews.CALLS}
instanceId={instanceId}
title="Call Details"
columns={columns}
/>
<div className="w-full space-y-6 pb-24">
<CallCard row={data} isDetailView={true} />
</div>
);
}