import * as icon from '@mdi/js'; import axios from 'axios'; import Head from 'next/head'; import Link from 'next/link'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import BaseButton from '../BaseButton'; import BaseButtons from '../BaseButtons'; import BaseIcon from '../BaseIcon'; import CardBox from '../CardBox'; import SectionMain from '../SectionMain'; import SectionTitleLineWithButton from '../SectionTitleLineWithButton'; import { getPageTitle } from '../../config'; import { useAppSelector } from '../../stores/hooks'; import { defaultGeoSeekLocation, GeoSeekItem, GeoSeekItemType, geoSeekModules, GeoSeekModuleKey, } from '../../data/geoseek'; import { createCartFromResults, createSmartDraft, currency, filterGeoSeekItems, getBusinessInsights, getCheckoutSummary, getDistancePriority, GeoSeekScoredItem, GeoSeekSmartDraft, } from '../../data/geoseekAutomation'; type Props = { moduleKey?: string; }; type GeoSeekActionHistory = { id: string; moduleKey: GeoSeekModuleKey; label: string; message: string; createdAt: string; }; type QuickAction = { label: string; message: string; color: 'info' | 'success' | 'warning' | 'contrast'; iconPath: string; }; type GeoSeekLocationSource = 'browser' | 'demo'; type GeoSeekResolvedLocation = { label: string; latitude: number; longitude: number; source: GeoSeekLocationSource; accuracyMeters?: number; }; type PublicPlaceOffering = { id?: string; name?: string; description?: string; offering_type?: string; price?: number | string | null; stock_status?: string; stock_label?: string; stock_quantity?: number | string | null; is_verified?: boolean; }; type PublicPlaceRow = { id?: string; name?: string; short_description?: string; full_description?: string; address?: string; city?: string; province?: string; latitude?: number | string | null; longitude?: number | string | null; average_price?: number | string | null; rating_average?: number | string | null; rating_count?: number | string | null; is_verified?: boolean; distance_km?: number | string | null; geo_score?: number | string | null; category?: { name?: string; slug?: string; description?: string; } | null; offerings?: PublicPlaceOffering[]; offerings_summary?: { products?: number; services?: number; total?: number; available?: number; }; live_status?: { status?: string; label?: string; }; external_source?: string; }; type PublicPlacesResponse = { rows?: PublicPlaceRow[]; count?: number; radius_km?: number | null; radius_zone?: { label?: string; range?: string; description?: string; } | null; filtered_by_radius?: boolean; expanded_for_nearest?: boolean; external_sources?: string[]; external_source_errors?: string[]; total_candidates?: number; }; type GeoSeekApiMeta = { count: number; totalCandidates: number; radiusZoneLabel: string; externalSources: string[]; externalSourceErrors: string[]; filteredByRadius: boolean; expandedForNearest: boolean; }; const typeLabels: Record = { place: 'Tempat', product: 'Produk', service: 'Jasa', business: 'UMKM', culinary: 'Kuliner', health: 'Kesehatan', property: 'Properti', automotive: 'Otomotif', tourism: 'Wisata', event: 'Event', promo: 'Promo', courier: 'Kurir', }; const backendDemoGeoSeekLocation: GeoSeekResolvedLocation = { label: 'Pekanbaru Data Demo', latitude: 0.5095, longitude: 101.4549, source: 'demo', }; const emptyApiMeta: GeoSeekApiMeta = { count: 0, totalCandidates: 0, radiusZoneLabel: 'Radius aktif', externalSources: [], externalSourceErrors: [], filteredByRadius: false, expandedForNearest: false, }; const radiusOptions = [1, 5, 10, 20]; const apiQueryByModule: Partial> = { home: 'produk jasa umkm lokal', search: 'produk jasa umkm lokal', map: '', products: 'produk stok toko', services: 'jasa servis booking', umkm: '', culinary: 'kuliner cafe warung', health: 'kesehatan apotek klinik', property: 'properti ruko rumah', automotive: 'bengkel otomotif servis', tourism: 'wisata hotel lokal', events: 'event bazar lokal', promos: 'promo diskon lokal', marketplace: 'produk marketplace lokal', booking: 'jasa booking lokal', courier: 'kurir antar lokal', 'business-dashboard': '', profile: '', }; const getApiQuery = (query: string, moduleKey: GeoSeekModuleKey) => query.trim() || apiQueryByModule[moduleKey] || ''; const toOptionalNumber = (value: unknown) => { if (value === null || value === undefined || value === '') return undefined; const number = Number(value); return Number.isFinite(number) ? number : undefined; }; const normalizeTagText = (value: unknown) => String(value || '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, ' ') .trim(); const createTags = (values: unknown[], fallbackTags: string[] = ['lokal', 'geoseek']) => { const tags = values .flatMap((value) => normalizeTagText(value).split(/\s+/)) .filter((word) => word.length > 2 && !['dan', 'atau', 'yang', 'untuk', 'dari'].includes(word)); return Array.from(new Set([...tags, ...fallbackTags])).slice(0, 8); }; const getPublicPlaceArea = (place: PublicPlaceRow) => [place.city, place.province].filter(Boolean).join(', ') || 'Area sekitar'; const getPublicPlaceAddress = (place: PublicPlaceRow) => [place.address, place.city, place.province].filter(Boolean).join(', ') || 'Alamat mengikuti data lokasi'; const getPublicPlaceCategory = (place: PublicPlaceRow) => place.category?.name || 'UMKM Lokal'; const getPublicPlaceDescription = (place: PublicPlaceRow) => place.short_description || place.full_description || 'Bisnis lokal dari database GeoSeek.'; const getPublicPlaceType = (place: PublicPlaceRow): GeoSeekItemType => { const haystack = normalizeTagText([ place.name, place.category?.name, place.category?.slug, place.category?.description, place.short_description, place.full_description, ].join(' ')); if (/kuliner|cafe|kopi|restoran|warung|kedai|makan|food/.test(haystack)) return 'culinary'; if (/kesehatan|apotek|klinik|herbal|obat|dokter|health/.test(haystack)) return 'health'; if (/bengkel|otomotif|mobil|motor|servis|service|auto/.test(haystack)) return 'automotive'; if (/properti|ruko|rumah|tanah|kios|property/.test(haystack)) return 'property'; if (/wisata|hotel|travel|tour/.test(haystack)) return 'tourism'; if (/event|bazar|agenda/.test(haystack)) return 'event'; return 'business'; }; const getOfferingType = (offering: PublicPlaceOffering): GeoSeekItemType => (offering.offering_type === 'product' ? 'product' : 'service'); const getOfferingStock = (offering: PublicPlaceOffering) => { const quantity = toOptionalNumber(offering.stock_quantity); if (quantity !== undefined) return Math.round(quantity); if (offering.stock_status === 'limited') return 8; if (offering.stock_status === 'in_stock') return 24; return undefined; }; const getApiActivityScore = (place: PublicPlaceRow, offering?: PublicPlaceOffering) => { const geoScore = toOptionalNumber(place.geo_score); const baseScore = geoScore ?? (place.is_verified ? 86 : 72); const verifiedBonus = offering?.is_verified ? 8 : 0; return Math.max(45, Math.min(100, Math.round(baseScore + verifiedBonus))); }; const publicPlaceToGeoSeekItems = (place: PublicPlaceRow): GeoSeekItem[] => { const placeId = place.id || `place-${place.name || 'unknown'}`; const category = getPublicPlaceCategory(place); const distanceKm = toOptionalNumber(place.distance_km) ?? 0; const rating = toOptionalNumber(place.rating_average) ?? 4.2; const reviews = Math.round(toOptionalNumber(place.rating_count) ?? 0); const latitude = toOptionalNumber(place.latitude) ?? backendDemoGeoSeekLocation.latitude; const longitude = toOptionalNumber(place.longitude) ?? backendDemoGeoSeekLocation.longitude; const offerings = Array.isArray(place.offerings) ? place.offerings : []; const hasProducts = offerings.some((offering) => offering.offering_type === 'product'); const hasServices = offerings.some((offering) => offering.offering_type === 'service'); const open = place.live_status?.status !== 'closed'; const baseTags = createTags([ place.name, category, place.category?.slug, place.short_description, place.full_description, offerings.map((offering) => offering.name).join(' '), ], ['backend', 'umkm', 'lokal']); const businessItem: GeoSeekItem = { id: `api-place-${placeId}`, type: getPublicPlaceType(place), name: place.name || 'Bisnis Lokal GeoSeek', businessName: place.name || 'Bisnis Lokal GeoSeek', category, description: getPublicPlaceDescription(place), address: getPublicPlaceAddress(place), area: getPublicPlaceArea(place), latitude, longitude, distanceKm, price: toOptionalNumber(place.average_price), rating, reviews, activityScore: getApiActivityScore(place), tags: baseTags, open, promo: place.is_verified ? 'Bisnis terverifikasi di data GeoSeek' : undefined, etaMinutes: hasServices ? Math.max(12, Math.min(60, Math.round(distanceKm * 5 + 15))) : undefined, bookingAvailable: hasServices, deliveryAvailable: hasProducts, }; const offeringItems = offerings.map((offering, index): GeoSeekItem => { const type = getOfferingType(offering); const stock = getOfferingStock(offering); const stockLabel = offering.stock_label || offering.stock_status || 'Info stok tersedia'; return { id: `api-offering-${placeId}-${offering.id || index}`, type, name: offering.name || (type === 'product' ? 'Produk lokal' : 'Jasa lokal'), businessName: businessItem.businessName, category: `${typeLabels[type]} • ${category}`, description: offering.description || getPublicPlaceDescription(place), address: businessItem.address, area: businessItem.area, latitude, longitude, distanceKm, price: toOptionalNumber(offering.price) ?? toOptionalNumber(place.average_price), stock, rating, reviews, activityScore: getApiActivityScore(place, offering), tags: createTags([offering.name, offering.description, category, stockLabel, place.name], [type, 'backend', 'lokal']), open, promo: offering.stock_status === 'limited' ? `Stok terbatas: ${stockLabel}` : undefined, etaMinutes: type === 'service' ? Math.max(12, Math.min(60, Math.round(distanceKm * 5 + 15))) : undefined, bookingAvailable: type === 'service', deliveryAvailable: type === 'product', }; }); return [businessItem, ...offeringItems]; }; const normalizePublicPlaceItems = (rows: PublicPlaceRow[]) => { const seen = new Set(); return rows.flatMap(publicPlaceToGeoSeekItems).filter((item) => { if (seen.has(item.id)) return false; seen.add(item.id); return true; }); }; const moduleIconMap: Partial> = { home: icon.mdiViewDashboardOutline, search: icon.mdiMagnify, map: icon.mdiMapSearchOutline, products: icon.mdiShoppingOutline, services: icon.mdiBriefcaseSearchOutline, umkm: icon.mdiStore, culinary: icon.mdiFoodForkDrink, health: icon.mdiHospitalBoxOutline, property: icon.mdiHomeCityOutline, automotive: icon.mdiCarWrench, tourism: icon.mdiBeach, events: icon.mdiCalendar, promos: icon.mdiTicketPercentOutline, marketplace: icon.mdiCart, booking: icon.mdiCalendar, courier: icon.mdiTruck, 'business-dashboard': icon.mdiFinance, profile: icon.mdiAccountCircle, }; const moduleOptions = geoSeekModules.map((module) => ({ href: module.key === 'home' ? '/geoseek' : `/geoseek/${module.key}`, key: module.key, label: module.menuLabel, iconPath: moduleIconMap[module.key] || icon.mdiTable, })); const quickPrompts = [ 'pupuk organik stok dekat pasar', 'nasi goreng promo antar', 'servis ac booking cepat', 'tambal ban buka sekarang', 'apotek vitamin 24 jam', 'gratis ongkir radius 3 km', ]; const defaultQueryByModule: Partial> = { home: 'pupuk organik', search: 'pupuk organik', map: '', products: '', services: '', umkm: '', culinary: '', health: '', property: '', automotive: '', tourism: '', events: '', promos: '', marketplace: '', booking: '', courier: '', 'business-dashboard': '', profile: '', }; const getDefaultQuery = (moduleKey: GeoSeekModuleKey) => defaultQueryByModule[moduleKey] ?? ''; const localPublishedItemsKey = 'geoseek-pro-published-items'; const localActionHistoryKey = 'geoseek-pro-action-history'; const getActionTimestamp = () => new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium', timeStyle: 'short', }).format(new Date()); const getDraftItemType = (category: string, moduleKey: GeoSeekModuleKey): GeoSeekItemType => { const normalizedCategory = category.toLowerCase(); if (moduleKey === 'map') return 'business'; if (moduleKey === 'products' || /produk|pertanian|tani|stok/.test(normalizedCategory)) return 'product'; if (moduleKey === 'culinary' || /kuliner|makan|minum|warung/.test(normalizedCategory)) return 'culinary'; if (moduleKey === 'health' || /kesehatan|obat|klinik|apotek/.test(normalizedCategory)) return 'health'; if (moduleKey === 'property' || /properti|ruko|rumah|tanah|kios/.test(normalizedCategory)) return 'property'; if (moduleKey === 'automotive' || /otomotif|bengkel|mobil|motor/.test(normalizedCategory)) return 'automotive'; if (moduleKey === 'tourism' || /wisata/.test(normalizedCategory)) return 'tourism'; if (moduleKey === 'events' || /event|bazar|agenda/.test(normalizedCategory)) return 'event'; if (moduleKey === 'promos' || /promo|diskon|voucher/.test(normalizedCategory)) return 'promo'; if (moduleKey === 'courier' || /kurir|antar|kirim|ongkir/.test(normalizedCategory)) return 'courier'; if (moduleKey === 'services' || moduleKey === 'booking' || /jasa|servis|booking/.test(normalizedCategory)) return 'service'; return 'business'; }; const createPublishedItem = ( input: string, draft: GeoSeekSmartDraft, moduleKey: GeoSeekModuleKey, origin: { latitude: number; longitude: number } = defaultGeoSeekLocation, ): GeoSeekItem => { const type = getDraftItemType(draft.category, moduleKey); const distanceKm = Math.min(2.8, Number((0.7 + (draft.itemName.length % 12) / 10).toFixed(1))); const supportsBooking = ['service', 'health', 'property', 'tourism', 'culinary'].includes(type); const supportsDelivery = ['product', 'culinary', 'promo', 'courier'].includes(type); return { id: `gsk-local-${Date.now()}`, type, name: draft.itemName || draft.businessName, businessName: draft.businessName, category: draft.category, description: `Item lokal dari Smart Input: ${input}`, address: draft.location, area: draft.location, latitude: origin.latitude + distanceKm / 1000, longitude: origin.longitude + distanceKm / 900, distanceKm, price: draft.price, stock: draft.stock, rating: 4.8, reviews: 1, activityScore: 95, tags: Array.from(new Set([...draft.tags, 'smart-input', 'lokal'])), open: draft.status !== 'Perlu review manual', promo: draft.promo.startsWith('Promo aktif') ? draft.promo : undefined, etaMinutes: supportsBooking || type === 'courier' ? 18 : undefined, bookingAvailable: supportsBooking, deliveryAvailable: supportsDelivery, }; }; const sampleSmartInputs = [ 'Warung Sari Rasa promo nasi goreng Rp15000 stok 42 dekat Alun-Alun buka sampai malam', 'Toko Tani Makmur menjual pupuk organik Rp38000 stok 64 dekat Pasar Tani gratis antar radius 2 km', 'Jaya Teknik buka jasa servis AC panggilan Rp85000 area Melati booking hari ini', ]; const getModule = (moduleKey?: string) => { const matchedModule = geoSeekModules.find((module) => module.key === moduleKey); return matchedModule || geoSeekModules[0]; }; const getActionSet = (moduleKey: GeoSeekModuleKey): QuickAction[] => { const baseActions: QuickAction[] = [ { label: 'Hitung GeoScore', message: 'GeoScore otomatis dihitung ulang dari jarak, relevansi, rating, dan aktivitas.', color: 'info', iconPath: icon.mdiChartTimelineVariant, }, { label: 'Buat Rute', message: 'Rute demo disiapkan dari lokasi pengguna ke hasil prioritas terdekat.', color: 'success', iconPath: icon.mdiMapMarker, }, ]; const moduleActions: Partial> = { products: [ { label: 'Auto Restock', message: 'Sistem menandai produk stok rendah dan membuat rekomendasi restock.', color: 'warning', iconPath: icon.mdiPackageVariantClosed, }, ], services: [ { label: 'Booking Jasa', message: 'Draft booking jasa dibuat dengan ETA dan penyedia terdekat.', color: 'success', iconPath: icon.mdiCalendar, }, ], marketplace: [ { label: 'Checkout Otomatis', message: 'Keranjang, ongkir, diskon, dan metode bayar simulasi berhasil disiapkan.', color: 'success', iconPath: icon.mdiCart, }, ], booking: [ { label: 'Buat Jadwal', message: 'Slot booking demo dibuat berdasarkan kategori, jarak, dan status buka.', color: 'success', iconPath: icon.mdiClockOutline, }, ], courier: [ { label: 'Dispatch Kurir', message: 'Kurir terdekat dipilih otomatis dengan ETA dan estimasi ongkir.', color: 'success', iconPath: icon.mdiTruck, }, ], promos: [ { label: 'Sebar Promo', message: 'Promo siap dikirim ke pengguna dalam radius yang dipilih.', color: 'warning', iconPath: icon.mdiTicketPercentOutline, }, ], 'business-dashboard': [ { label: 'Buat Insight', message: 'Insight stok, promo, booking, dan peluang bisnis berhasil diperbarui.', color: 'contrast', iconPath: icon.mdiFinance, }, ], }; return [...baseActions, ...(moduleActions[moduleKey] || [])]; }; const getItemActionLabel = (moduleKey: GeoSeekModuleKey, item: GeoSeekScoredItem) => { if (moduleKey === 'marketplace' || item.type === 'product' || item.type === 'culinary') return 'Tambah ke Keranjang'; if (moduleKey === 'booking' || item.bookingAvailable) return 'Booking Otomatis'; if (moduleKey === 'courier' || item.type === 'courier') return 'Pilih Kurir'; if (moduleKey === 'map') return 'Buka Rute'; if (moduleKey === 'promos' || item.type === 'promo') return 'Klaim Promo'; return 'Aktifkan Otomasi'; }; const getMapPosition = (item: GeoSeekScoredItem, index: number, origin: { latitude: number; longitude: number } = defaultGeoSeekLocation) => { const left = Math.min(88, Math.max(8, 50 + (item.longitude - origin.longitude) * 1800 + index * 2)); const top = Math.min(82, Math.max(10, 50 + (item.latitude - origin.latitude) * -1800 + index * 3)); return { left: `${left}%`, top: `${top}%` }; }; const StatCard = ({ label, value, help, iconPath }: { label: string; value: string | number; help: string; iconPath: string }) => (

{label}

{value}

{help}

); const ResultCard = ({ item, moduleKey, onAction, }: { item: GeoSeekScoredItem; moduleKey: GeoSeekModuleKey; onAction: (message: string) => void; }) => (
{typeLabels[item.type]} GeoScore {item.geoScore} {item.distanceKm.toFixed(1)} km • {item.distancePriority.label} {item.open && ( Buka )}

{item.name}

{item.businessName} • {item.category}

{item.description}

Lokasi: {item.address}, {item.area}
Rating: {item.rating.toFixed(1)} / 5 • {item.reviews} ulasan
Harga: {currency(item.price)}
Stok/ETA:{' '} {typeof item.stock === 'number' ? `${item.stock} tersedia` : item.etaMinutes ? `${item.etaMinutes} menit` : 'Tersedia sesuai permintaan'}
{item.promo && (
{item.promo}
)}
{item.tags.slice(0, 6).map((tag) => ( #{tag} ))}

Rincian GeoScore

Prioritas jarak{item.distancePriority.label}
Jarak 60%{item.distanceScore}
Relevansi 20%{item.relevanceScore}
Rating 10%{item.ratingScore}
Aktivitas 10%{item.activityContribution}
{item.automationNotes.map((note) => (
✓ {note}
))}
onAction(`${getItemActionLabel(moduleKey, item)} untuk “${item.name}” berhasil dibuat sebagai simulasi otomatis.`)} />
); export default function GeoSeekProWorkspace({ moduleKey = 'home' }: Props) { const normalizedModuleKey = getModule(moduleKey).key; const activeModule = getModule(normalizedModuleKey); const [query, setQuery] = useState(getDefaultQuery(activeModule.key)); const [radiusKm, setRadiusKm] = useState(5); const [smartInput, setSmartInput] = useState(sampleSmartInputs[0]); const [actionStatus, setActionStatus] = useState('Sistem otomasi siap digunakan. Pilih aksi cepat atau jalankan Smart Input.'); const [publishedItems, setPublishedItems] = useState([]); const [actionHistory, setActionHistory] = useState([]); const [isLocalStateReady, setIsLocalStateReady] = useState(false); const [resolvedLocation, setResolvedLocation] = useState(backendDemoGeoSeekLocation); const [isResolvingLocation, setIsResolvingLocation] = useState(true); const [locationStatus, setLocationStatus] = useState('Mengambil lokasi browser untuk pencarian radius GeoSeek...'); const [apiItems, setApiItems] = useState([]); const [isLoadingPlaces, setIsLoadingPlaces] = useState(false); const [apiError, setApiError] = useState(''); const [apiMeta, setApiMeta] = useState(emptyApiMeta); const [searchRequestVersion, setSearchRequestVersion] = useState(0); const { currentUser } = useAppSelector((state) => state.auth); const activeDistancePriority = useMemo(() => getDistancePriority(radiusKm), [radiusKm]); const hasBackendItemsForModule = useMemo( () => apiItems.some((item) => activeModule.includeTypes.includes(item.type)), [activeModule.includeTypes, apiItems], ); const liveExtraItems = useMemo(() => [...apiItems, ...publishedItems], [apiItems, publishedItems]); const results = useMemo( () => filterGeoSeekItems({ query, radiusKm, includeTypes: activeModule.includeTypes, moduleKey: activeModule.key, extraItems: liveExtraItems, baseItems: hasBackendItemsForModule ? [] : undefined, }), [activeModule.includeTypes, activeModule.key, hasBackendItemsForModule, liveExtraItems, query, radiusKm], ); const allModuleResults = useMemo( () => filterGeoSeekItems({ query: '', radiusKm: 20, includeTypes: activeModule.includeTypes, moduleKey: activeModule.key, extraItems: liveExtraItems, baseItems: hasBackendItemsForModule ? [] : undefined, }), [activeModule.includeTypes, activeModule.key, hasBackendItemsForModule, liveExtraItems], ); const smartDraft = useMemo(() => createSmartDraft(smartInput), [smartInput]); const insight = useMemo(() => getBusinessInsights(allModuleResults), [allModuleResults]); const cartItems = useMemo(() => createCartFromResults(results), [results]); const checkout = useMemo(() => getCheckoutSummary(cartItems), [cartItems]); const actionSet = getActionSet(activeModule.key); const mapItems = (results.length ? results : allModuleResults).slice(0, 8); const requestCurrentLocation = useCallback(() => { setIsResolvingLocation(true); setLocationStatus('Mengambil lokasi browser untuk pencarian radius GeoSeek...'); if (typeof navigator === 'undefined' || !navigator.geolocation) { setResolvedLocation(backendDemoGeoSeekLocation); setLocationStatus('Browser tidak mendukung GPS. GeoSeek memakai lokasi demo backend Pekanbaru.'); setIsResolvingLocation(false); return; } navigator.geolocation.getCurrentPosition( (position) => { setResolvedLocation({ label: 'Lokasi browser Anda', latitude: position.coords.latitude, longitude: position.coords.longitude, source: 'browser', accuracyMeters: Math.round(position.coords.accuracy), }); setLocationStatus('GPS aktif. Hasil GeoSeek dihitung dari lokasi browser Anda.'); setIsResolvingLocation(false); }, (error) => { console.warn('GeoSeek tidak mendapat izin/lokasi browser, memakai fallback demo backend:', error); setResolvedLocation(backendDemoGeoSeekLocation); setLocationStatus('GPS belum diizinkan. GeoSeek memakai lokasi demo backend Pekanbaru agar data nyata tetap tampil.'); setIsResolvingLocation(false); }, { enableHighAccuracy: true, maximumAge: 60000, timeout: 8000, }, ); }, []); const recordAction = (message: string, label = activeModule.menuLabel) => { setActionStatus(message); setActionHistory((previousHistory) => [ { id: `gsk-action-${Date.now()}`, moduleKey: activeModule.key, label, message, createdAt: getActionTimestamp(), }, ...previousHistory, ].slice(0, 8)); }; const publishSmartDraft = () => { const newItem = createPublishedItem(smartInput, smartDraft, activeModule.key, resolvedLocation); setPublishedItems((previousItems) => [ newItem, ...previousItems.filter((item) => item.name.toLowerCase() !== newItem.name.toLowerCase()), ].slice(0, 20)); setQuery(''); setRadiusKm((currentRadius) => Math.max(currentRadius, Math.ceil(newItem.distanceKm))); recordAction(`Draft “${newItem.name}” dipublikasikan sebagai ${typeLabels[newItem.type]} lokal dan langsung masuk hasil GeoSeek.`, 'Publikasi Smart Input'); }; useEffect(() => { try { const storedItems = window.localStorage.getItem(localPublishedItemsKey); const storedHistory = window.localStorage.getItem(localActionHistoryKey); if (storedItems) { const parsedItems = JSON.parse(storedItems); if (Array.isArray(parsedItems)) { setPublishedItems(parsedItems); } else { console.error('Data item lokal GeoSeek bukan array:', parsedItems); setActionStatus('Data item lokal tidak valid, data demo tetap digunakan.'); } } if (storedHistory) { const parsedHistory = JSON.parse(storedHistory); if (Array.isArray(parsedHistory)) { setActionHistory(parsedHistory); } else { console.error('Data riwayat GeoSeek bukan array:', parsedHistory); setActionStatus('Data riwayat lokal tidak valid, riwayat baru tetap bisa dibuat.'); } } } catch (error) { console.error('Gagal memuat data lokal GeoSeek:', error); setActionStatus('Gagal memuat data lokal GeoSeek. Data demo tetap bisa digunakan.'); } finally { setIsLocalStateReady(true); } }, []); useEffect(() => { if (!isLocalStateReady) return; try { window.localStorage.setItem(localPublishedItemsKey, JSON.stringify(publishedItems)); window.localStorage.setItem(localActionHistoryKey, JSON.stringify(actionHistory)); } catch (error) { console.error('Gagal menyimpan data lokal GeoSeek:', error); setActionStatus('Gagal menyimpan data lokal GeoSeek di browser.'); } }, [actionHistory, isLocalStateReady, publishedItems]); useEffect(() => { requestCurrentLocation(); }, [requestCurrentLocation]); useEffect(() => { let isActive = true; const timeoutId = window.setTimeout(async () => { setIsLoadingPlaces(true); setApiError(''); try { const apiQuery = getApiQuery(query, activeModule.key); const response = await axios.get('/public/places', { params: { q: apiQuery, lat: resolvedLocation.latitude, lng: resolvedLocation.longitude, radiusKm, limit: 36, }, }); if (!isActive) return; const rows = Array.isArray(response.data?.rows) ? response.data.rows : []; const externalSourceErrors = Array.isArray(response.data?.external_source_errors) ? response.data.external_source_errors : []; const radiusZoneParts = [response.data?.radius_zone?.label, response.data?.radius_zone?.range].filter(Boolean); setApiItems(normalizePublicPlaceItems(rows)); setApiMeta({ count: Number(response.data?.count || rows.length), totalCandidates: Number(response.data?.total_candidates || rows.length), radiusZoneLabel: radiusZoneParts.join(' • ') || 'Radius aktif', externalSources: Array.isArray(response.data?.external_sources) ? response.data.external_sources : [], externalSourceErrors, filteredByRadius: Boolean(response.data?.filtered_by_radius), expandedForNearest: Boolean(response.data?.expanded_for_nearest), }); setApiError(externalSourceErrors.length ? externalSourceErrors.join(' ') : ''); } catch (error) { if (!isActive) return; console.error('Gagal memuat data backend GeoSeek Pro:', error); setApiItems([]); setApiMeta(emptyApiMeta); setApiError('Data backend GeoSeek belum bisa dimuat. UI memakai fallback demo lokal.'); } finally { if (isActive) { setIsLoadingPlaces(false); } } }, 350); return () => { isActive = false; window.clearTimeout(timeoutId); }; }, [activeModule.key, query, radiusKm, resolvedLocation.latitude, resolvedLocation.longitude, searchRequestVersion]); useEffect(() => { setQuery(getDefaultQuery(activeModule.key)); setRadiusKm(5); setActionStatus(`Menu ${activeModule.menuLabel} siap. GeoSeek memuat data backend nyata dan memakai fallback demo jika data sekitar belum tersedia.`); }, [activeModule.key, activeModule.menuLabel]); const profileName = [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') || currentUser?.email || 'Pengguna GeoSeek'; return ( <> {getPageTitle(activeModule.title)}

GeoSeek Pro Automation

{activeModule.subtitle}

Lokasi aktif: {resolvedLocation.label}. GeoSeek kini mengambil data nyata dari backend `/public/places`, memakai GPS browser bila diizinkan, dan tetap punya fallback demo agar pencarian radius, produk, jasa, UMKM, peta, booking, kurir, dan insight bisnis selalu bisa dicoba.

{quickPrompts.slice(0, 4).map((prompt) => ( ))}

Profil aktif

{profileName}

Radius aktif {radiusKm} km ({activeDistancePriority.label}) dari {resolvedLocation.label}

{resolvedLocation.source === 'browser' ? 'Sumber: GPS browser' : 'Sumber: fallback backend demo'}{resolvedLocation.accuracyMeters ? ` • akurasi ±${resolvedLocation.accuracyMeters} m` : ''}

{results.length} hasil untuk modul {activeModule.menuLabel}

{apiItems.length ? `${apiItems.length} item backend dinormalisasi` : 'Fallback demo lokal aktif'}

Kandidat backend {apiMeta.totalCandidates} • draft lokal {publishedItems.length}

{apiMeta.radiusZoneLabel}

Status: {isLoadingPlaces ? 'Memuat data backend GeoSeek...' : actionStatus}
{moduleOptions.map((option) => { const isActive = option.key === activeModule.key; return ( {option.label} ); })}
{ setSearchRequestVersion((version) => version + 1); recordAction(`Pencarian “${query || getApiQuery(query, activeModule.key)}” dikirim ke backend GeoSeek dan diurutkan dengan GeoScore.`, 'Pencarian'); }} />
Lokasi: {locationStatus}
Data: {isLoadingPlaces ? 'memuat backend...' : apiItems.length ? `${apiItems.length} item backend aktif` : 'fallback demo aktif'}
Zona: {apiMeta.radiusZoneLabel}
{(apiError || apiMeta.externalSources.length > 0 || apiMeta.expandedForNearest) && (
{apiError || `Sumber eksternal aktif: ${apiMeta.externalSources.join(', ')}`} {apiMeta.expandedForNearest ? ' Radius diperluas otomatis untuk mencari hasil terdekat.' : ''}
)}

{activeModule.automationTitle}

{activeModule.automationDescription}

{actionSet.map((action) => ( recordAction(action.message, action.label)} /> ))}
Status otomasi: {actionStatus}

Riwayat aksi lokal

Order, booking, dispatch, dan publikasi tersimpan di browser.

{ setActionHistory([]); setPublishedItems([]); setActionStatus('Data lokal GeoSeek dibersihkan. Data demo bawaan tetap tersedia.'); }} />
{actionHistory.length ? actionHistory.slice(0, 4).map((history) => (
{history.label} {history.createdAt}

{history.message}

)) : (

Belum ada aksi. Jalankan pencarian, publish draft, checkout, booking, atau kurir.

)}

Smart Input / Auto Input

Tulis laporan bebas, sistem membuat draft data terstruktur.