added maps to the detail view

This commit is contained in:
suryacp23 2026-07-28 12:12:44 +05:30
parent 7d6f1a9021
commit fdfa5e386b
5 changed files with 265 additions and 47 deletions

View File

@ -4,18 +4,204 @@ import type { WiredDetailViewProps } from './OrderDetail';
import { useDetailViewData } from './useDetailViewData'; import { useDetailViewData } from './useDetailViewData';
import { Spinner } from '../reusable/Spinner'; import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState'; import { EmptyState } from '../reusable/EmptyState';
import { DailyLogCard } from '../cards/DailyLogCard'; import { formatValue } from '../../lib/format';
import { ArrowLeft, MapPin, Clock, FileText } from 'lucide-react';
import { NearestStoresMap } from '../maps/NearestStoresMap';
export function DailyLogDetail({ instanceId }: WiredDetailViewProps) { export interface DailyLogDetailProps extends WiredDetailViewProps {
onBack?: () => void;
}
export function DailyLogDetail({ instanceId, onBack }: DailyLogDetailProps) {
const { data, loading, error } = useDetailViewData(dailyReportsClient, DAILY_REPORTS.detailViews.DAILY_LOGS, instanceId); const { data, loading, error } = useDetailViewData(dailyReportsClient, DAILY_REPORTS.detailViews.DAILY_LOGS, instanceId);
if (error) return <EmptyState title="Couldn't load record" hint={error} />; if (error) {
if (loading) return <div className="py-8 flex justify-center"><Spinner label="Loading Daily Log..." /></div>;
if (!data) return <EmptyState title="No details found" />;
return ( return (
<div className="w-full"> <div className="bg-[var(--tiles-card-bg)] p-8 rounded-2xl border border-border-default shadow-sm text-center">
<DailyLogCard row={data} isDetailView={true} /> <EmptyState title="Couldn't load record" hint={error} />
</div>
);
}
if (loading) {
return (
<div className="bg-[var(--tiles-card-bg)] p-12 rounded-2xl border border-border-default shadow-sm flex justify-center items-center">
<Spinner label="Loading daily log..." />
</div>
);
}
if (!data) return <EmptyState title="No details found" />;
const row = data;
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.performed_by_email || row.user_email || '—');
const initials = userName !== '—' ? String(userName).substring(0, 2).toUpperCase() : 'SO';
// Route Details
const routeCode = formatValue(row.route_code || row.route || '—');
const subRoute = formatValue(row.sub_route || row.area || '—');
// Stats
const prodCalls = formatValue(row.total_productive_calls || 0);
const nonProdCalls = formatValue(row.total_non_productive_calls || 0);
const eodNotes = formatValue(row.eod_notes_remarks || row.notes || '—');
// Format Date & Time
let dateStr = '—';
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) {}
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 checkInTimeRaw = row.time || row.created_at || row.punch_in_time;
const checkInTime = formatTimeStr(checkInTimeRaw);
let checkOutTimeRaw = row.check_out_time || row.punch_out_time;
if (!checkOutTimeRaw && stateName.toLowerCase().includes('out') && row.time) {
checkOutTimeRaw = row.time;
}
const checkOutTime = checkOutTimeRaw ? formatTimeStr(checkOutTimeRaw) : null;
const isPunchedIn = stateName.toLowerCase().includes('punched in') || stateName.toLowerCase().includes('active') || (stateName.toLowerCase().includes('in') && !stateName.toLowerCase().includes('out'));
return (
<div className="text-slate-800 py-6 lg:py-8 font-sans space-y-6">
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm space-y-6">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
<div className="flex items-start gap-4">
{onBack && (
<button onClick={onBack} className="mt-1 p-1.5 sm:p-2 text-slate-800 hover:text-slate-800 hover:bg-slate-100/80 rounded-full transition-colors shrink-0 -ml-2">
<ArrowLeft size={20} strokeWidth={2.5} />
</button>
)}
<div className="w-12 h-12 rounded-full bg-slate-200 text-slate-700 flex items-center justify-center font-bold text-lg shrink-0">
{initials}
</div>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2 text-xs mb-1">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${
isPunchedIn ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' : 'bg-blue-50 text-blue-700 border border-blue-200'
}`}>
{stateName}
</span>
</div>
<h1 className="text-xl font-bold text-slate-900 tracking-tight">
{userName}
</h1>
{userEmail !== '—' && (
<div className="text-sm font-medium text-slate-500">{userEmail}</div>
)}
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-800 font-medium pt-1">
<span className="flex items-center gap-1">
<MapPin size={13} className="text-slate-800" /> {routeCode}
</span>
<span></span>
<span>{subRoute}</span>
</div>
</div>
</div>
<div className="flex flex-nowrap items-center gap-3 w-full lg:w-auto overflow-x-auto pb-2 min-w-0">
<div className="bg-primary-light-bg border border-primary-light-border rounded-2xl p-3 text-center min-w-[100px] flex-1">
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-0.5">PROD. CALLS</div>
<div className="text-[20px] font-extrabold text-primary">{prodCalls}</div>
</div>
<div className="bg-primary-light-bg border border-primary-light-border rounded-2xl p-3 text-center min-w-[100px] flex-1">
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-0.5">NON PROD.</div>
<div className="text-[20px] font-extrabold text-primary">{nonProdCalls}</div>
</div>
<div className="bg-primary-light-bg border border-primary-light-border rounded-2xl p-3 text-center min-w-[110px] flex-1">
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-0.5">TOTAL CALLS</div>
<div className="text-sm font-bold text-primary mt-1 whitespace-nowrap overflow-hidden text-ellipsis">
{Number(prodCalls) + Number(nonProdCalls)}
</div>
</div>
</div>
</div>
<div className="border-t border-slate-100 pt-4">
<div className="flex items-center gap-8 overflow-x-auto pb-2">
<div className="flex items-center gap-3 min-w-max">
<div className="w-8 h-8 rounded-full bg-emerald-50 text-emerald-600 flex items-center justify-center text-xs shrink-0">
<Clock size={16} />
</div>
<div className="text-xs">
<div className="font-bold text-slate-800 uppercase tracking-wider text-[10px] mb-0.5">PUNCH IN</div>
<div className="text-slate-800 font-medium text-[12px] leading-tight">
{dateStr} {checkInTime}
</div>
</div>
</div>
{checkOutTime && (
<div className="flex items-center gap-3 min-w-max">
<div className="w-8 h-8 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center text-xs shrink-0">
<Clock size={16} />
</div>
<div className="text-xs">
<div className="font-bold text-slate-800 uppercase tracking-wider text-[10px] mb-0.5">PUNCH OUT</div>
<div className="text-slate-800 font-medium text-[12px] leading-tight">
{dateStr} {checkOutTime}
</div>
</div>
</div>
)}
</div>
</div>
</div>
{!isPunchedIn && eodNotes !== '—' && (
<div className="bg-[var(--tiles-card-bg)] rounded-2xl border border-slate-200/80 p-6 shadow-sm">
<div className="flex items-center gap-2 mb-3 text-slate-800">
<FileText size={18} className="text-slate-500" />
<h3 className="font-bold text-lg">EOD Notes</h3>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-sm text-slate-700 leading-relaxed whitespace-pre-wrap">
{eodNotes}
</div>
</div>
)}
{/* Embed Map at the bottom of the detail view */}
<div className="w-full overflow-hidden">
<NearestStoresMap />
</div>
</div> </div>
); );
} }

View File

@ -628,6 +628,21 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
); );
} }
if (type === 'app_user') {
const user = client.currentUser();
const displayVal = user?.name || val;
return (
<TextField
label={f.name}
required={f.mandatory}
type="text"
value={(displayVal as string) ?? ''}
onChange={() => {}}
disabled
/>
);
}
return ( return (
<TextField <TextField
label={f.name} label={f.name}

View File

@ -6,12 +6,16 @@ export function TextField({
required, required,
value, value,
onChange, onChange,
disabled,
readOnly,
}: { }: {
label: string; label: string;
type: string; type: string;
required?: boolean; required?: boolean;
value: string; value: string;
onChange: (val: string) => void; onChange: (val: string) => void;
disabled?: boolean;
readOnly?: boolean;
}) { }) {
return ( return (
<Input <Input
@ -20,6 +24,8 @@ export function TextField({
type={type === 'number' ? 'number' : type === 'email' ? 'email' : 'text'} type={type === 'number' ? 'number' : type === 'email' ? 'email' : 'text'}
value={value ?? ''} value={value ?? ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
disabled={disabled}
readOnly={readOnly}
/> />
); );
} }

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api'; import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api';
import { PIPELINE, BASE_URL } from '../../api/config'; import { PIPELINE, BASE_URL } from '../../api/config';
import { Loader2, MapPin } from 'lucide-react'; import { Loader2, MapPin, ChevronDown, ChevronUp } from 'lucide-react';
// Interfaces for the API response // Interfaces for the API response
interface StoreLocation { interface StoreLocation {
@ -42,6 +42,7 @@ export function NearestStoresMap() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [selectedStore, setSelectedStore] = useState<Store | null>(null); const [selectedStore, setSelectedStore] = useState<Store | null>(null);
const [isListExpanded, setIsListExpanded] = useState(false);
const isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); const isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
@ -145,7 +146,7 @@ export function NearestStoresMap() {
</div> </div>
)} )}
<div className="w-full h-[50vh] min-h-[350px] relative px-4 pt-4"> <div className="w-full h-[50vh] min-h-[350px] relative pt-4">
<GoogleMap <GoogleMap
mapContainerStyle={mapContainerStyle} mapContainerStyle={mapContainerStyle}
center={userLocation || defaultCenter} center={userLocation || defaultCenter}
@ -203,11 +204,18 @@ export function NearestStoresMap() {
</GoogleMap> </GoogleMap>
</div> </div>
<div className="mx-4 mt-4 mb-4 bg-card rounded-2xl shadow-[0_1px_0_0_rgba(0,0,0,0.02),0_20px_40px_-24px_rgba(20,80,60,0.15)] p-5 flex flex-col gap-4"> <div className="mt-4 mb-4 bg-card rounded-2xl shadow-[0_1px_0_0_rgba(0,0,0,0.02),0_20px_40px_-24px_rgba(20,80,60,0.15)] p-5 flex flex-col gap-4">
<div className="flex justify-between items-center cursor-pointer" onClick={() => setIsListExpanded(!isListExpanded)}>
<h3 className="font-bold text-[17px] text-strong flex items-center gap-2"> <h3 className="font-bold text-[17px] text-strong flex items-center gap-2">
<MapPin className="w-5 h-5 text-primary" /> <MapPin className="w-5 h-5 text-primary" />
Nearest Stores ({stores.length}) Nearest Stores ({stores.length})
</h3> </h3>
<button className="text-slate-400 hover:text-slate-600 transition-colors p-1">
{isListExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</button>
</div>
{isListExpanded && (
<>
{loading && stores.length === 0 && ( {loading && stores.length === 0 && (
<div className="py-4 text-center text-sm text-slate-500"> <div className="py-4 text-center text-sm text-slate-500">
Fetching nearby stores... Fetching nearby stores...
@ -232,6 +240,8 @@ export function NearestStoresMap() {
</div> </div>
))} ))}
</div> </div>
</>
)}
</div> </div>
</div> </div>
); );

View File

@ -8,7 +8,7 @@ import { ClipboardList } from 'lucide-react';
import { DynamicForm } from '../components/forms/DynamicForm'; import { DynamicForm } from '../components/forms/DynamicForm';
import { DAILY_REPORTS } from '../api/config'; import { DAILY_REPORTS } from '../api/config';
import { dailyReportsClient } from '../api/clients'; import { dailyReportsClient } from '../api/clients';
import { NearestStoresMap } from '../components/maps/NearestStoresMap';
export function DailyLogsPage() { export function DailyLogsPage() {
const params = useParams(); const params = useParams();
@ -20,10 +20,20 @@ export function DailyLogsPage() {
const [punchOutTitle, setPunchOutTitle] = useState("Punch Out"); const [punchOutTitle, setPunchOutTitle] = useState("Punch Out");
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
if (instanceId != null) {
return (
<div className="flex flex-col h-full bg-transparent w-full relative">
<div className="flex-1 pb-24 w-full overflow-y-auto">
<DailyLogDetail instanceId={instanceId} onBack={() => navigate(`/daily`)} />
</div>
</div>
);
}
return ( return (
<div className="flex flex-col h-full bg-transparent w-full relative"> <div className="flex flex-col h-full bg-transparent w-full relative">
<div className="flex-1 overflow-y-auto pb-24"> <div className="flex-1 overflow-y-auto pb-24">
<NearestStoresMap />
<DailyLogsView <DailyLogsView
hideTiles hideTiles
@ -85,15 +95,6 @@ export function DailyLogsPage() {
/> />
)} )}
</Modal> </Modal>
<Modal
open={instanceId != null}
onClose={() => navigate(`/daily`)}
title={instanceId != null ? `Daily Log #${instanceId}` : undefined}
width="lg"
>
{instanceId != null && <DailyLogDetail instanceId={instanceId} />}
</Modal>
</div> </div>
); );
} }