krishna_sales/src/components/dv/DailyLogDetail.tsx
2026-07-24 19:11:39 +05:30

386 lines
22 KiB
TypeScript

import { useEffect, useState } from 'react';
import { dailyReportsClient } from '../../api/clients';
import { DAILY_REPORTS, APP_ID } from '../../api/config';
import { Card } from '../reusable/Card';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { Calendar, Clock, Map, User, ClipboardList, Camera, MapPin, Hash, CheckCircle, XCircle, Activity, ArrowLeft, Mail } from 'lucide-react';
import { Button } from '../buttons/Button';
export interface DailyLogDetailProps {
instanceId: number | string;
refreshKey?: number;
onPunchOut?: () => void;
onBack?: () => void;
}
export function DailyLogDetail({ instanceId, refreshKey, onPunchOut, onBack }: DailyLogDetailProps) {
const [data, setData] = useState<Record<string, unknown> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let live = true;
async function run() {
setLoading(true);
setError(null);
try {
const r = await dailyReportsClient.detailView(DAILY_REPORTS.detailViews.DAILY_LOGS, instanceId);
if (live) setData(r.data || {});
} catch (e) {
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
} finally {
if (live) setLoading(false);
}
}
run();
return () => { live = false; };
}, [instanceId, refreshKey]);
if (error) {
return (
<Card title={`Daily Log #${instanceId}`}>
<EmptyState title={`Couldn't load record`} hint={error} />
</Card>
);
}
if (loading) {
return (
<Card title={`Daily Log #${instanceId}`}>
<div className="py-8 flex justify-center"><Spinner label="Loading details..." /></div>
</Card>
);
}
if (!data || Object.keys(data).length === 0) {
return (
<Card title={`Daily Log #${instanceId}`}>
<EmptyState title="No details found" />
</Card>
);
}
const remainingData = { ...data };
const extract = (key: string, obj: any = data) => {
if (obj && key in obj) {
const val = obj[key];
return val;
}
return null;
};
// State
const stateName = extract('current_state_name', remainingData) as string || '-';
const isPunchedIn = stateName.toLowerCase().includes('punched in');
// Date and Time
let dateRaw = extract('date', remainingData);
let timeRaw = extract('time', remainingData);
let formattedDate = '-';
if (dateRaw) {
formattedDate = new Date(dateRaw as string).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
}
// Format punch in text
const punchInTimeText = timeRaw ? `Punch-in ${timeRaw}` : 'Punch-in time unknown';
// Route Info
const routeCode = extract('route_code', remainingData) || '-';
const subRoute = extract('sub_route', remainingData) || '-';
// Notes & Details
const dayPlanNotes = extract('day_plan_notes', remainingData) || '-';
const eodNotes = extract('eod_notes_remarks', remainingData) || '-';
// KPIs
const prodCalls = extract('total_productive_calls', remainingData) || 0;
const nonProdCalls = extract('total_non_productive_calls', remainingData) || 0;
const totalCalls = Number(prodCalls) + Number(nonProdCalls);
const productivity = totalCalls > 0 ? Math.round((Number(prodCalls) / totalCalls) * 100) : 0;
// SO Info
const soKey = Object.keys(remainingData).find(k => k.endsWith('__user_id')) || 'sales_officer_name';
const soRaw = extract(soKey, remainingData);
let soName = '-';
let soEmail = '-';
let soId = '-';
if (soRaw && typeof soRaw === 'object') {
soName = (soRaw as any).name || '-';
soEmail = (soRaw as any).email || '-';
soId = (soRaw as any).user_id || (soRaw as any).id || (soRaw as any).uid || '-';
}
const soInitials = soName.length > 2 ? soName.substring(0, 2).toUpperCase() : 'SO';
// Meta
const createdAtKey = Object.keys(remainingData).find(k => k.endsWith('__created_at'));
const createdAtRaw = createdAtKey ? extract(createdAtKey, remainingData) : null;
let createdAt = '-';
if (createdAtRaw) {
createdAt = new Date(createdAtRaw as string).toLocaleString('en-US');
}
// Image
const storeImage = extract('store_image', remainingData);
const imageFiles = Array.isArray(storeImage) ? storeImage : [];
return (
<div className="w-full max-w-[1000px] mx-auto flex flex-col gap-6 pb-12">
{/* Top Header Card */}
<div className="border border-slate-200 rounded-xl p-6 shadow-sm bg-[var(--tiles-card-bg)]">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
<div className="flex items-start gap-4">
{onBack && (
<button
onClick={onBack}
className="w-10 h-10 flex mt-1 items-center justify-center rounded-full bg-slate-50 hover:bg-slate-100 transition-colors text-slate-600 shrink-0 border border-slate-200 shadow-sm"
>
<ArrowLeft size={18} />
</button>
)}
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Day Plan</div>
<div className="text-3xl font-black text-slate-900 mb-2">{formattedDate}</div>
<div className="flex items-center gap-4 text-xs text-slate-500 font-medium">
<span className="flex items-center gap-1.5"><Clock size={14} /> {punchInTimeText}</span>
<span className="flex items-center gap-1.5"><MapPin size={14} /> Route {String(routeCode)} - {String(subRoute)}</span>
</div>
</div>
</div>
<div className="flex flex-col items-end gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-slate-900 text-white flex items-center justify-center font-bold text-sm shrink-0">
{soInitials}
</div>
<div className="text-right">
<div className="text-sm font-bold text-slate-900">{String(soName)}</div>
<div className="text-[10px] text-slate-500">Sales Officer - ID {String(soId)}</div>
</div>
</div>
{isPunchedIn && onPunchOut && (
<Button onClick={onPunchOut}>
Punch Out
</Button>
)}
</div>
</div>
{/* KPI Row & Progress Bar (Only show if Punched Out) */}
{!isPunchedIn && (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="border border-slate-200 rounded-xl p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-emerald-500 uppercase tracking-wider mb-3">
<CheckCircle size={14} /> Productive Calls
</div>
<div className="text-2xl font-bold text-slate-900">{String(prodCalls)}</div>
</div>
<div className="border border-slate-200 rounded-xl p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-rose-500 uppercase tracking-wider mb-3">
<XCircle size={14} /> Non Productive
</div>
<div className="text-2xl font-bold text-slate-900">{String(nonProdCalls)}</div>
</div>
<div className="border border-slate-200 rounded-xl p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-500 uppercase tracking-wider mb-3">
<Activity size={14} /> Total Calls
</div>
<div className="text-2xl font-bold text-slate-900">{totalCalls}</div>
</div>
<div className="border border-slate-200 rounded-xl p-4 flex flex-col justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold text-blue-500 uppercase tracking-wider mb-3">
<Activity size={14} /> Productivity
</div>
<div className="text-2xl font-bold text-slate-900">{productivity}%</div>
</div>
</div>
<div className="flex items-center justify-between text-[10px] text-slate-500 mb-2">
<span>Productive vs Non Productive</span>
<span>{String(prodCalls)} / {totalCalls}</span>
</div>
<div className="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden flex">
{totalCalls > 0 && (
<>
<div className="h-full bg-emerald-500 transition-all duration-500" style={{ width: `${productivity}%` }}></div>
<div className="h-full bg-rose-400 transition-all duration-500" style={{ width: `${100 - productivity}%` }}></div>
</>
)}
</div>
</>
)}
</div>
{/* Split Content */}
<div className="flex flex-col lg:flex-row gap-6">
{/* Left Column */}
<div className="flex-[2] flex flex-col gap-6">
{/* Timeline */}
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-6">
<Calendar size={14} className="text-slate-500" /> Day Timeline
</div>
<div className="relative pl-6 flex flex-col gap-8 border-l border-slate-200 ml-3">
{/* Punched In */}
<div className="relative">
<div className="absolute -left-[31px] bg-[var(--tiles-card-bg)] rounded-full p-0.5">
<div className="w-4 h-4 rounded-full bg-blue-500 flex items-center justify-center">
<Clock size={10} className="text-white" />
</div>
</div>
<div className="text-sm font-bold text-slate-900 -mt-0.5">Punched In</div>
<div className="text-[10px] text-slate-500 mt-1">{formattedDate} {timeRaw ? `- ${timeRaw}` : ''}</div>
</div>
{/* Day Plan */}
<div className="relative">
<div className="absolute -left-[31px] bg-[var(--tiles-card-bg)] rounded-full p-0.5">
<div className="w-4 h-4 rounded-full border-2 border-slate-300 bg-[var(--tiles-card-bg)] flex items-center justify-center"></div>
</div>
<div className="text-sm font-bold text-slate-900 -mt-0.5">Day Plan Recorded</div>
<div className="text-[10px] text-slate-600 mt-1">{String(dayPlanNotes)}</div>
</div>
{/* EOD Submitted */}
<div className="relative">
<div className="absolute -left-[31px] bg-[var(--tiles-card-bg)] rounded-full p-0.5">
<div className={`w-4 h-4 rounded-full flex items-center justify-center ${isPunchedIn ? 'bg-slate-300' : 'bg-slate-900'}`}>
<ClipboardList size={10} className="text-white" />
</div>
</div>
<div className={`text-sm font-bold -mt-0.5 ${isPunchedIn ? 'text-slate-400' : 'text-slate-900'}`}>EOD Submitted</div>
{!isPunchedIn && (
<div className="text-[10px] text-slate-500 mt-1">{createdAt}</div>
)}
</div>
</div>
</div>
{/* Notes Row */}
<div className={`grid grid-cols-1 ${!isPunchedIn ? 'md:grid-cols-2' : ''} gap-6`}>
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6 flex flex-col">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
<ClipboardList size={14} className="text-slate-500" /> Day Plan Notes
</div>
<div className="text-xs text-slate-700 leading-relaxed whitespace-pre-wrap">{String(dayPlanNotes)}</div>
</div>
{!isPunchedIn && (
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6 flex flex-col">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
<ClipboardList size={14} className="text-slate-500" /> EOD Remarks
</div>
<div className="text-xs text-slate-700 leading-relaxed whitespace-pre-wrap">{String(eodNotes)}</div>
</div>
)}
</div>
{/* Route Assignment */}
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-6">
<Map size={14} className="text-slate-500" /> Route Assignment
</div>
<div className="grid grid-cols-2 gap-4">
<div className="border border-slate-100 rounded-xl p-4 bg-slate-50 flex flex-col justify-between">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">Route Code</div>
<div className="text-lg font-bold text-slate-900">{String(routeCode)}</div>
</div>
<div className="border border-slate-100 rounded-xl p-4 bg-slate-50 flex flex-col justify-between">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-3">Sub Route</div>
<div className="text-lg font-bold text-slate-900">{String(subRoute)}</div>
</div>
</div>
</div>
</div>
{/* Right Column */}
<div className="flex-1 flex flex-col gap-6 min-w-[280px]">
{/* Store Image */}
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
<Camera size={14} className="text-slate-500" /> Store Image
</div>
<div className="flex flex-col gap-2">
{imageFiles.length > 0 ? imageFiles.map((file: any, idx: number) => {
const previewUrl = `${dailyReportsClient.baseUrl}/app/${APP_ID}/view/files/${file.uuid}/preview`;
return (
<div key={file.uuid || idx} className="rounded-lg overflow-hidden bg-slate-100 border border-slate-200">
<img src={previewUrl} alt="Store" className="w-full h-auto object-cover" />
</div>
);
}) : (
<div className="w-full h-32 rounded-lg bg-slate-50 border border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-400">
<Camera size={24} className="mb-2 opacity-50" />
<span className="text-xs">No image uploaded</span>
</div>
)}
<div className="text-[10px] text-slate-400 mt-2">Uploaded at punch in.</div>
</div>
</div>
{/* SO Card */}
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-5">
<User size={14} className="text-slate-500" /> Sales Officer
</div>
<div className="flex flex-col gap-5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center shrink-0">
<User size={12} className="text-slate-500" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Name</div>
<div className="text-xs font-semibold text-slate-900">{String(soName)}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center shrink-0">
<Mail size={12} className="text-slate-500" />
</div>
<div className="overflow-hidden">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Email</div>
<div className="text-xs font-semibold text-slate-900 truncate">{String(soEmail)}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center shrink-0">
<Hash size={12} className="text-slate-500" />
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">User ID</div>
<div className="text-xs font-semibold text-slate-900">{String(soId)}</div>
</div>
</div>
</div>
</div>
{/* Meta Card */}
<div className="border border-slate-200 rounded-xl bg-[var(--tiles-card-bg)] shadow-sm overflow-hidden p-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-4">
<Hash size={14} className="text-slate-500" /> Meta
</div>
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-500">Instance</span>
<span className="font-bold text-slate-900">#{instanceId}</span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-500">State</span>
<span className="font-bold text-slate-900">{stateName}</span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-500">Performed At</span>
<span className="font-bold text-slate-900">{createdAt}</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
}