From f8abf9b5536e80fbae1bec1d3312586a77a4b7f4 Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Wed, 29 Jul 2026 10:29:14 +0530 Subject: [PATCH] admin page added --- src/App.tsx | 6 + src/api/client.ts | 19 + src/api/types.ts | 14 + src/components/cards/DailyLogCard.tsx | 55 ++- src/components/dv/DailyLogDetail.tsx | 47 +- .../forms/fields/GeolocationInput.tsx | 157 +++--- src/screens/ConsoleLayout.tsx | 16 +- src/screens/admin/DatasetItemsPage.tsx | 459 ++++++++++++++++++ src/screens/admin/DatasetsPage.tsx | 105 ++++ 9 files changed, 773 insertions(+), 105 deletions(-) create mode 100644 src/screens/admin/DatasetItemsPage.tsx create mode 100644 src/screens/admin/DatasetsPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 0bbea71..e3d382a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,8 @@ import { DailyLogsPage } from './screens/DailyLogsPage' import { AllDailyLogsPage } from './screens/AllDailyLogsPage' import { AnalyticsPage } from './screens/AnalyticsPage' import { DailySalesReportPage } from './screens/DailySalesReportPage' +import { DatasetsPage } from './screens/admin/DatasetsPage' +import { DatasetItemsPage } from './screens/admin/DatasetItemsPage' function App() { return ( @@ -35,6 +37,10 @@ function App() { } /> } /> + + }> + } /> + } /> diff --git a/src/api/client.ts b/src/api/client.ts index 308d894..cf8a2b8 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -3,6 +3,7 @@ import type { AiDecision, ApiError, AuditEntry, + Dataset, FormScreenResponse, LoginResponse, RecordViewParams, @@ -310,6 +311,24 @@ export class ZinoClient { }); } + // --- Datasets --- + + getDatasets(): Promise { + return this.request('GET', `/app/${APP_ID}/datasets`); + } + + getDataset(id: number | string): Promise { + return this.request('GET', `/app/${APP_ID}/datasets/${id}`); + } + + createDataset(payload: { name: string; data_type: 'string' | 'object'; keys: string[]; description?: string }): Promise { + return this.request('POST', `/app/${APP_ID}/datasets`, payload); + } + + updateDataset(id: number | string, payload: Partial<{ name: string; data_type: 'string' | 'object'; keys: string[]; description: string; items: any[] }>): Promise { + return this.request('PUT', `/app/${APP_ID}/datasets/${id}`, payload); + } + // --- File upload + OCR (multipart, field-scoped) --- private fieldForm(file: File, ctx: FieldContext): FormData { diff --git a/src/api/types.ts b/src/api/types.ts index 42ee795..383bf54 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -19,6 +19,20 @@ export interface LoginResponse { user: User; } +// --- Datasets --- + +export interface Dataset { + id: number; + uuid?: string; + name: string; + description?: string; + data_type: 'string' | 'object'; + keys: string[]; + item_count?: number; + items?: any[]; +} + + // --- Record view (POST /app/{appId}/view/recordview) --- export interface RecordViewField { diff --git a/src/components/cards/DailyLogCard.tsx b/src/components/cards/DailyLogCard.tsx index 7723851..f248d26 100644 --- a/src/components/cards/DailyLogCard.tsx +++ b/src/components/cards/DailyLogCard.tsx @@ -21,25 +21,29 @@ export function DailyLogCard({ row, onPunchOut, isDetailView = false }: { row: R const nonProdCalls = formatValue(row.total_non_productive_calls || 0); const eodNotes = formatValue(row.eod_notes_remarks || row.notes || '—'); - // Date - 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); + const formatDateStr = (raw: string | undefined | null) => { + if (!raw || raw === '—') return '—'; + try { + const d = new Date(raw); if (!isNaN(d.getTime())) { - dateStr = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-'); + return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-'); } - } - } catch (e) {} + } catch (e) {} + const part = String(raw).split('T')[0]; + return part || '—'; + }; const formatTimeStr = (t: string | undefined | null) => { if (!t) return '—'; const str = String(t); if (str === '—') return str; try { + if (str.includes('T') || (str.includes('-') && str.includes(':'))) { + const d = new Date(str); + if (!isNaN(d.getTime())) { + return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); + } + } let timeParts = str.split(':'); if (str.includes('T')) { timeParts = str.split('T')[1].replace('Z', '').split('.')[0].split(':'); @@ -59,16 +63,29 @@ export function DailyLogCard({ row, onPunchOut, isDetailView = false }: { row: R } }; - // Checkin time - let checkInTimeRaw = row.time || row.created_at || row.punch_in_time; + // Checkin (Punch In) time & date + let checkInTimeRaw = + row['ac04a444-9b96-463d-bc91-447fa80b028f__created_at'] || + row['ac04a444_9b96_463d_bc91_447fa80b028f__created_at'] || + row['ac04a444-9b96-463d-bc91-447fa80b028f_created_at'] || + row['ac04a444_9b96_463d_bc91_447fa80b028f_created_at'] || + row.time || row.created_at || row.punch_in_time; let checkInTime = formatTimeStr(checkInTimeRaw); + let punchInDateStr = formatDateStr(checkInTimeRaw || row.date || row.created_at); - // Check out time - let checkOutTimeRaw = row.check_out_time || row.punch_out_time; + // Check out (Punch Out) time & date + let checkOutTimeRaw = + row['37c218fc-fd91-41d1-9964-fcaeade54e74__created_at'] || + row['37c218fc_fd91-41d1-9964-fcaeade54e74__created_at'] || + row['37c218fc-fd91-41d1-9964-fcaeade54e74_created_at'] || + row['37c218fc_fd91-41d1-9964-fcaeade54e74_created_at'] || + 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; + let punchOutDateStr = checkOutTimeRaw ? formatDateStr(checkOutTimeRaw) : '—'; + if (punchOutDateStr === '—') punchOutDateStr = punchInDateStr; const isPunchedIn = stateName.toLowerCase().includes('punched in') || stateName.toLowerCase().includes('active') || (stateName.toLowerCase().includes('in') && !stateName.toLowerCase().includes('out')); @@ -112,7 +129,7 @@ export function DailyLogCard({ row, onPunchOut, isDetailView = false }: { row: R
PUNCH-IN TIME
- {dateStr} + {punchInDateStr} {checkInTime}
@@ -122,7 +139,7 @@ export function DailyLogCard({ row, onPunchOut, isDetailView = false }: { row: R
PUNCH-OUT TIME
- {dateStr} + {punchOutDateStr} {checkOutTime}
@@ -148,12 +165,12 @@ export function DailyLogCard({ row, onPunchOut, isDetailView = false }: { row: R

PUNCH-IN

-

{dateStr} {checkInTime}

+

{punchInDateStr} {checkInTime}

{checkOutTime && (

PUNCH-OUT

-

{dateStr} {checkOutTime}

+

{punchOutDateStr} {checkOutTime}

)}
diff --git a/src/components/dv/DailyLogDetail.tsx b/src/components/dv/DailyLogDetail.tsx index d05f989..8fb1678 100644 --- a/src/components/dv/DailyLogDetail.tsx +++ b/src/components/dv/DailyLogDetail.tsx @@ -51,25 +51,29 @@ export function DailyLogDetail({ instanceId, onBack }: DailyLogDetailProps) { 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); + const formatDateStr = (raw: string | undefined | null) => { + if (!raw || raw === '—') return '—'; + try { + const d = new Date(raw); if (!isNaN(d.getTime())) { - dateStr = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-'); + return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-'); } - } - } catch (e) {} + } catch (e) {} + const part = String(raw).split('T')[0]; + return part || '—'; + }; const formatTimeStr = (t: string | undefined | null) => { if (!t) return '—'; const str = String(t); if (str === '—') return str; try { + if (str.includes('T') || (str.includes('-') && str.includes(':'))) { + const d = new Date(str); + if (!isNaN(d.getTime())) { + return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); + } + } let timeParts = str.split(':'); if (str.includes('T')) { timeParts = str.split('T')[1].replace('Z', '').split('.')[0].split(':'); @@ -89,14 +93,27 @@ export function DailyLogDetail({ instanceId, onBack }: DailyLogDetailProps) { } }; - const checkInTimeRaw = row.time || row.created_at || row.punch_in_time; + const checkInTimeRaw = + row['ac04a444-9b96-463d-bc91-447fa80b028f__created_at'] || + row['ac04a444_9b96_463d_bc91_447fa80b028f__created_at'] || + row['ac04a444-9b96-463d-bc91-447fa80b028f_created_at'] || + row['ac04a444_9b96_463d_bc91_447fa80b028f_created_at'] || + row.time || row.created_at || row.punch_in_time; const checkInTime = formatTimeStr(checkInTimeRaw); + const punchInDateStr = formatDateStr(checkInTimeRaw || row.date || row.created_at); - let checkOutTimeRaw = row.check_out_time || row.punch_out_time; + let checkOutTimeRaw = + row['37c218fc-fd91-41d1-9964-fcaeade54e74__created_at'] || + row['37c218fc_fd91-41d1-9964-fcaeade54e74__created_at'] || + row['37c218fc-fd91-41d1-9964-fcaeade54e74_created_at'] || + row['37c218fc_fd91-41d1-9964-fcaeade54e74_created_at'] || + 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; + let punchOutDateStr = checkOutTimeRaw ? formatDateStr(checkOutTimeRaw) : '—'; + if (punchOutDateStr === '—') punchOutDateStr = punchInDateStr; const isPunchedIn = stateName.toLowerCase().includes('punched in') || stateName.toLowerCase().includes('active') || (stateName.toLowerCase().includes('in') && !stateName.toLowerCase().includes('out')); @@ -164,7 +181,7 @@ export function DailyLogDetail({ instanceId, onBack }: DailyLogDetailProps) {
PUNCH IN
- {dateStr} • {checkInTime} + {punchInDateStr} • {checkInTime}
@@ -177,7 +194,7 @@ export function DailyLogDetail({ instanceId, onBack }: DailyLogDetailProps) {
PUNCH OUT
- {dateStr} • {checkOutTime} + {punchOutDateStr} • {checkOutTime}
diff --git a/src/components/forms/fields/GeolocationInput.tsx b/src/components/forms/fields/GeolocationInput.tsx index cc81063..406d7b0 100644 --- a/src/components/forms/fields/GeolocationInput.tsx +++ b/src/components/forms/fields/GeolocationInput.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Modal } from '../../reusable/Modal'; import { Button } from '../../buttons/Button'; import { useJsApiLoader, GoogleMap, Marker } from '@react-google-maps/api'; -import { X } from "lucide-react"; +import { MapPin, Crosshair, Map as MapIcon, Loader2, X } from 'lucide-react'; const mapContainerStyle = { width: '100%', @@ -10,11 +10,6 @@ const mapContainerStyle = { borderRadius: '8px' }; -const defaultCenter = { - lat: 20.5937, - lng: 78.9629 -}; - export function GeolocationInput({ label, required, @@ -23,14 +18,14 @@ export function GeolocationInput({ }: { label: string; required?: boolean; - value: { latitude: number; longitude: number; accuracy?: number } | null; + value: { latitude?: number; longitude?: number; lat?: number; lng?: number; accuracy?: number } | null; onChange: (val: { latitude: number; longitude: number; accuracy?: number } | null) => void; }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isMapModalOpen, setIsMapModalOpen] = useState(false); - const [mapMarkerPos, setMapMarkerPos] = useState<{lat: number, lng: number} | null>(null); + const [mapMarkerPos, setMapMarkerPos] = useState<{ lat: number; lng: number } | null>(null); const isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); @@ -39,41 +34,46 @@ export function GeolocationInput({ googleMapsApiKey: isLocalhost ? '' : (import.meta.env.VITE_GOOGLE_MAPS_API_KEY || '') }); - // Auto-fetch location on first mount if no value is present - useEffect(() => { - if (!value) { - fetchLocation(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Extract lat, lng & accuracy from value (supporting both latitude/longitude and lat/lng) + const lat = value ? (value.latitude ?? value.lat ?? null) : null; + const lng = value ? (value.longitude ?? value.lng ?? null) : null; + const accuracy = value?.accuracy; - const fetchLocation = () => { - if (!navigator.geolocation) { - setError('Geolocation is not supported by your browser.'); + const fetchLocation = useCallback(() => { + setError(null); + if (!("geolocation" in navigator)) { + setError("Geolocation not supported on this device"); return; } setLoading(true); - setError(null); navigator.geolocation.getCurrentPosition( - (position) => { + (pos) => { onChange({ - latitude: position.coords.latitude, - longitude: position.coords.longitude, - accuracy: position.coords.accuracy, + latitude: pos.coords.latitude, + longitude: pos.coords.longitude, + accuracy: pos.coords.accuracy, }); setLoading(false); }, (err) => { - setError(err.message); + setError(err.message || "Unable to get location"); setLoading(false); }, - { enableHighAccuracy: true } + { enableHighAccuracy: true, timeout: 10000 } ); - }; + }, [onChange]); + + // Auto-fetch location on first mount if no value is present + useEffect(() => { + if (!value || (lat == null && lng == null)) { + fetchLocation(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const handleOpenMap = () => { - if (value) { - setMapMarkerPos({ lat: value.latitude, lng: value.longitude }); + if (lat != null && lng != null) { + setMapMarkerPos({ lat, lng }); } else { setMapMarkerPos(null); } @@ -93,72 +93,89 @@ export function GeolocationInput({ if (mapMarkerPos) { onChange({ latitude: mapMarkerPos.lat, - longitude: mapMarkerPos.lng + longitude: mapMarkerPos.lng, + accuracy: 10, }); setIsMapModalOpen(false); } }; const clear = () => onChange(null); - const hasValue = !!value; + const hasValidCoords = lat != null && lng != null; return ( -
- +
+