diff --git a/src/components/dv/DailyLogDetail.tsx b/src/components/dv/DailyLogDetail.tsx index 73338e5..d05f989 100644 --- a/src/components/dv/DailyLogDetail.tsx +++ b/src/components/dv/DailyLogDetail.tsx @@ -4,18 +4,204 @@ import type { WiredDetailViewProps } from './OrderDetail'; import { useDetailViewData } from './useDetailViewData'; import { Spinner } from '../reusable/Spinner'; 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); - if (error) return ; - if (loading) return
; + if (error) { + return ( +
+ +
+ ); + } + + if (loading) { + return ( +
+ +
+ ); + } + if (!data) return ; + 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 | 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 ( -
- +
+
+
+
+ {onBack && ( + + )} +
+ {initials} +
+
+
+ + {stateName} + +
+

+ {userName} +

+ {userEmail !== '—' && ( +
{userEmail}
+ )} +
+ + {routeCode} + + + {subRoute} +
+
+
+ +
+
+
PROD. CALLS
+
{prodCalls}
+
+
+
NON PROD.
+
{nonProdCalls}
+
+
+
TOTAL CALLS
+
+ {Number(prodCalls) + Number(nonProdCalls)} +
+
+
+
+ +
+
+
+
+ +
+
+
PUNCH IN
+
+ {dateStr} • {checkInTime} +
+
+
+ + {checkOutTime && ( +
+
+ +
+
+
PUNCH OUT
+
+ {dateStr} • {checkOutTime} +
+
+
+ )} +
+
+
+ + {!isPunchedIn && eodNotes !== '—' && ( +
+
+ +

EOD Notes

+
+
+ {eodNotes} +
+
+ )} + + {/* Embed Map at the bottom of the detail view */} +
+ +
); } diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index e278f88..42c64aa 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -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 ( + {}} + disabled + /> + ); + } + return ( void; + disabled?: boolean; + readOnly?: boolean; }) { return ( onChange(e.target.value)} + disabled={disabled} + readOnly={readOnly} /> ); } diff --git a/src/components/maps/NearestStoresMap.tsx b/src/components/maps/NearestStoresMap.tsx index 687cb71..464411f 100644 --- a/src/components/maps/NearestStoresMap.tsx +++ b/src/components/maps/NearestStoresMap.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api'; 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 interface StoreLocation { @@ -42,6 +42,7 @@ export function NearestStoresMap() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedStore, setSelectedStore] = useState(null); + const [isListExpanded, setIsListExpanded] = useState(false); const isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); @@ -145,7 +146,7 @@ export function NearestStoresMap() {
)} -
+
-
-

- - Nearest Stores ({stores.length}) -

- {loading && stores.length === 0 && ( -
- Fetching nearby stores... -
- )} - {!loading && stores.length === 0 && userLocation && ( -
- No stores found nearby. -
- )} -
- {stores.map(store => ( -
setSelectedStore(store)}> -
- {store.business_name} -
-

{store.area}

-
- {store.route_name} - {store.distance_km} km -
-
- ))} +
+
setIsListExpanded(!isListExpanded)}> +

+ + Nearest Stores ({stores.length}) +

+
+ {isListExpanded && ( + <> + {loading && stores.length === 0 && ( +
+ Fetching nearby stores... +
+ )} + {!loading && stores.length === 0 && userLocation && ( +
+ No stores found nearby. +
+ )} +
+ {stores.map(store => ( +
setSelectedStore(store)}> +
+ {store.business_name} +
+

{store.area}

+
+ {store.route_name} + {store.distance_km} km +
+
+ ))} +
+ + )}
); diff --git a/src/screens/DailyLogsPage.tsx b/src/screens/DailyLogsPage.tsx index 497b690..0c485ca 100644 --- a/src/screens/DailyLogsPage.tsx +++ b/src/screens/DailyLogsPage.tsx @@ -8,7 +8,7 @@ import { ClipboardList } from 'lucide-react'; import { DynamicForm } from '../components/forms/DynamicForm'; import { DAILY_REPORTS } from '../api/config'; import { dailyReportsClient } from '../api/clients'; -import { NearestStoresMap } from '../components/maps/NearestStoresMap'; + export function DailyLogsPage() { const params = useParams(); @@ -20,10 +20,20 @@ export function DailyLogsPage() { const [punchOutTitle, setPunchOutTitle] = useState("Punch Out"); const [refreshKey, setRefreshKey] = useState(0); + if (instanceId != null) { + return ( +
+
+ navigate(`/daily`)} /> +
+
+ ); + } + return (
- + )} - - navigate(`/daily`)} - title={instanceId != null ? `Daily Log #${instanceId}` : undefined} - width="lg" - > - {instanceId != null && } -
); }