dashboard page view done
This commit is contained in:
@ -1,408 +1,20 @@
|
||||
import { Container, KeenIcon, DefaultTooltip } from '@/components';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Chart, YearPicker } from './blocks';
|
||||
import moment from 'moment';
|
||||
import { useFetchCardData, useFetchChartData } from './hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useFetchYear } from './hooks/useFetchYear';
|
||||
import { get5LastYear } from '@/utils/Date';
|
||||
import { staticChartData } from './staticChart';
|
||||
import { Container } from '@/components';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import BalanceCard from './blocks/BalanceCard';
|
||||
import { getAuth } from '@/auth';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import TransactionValue from './blocks/TransactionValue';
|
||||
import TransactionPieChart from './blocks/TransactionPieChart';
|
||||
import MemberActivity from './blocks/MemberActivity';
|
||||
import BankSaldo from './blocks/BankSaldo';
|
||||
|
||||
// sum -> nominal, count-> total
|
||||
type CountType = 'sum' | 'count';
|
||||
type ChartType = 'line' | 'bar';
|
||||
type ChartLegend = 'true' | 'false';
|
||||
import SitesSatelliteMap from './blocks/SitesateliteMap';
|
||||
|
||||
const DashboardHomePage = () => {
|
||||
const selectYear = get5LastYear();
|
||||
const [initialYear, setInitialYear] = useState<string>('');
|
||||
const [selectedYear, setSelectedYear] = useState<string>('');
|
||||
const [count, setCount] = useState<CountType>('sum');
|
||||
const [chartType, setChartType] = useState<ChartType>('line');
|
||||
const [chartLegend, setChartLegend] = useState<ChartLegend>('true');
|
||||
const [dateRange, setDateRange] = useState<{ from: Date; to: Date }>({
|
||||
from: new Date(),
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
const getFirstDayOfMonth = () => {
|
||||
const now = new Date();
|
||||
return new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
};
|
||||
|
||||
const getToday = () => {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
const [fromDate, setFromDate] = useState(getFirstDayOfMonth());
|
||||
const [toDate, setToDate] = useState(getToday());
|
||||
|
||||
|
||||
const API_URL = apiConfig.api_dashboard;
|
||||
const { GetData } = useCallApi();
|
||||
|
||||
const API_URL_BANK = apiConfig.service_wallet;
|
||||
|
||||
const [responseStatisticCard, setResponseStatisticCard] = useState<any>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
const res = await GetData(`${API_URL}/card-statistic`, {});
|
||||
setResponseStatisticCard(res);
|
||||
};
|
||||
|
||||
const [bankaccount, setbankaccount] = useState<any>(null);
|
||||
|
||||
const fetchDataBankAccount = async () => {
|
||||
const res = await GetData(`${API_URL_BANK}/dashboard/balance/account/${getAuth()?.user?.customer?.id}`, {});
|
||||
setbankaccount(res);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataBankAccount();
|
||||
}, []);
|
||||
|
||||
const [responseGraphic, setresponseGraphic] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDataGraphic = async () => {
|
||||
const res = await GetData(`${API_URL}/cashin-vs-cashout`, {
|
||||
date_from: fromDate.toISOString(),
|
||||
date_to: toDate.toISOString()
|
||||
});
|
||||
setresponseGraphic(res);
|
||||
};
|
||||
|
||||
fetchDataGraphic();
|
||||
}, [fromDate, toDate]);
|
||||
|
||||
let staticChartDataApiFetch = [];
|
||||
|
||||
// Make map for date => { cashin: 0, cashout: 0 }
|
||||
const dataMap: Record<string, { cashin: number; cashout: number }> = {};
|
||||
const current = moment(fromDate);
|
||||
const end = moment(toDate);
|
||||
|
||||
// Inisialization dataMap with all dates
|
||||
while (current.isSameOrBefore(end, 'day')) {
|
||||
const dateStr = current.format('DD-MM-YYYY');
|
||||
dataMap[dateStr] = { cashin: 0, cashout: 0 };
|
||||
current.add(1, 'day');
|
||||
}
|
||||
|
||||
// Add cashin
|
||||
if (Array.isArray(responseGraphic?.data.total_cashin)) {
|
||||
responseGraphic.data.total_cashin.forEach((item: any) => {
|
||||
const date = moment(item.created_date).format('DD-MM-YYYY');
|
||||
// Only update cashin if it's not already set (meaning no value was added before)
|
||||
if (dataMap[date]) {
|
||||
dataMap[date].cashin = Number(item.total_count ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add cashout
|
||||
if (Array.isArray(responseGraphic?.data.total_cashout)) {
|
||||
responseGraphic.data.total_cashout.forEach((item: any) => {
|
||||
const date = moment(item.created_date).format('DD-MM-YYYY');
|
||||
// Only update cashout if it's not already set (meaning no value was added before)
|
||||
if (dataMap[date]) {
|
||||
dataMap[date].cashout = Number(item.total_count ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Change to array for chart
|
||||
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
||||
month,
|
||||
cashin: values.cashin,
|
||||
cashout: values.cashout
|
||||
}));
|
||||
|
||||
// Change to array for chart
|
||||
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
||||
month,
|
||||
cashin: values.cashin,
|
||||
cashout: values.cashout
|
||||
}));
|
||||
|
||||
const currentRole = getAuth()?.role_name;
|
||||
const idCustomer = getAuth()?.user.customer?.id;
|
||||
// console.log(currentRole);
|
||||
|
||||
// Menyusun tanggal awal dan akhir berdasarkan selectedYear
|
||||
useEffect(() => {
|
||||
if (selectYear.length > 0 && !selectedYear) {
|
||||
let latestYear = Math.max(...selectYear.map((item) => parseInt(item, 10))).toString();
|
||||
// console.log('latestYear :', latestYear);
|
||||
setSelectedYear(latestYear);
|
||||
setInitialYear(latestYear);
|
||||
}
|
||||
}, [selectYear, selectedYear]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedYear) {
|
||||
setDateRange({
|
||||
from: moment(`${selectedYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
||||
to: moment(`${selectedYear}-12-31`, 'YYYY-MM-DD').toDate()
|
||||
});
|
||||
}
|
||||
}, [selectedYear]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialYear) {
|
||||
setDateRange({
|
||||
from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
||||
to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate()
|
||||
});
|
||||
}
|
||||
}, [initialYear]);
|
||||
|
||||
const handleYearChange = (year: string) => {
|
||||
setSelectedYear(year);
|
||||
};
|
||||
|
||||
const handleCountType = (value: CountType) => {
|
||||
setCount(value);
|
||||
};
|
||||
|
||||
const handleChartType = (value: ChartType) => {
|
||||
setChartType(value);
|
||||
};
|
||||
|
||||
const handleChartLegend = (value: ChartLegend) => {
|
||||
setChartLegend(value);
|
||||
};
|
||||
|
||||
const resetFilter = useCallback(() => {
|
||||
const firstDay = getFirstDayOfMonth();
|
||||
const today = getToday();
|
||||
|
||||
setFromDate(firstDay);
|
||||
setToDate(today);
|
||||
|
||||
// reset filter yang lain kalau perlu
|
||||
setDateRange({ from: firstDay, to: today });
|
||||
setSelectedYear(initialYear);
|
||||
setCount('sum');
|
||||
setChartType('line');
|
||||
setChartLegend('true');
|
||||
}, [initialYear]);
|
||||
|
||||
const { cardData } = useFetchCardData(
|
||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
||||
count
|
||||
);
|
||||
|
||||
const { chartData } = useFetchChartData(
|
||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
||||
count
|
||||
);
|
||||
|
||||
const toolbar = (
|
||||
<div className="flex gap-3 items-center w-1/2">
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={chartType} onValueChange={handleChartType}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Chart Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="line">Line</SelectItem>
|
||||
<SelectItem value="bar">Bar</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={chartLegend} onValueChange={handleChartLegend}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Legend Visibility" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full">
|
||||
<SelectItem value="true">Show Legend</SelectItem>
|
||||
<SelectItem value="false">Hide Legend</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const number: number = responseStatisticCard?.data.total_cash_in ?? 0;
|
||||
const formattedNumber: number = parseFloat(number.toFixed(2));
|
||||
|
||||
const numbercashout: number = responseStatisticCard?.data.total_cash_out ?? 0;
|
||||
const formattedNumbercashout: number = parseFloat(numbercashout.toFixed(2));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TPAY | Dashboard</title>
|
||||
<title>REVENUE | Dashboard</title>
|
||||
</Helmet>
|
||||
<Container>
|
||||
{/* Account Balance Cards */}
|
||||
{/* {currentRole === 'Escrow' || currentRole === 'Master Agent' ? (
|
||||
<div className="grid gap-5 lg:gap-7.5 mb-14 mt-7">
|
||||
<BalanceCard id={idCustomer} />
|
||||
</div>
|
||||
) : null} */}
|
||||
|
||||
<div className="flex space-x-4 mt-5">
|
||||
{bankaccount?.data && bankaccount?.data.length > 0
|
||||
&& getAuth()?.statusbalance=='Y'
|
||||
? (
|
||||
bankaccount?.data.map((bankaccountdatas: { id_balance:string,amount: string, credit_limit: string, monthly_limit: string; wallet: string; }, index: number) => (
|
||||
<BankSaldo
|
||||
title={bankaccountdatas.wallet}
|
||||
balance={bankaccountdatas.amount}
|
||||
creditLimit={bankaccountdatas.credit_limit}
|
||||
monthlyLimit={bankaccountdatas.monthly_limit}
|
||||
idbalance={bankaccountdatas.id_balance}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<div className="mt-5">
|
||||
<SitesSatelliteMap />
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="flex gap-6 overflow-x-auto pb-2 mt-5">
|
||||
<Card
|
||||
title="Registered Users"
|
||||
total={responseStatisticCard?.data.total_registered ?? 0}
|
||||
growth={parseFloat((responseStatisticCard?.data?.registered_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
||||
surplus={responseStatisticCard?.data.registered_last_week.surplus ?? false}
|
||||
icon="test"
|
||||
/>
|
||||
<Card
|
||||
title="Unregistered Users"
|
||||
total={responseStatisticCard?.data.total_unregistered ?? 0}
|
||||
growth={parseFloat((responseStatisticCard?.data?.unregistered_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
||||
surplus={responseStatisticCard?.data.unregistered_last_week.surplus ?? false}
|
||||
icon="test"
|
||||
|
||||
/>
|
||||
<Card
|
||||
title="Total Cash-in"
|
||||
total={formattedNumber.toFixed(2)} // now a number: 972.80
|
||||
growth={parseFloat((responseStatisticCard?.data?.cash_in_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
||||
surplus={responseStatisticCard?.data.cash_in_last_week.surplus ?? false}
|
||||
icon="test"
|
||||
/>
|
||||
<Card
|
||||
title="Total Cash-out"
|
||||
total={formattedNumbercashout.toFixed(2)} // now a number: 972.80
|
||||
growth={parseFloat((responseStatisticCard?.data?.cash_out_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
||||
surplus={responseStatisticCard?.data.cash_out_last_week.surplus ?? false}
|
||||
icon="test"
|
||||
/>
|
||||
<Card
|
||||
title="Active Event"
|
||||
total={responseStatisticCard?.data.total_event ?? 0}
|
||||
growth={parseFloat((responseStatisticCard?.data?.event_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
||||
surplus={responseStatisticCard?.data.event_last_week.surplus ?? false}
|
||||
icon="test"
|
||||
/>
|
||||
<Card
|
||||
title="Total Billing"
|
||||
total={responseStatisticCard?.data.total_billing ?? 0}
|
||||
growth={parseFloat((responseStatisticCard?.data?.billing_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
||||
surplus={responseStatisticCard?.data.billing_last_week.surplus ?? false}
|
||||
icon='test'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 items-center mb-6 mt-6">
|
||||
<div className="flex gap-3 items-center w-full md:w-auto">
|
||||
<label className="input input-sm w-[160px]">
|
||||
From
|
||||
<input
|
||||
type="date"
|
||||
name="from"
|
||||
value={moment(fromDate).format('YYYY-MM-DD')}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
setFromDate(new Date(e.target.value));
|
||||
} else {
|
||||
// Reset to default value (e.g. first day of current month)
|
||||
setFromDate(getFirstDayOfMonth());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="input input-sm w-[160px]">
|
||||
To
|
||||
<input
|
||||
type="date"
|
||||
name="to"
|
||||
value={moment(toDate).format('YYYY-MM-DD')}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
setToDate(new Date(e.target.value));
|
||||
} else {
|
||||
// Reset to default value (e.g. today)
|
||||
setToDate(getToday());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DefaultTooltip title="Reset Filter" placement="top">
|
||||
<Button variant="outline" className="h-7.5" onClick={resetFilter}>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Chart */}
|
||||
<div className="grid gap-5 lg:gap-7.5 mt-5">
|
||||
<div className="grid lg:grid-cols-1 gap-y-5 lg:gap-5 items-stretch">
|
||||
<div className="lg:col-span-1">
|
||||
<Chart
|
||||
title="Overview"
|
||||
count={12}
|
||||
toolbar={toolbar}
|
||||
chartData={staticChartDataApiFetch}
|
||||
chartType={chartType}
|
||||
chartLegend={chartLegend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 mt-5">
|
||||
<TransactionValue startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
||||
<TransactionPieChart startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
||||
<MemberActivity startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
||||
</div>
|
||||
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardHomePage;
|
||||
export default DashboardHomePage;
|
||||
189
src/pages/dashboards/home/blocks/SitesateliteMap.tsx
Normal file
189
src/pages/dashboards/home/blocks/SitesateliteMap.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet.markercluster';
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||
import { sitePoints, SitePoint } from '../sitepoints';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Satellite map (Leaflet + Esri World Imagery, no API key required) with a
|
||||
// pin for every Cell/Sector row from the uploaded Excel
|
||||
// (Sites_Coordinates_untuk_CC.xlsm -> "site coordinates 4G").
|
||||
//
|
||||
// SETUP REQUIRED:
|
||||
// npm install leaflet leaflet.markercluster
|
||||
// npm install --save-dev @types/leaflet @types/leaflet.markercluster
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DILI_CENTER: [number, number] = [-8.5586, 125.5736];
|
||||
|
||||
// Groups points that share the exact same coordinates so a single pin can
|
||||
// list every cell/sector at that physical site.
|
||||
const groupByCoordinate = (points: SitePoint[]) => {
|
||||
const map = new Map<string, SitePoint[]>();
|
||||
points.forEach((p) => {
|
||||
const key = `${p.lat.toFixed(6)},${p.lng.toFixed(6)}`;
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(p);
|
||||
});
|
||||
return map;
|
||||
};
|
||||
|
||||
// Signal-tower badge icon: colored circle background + white "broadcast
|
||||
// tower" glyph, sized up slightly for pins that represent more than one
|
||||
// cell/sector at the same coordinates.
|
||||
const pinIcon = (multi: boolean) => {
|
||||
const size = multi ? 30 : 26;
|
||||
const bg = multi ? '#2563eb' : '#ef4444';
|
||||
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `
|
||||
<div style="
|
||||
width:${size}px;
|
||||
height:${size}px;
|
||||
border-radius:50%;
|
||||
background:${bg};
|
||||
border:2px solid white;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,0.5);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
">
|
||||
<svg width="${size * 0.62}" height="${size * 0.62}" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="21" x2="12" y2="10"></line>
|
||||
<circle cx="12" cy="7" r="2" fill="white" stroke="none"></circle>
|
||||
<path d="M8.5 10.5c0-2 1.5-3.5 3.5-3.5s3.5 1.5 3.5 3.5"></path>
|
||||
<path d="M5.5 12.5c0-4 3-6.5 6.5-6.5s6.5 2.5 6.5 6.5"></path>
|
||||
</svg>
|
||||
</div>`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
popupAnchor: [0, -size / 2]
|
||||
});
|
||||
};
|
||||
|
||||
const SitesSatelliteMap = () => {
|
||||
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapRef = useRef<L.Map | null>(null);
|
||||
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||
const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map());
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedCount, setSelectedCount] = useState(sitePoints.length);
|
||||
|
||||
const grouped = useMemo(() => groupByCoordinate(sitePoints), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapContainerRef.current || mapRef.current) return;
|
||||
|
||||
const map = L.map(mapContainerRef.current, {
|
||||
center: DILI_CENTER,
|
||||
zoom: 12,
|
||||
zoomControl: true
|
||||
});
|
||||
mapRef.current = map;
|
||||
|
||||
// Esri World Imagery — free satellite tiles, no API key required.
|
||||
L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{
|
||||
attribution:
|
||||
'Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community',
|
||||
maxZoom: 19
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
// Optional labels/roads overlay on top of the satellite imagery.
|
||||
L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
|
||||
{ maxZoom: 19, opacity: 0.9 }
|
||||
).addTo(map);
|
||||
|
||||
const cluster = L.markerClusterGroup({ maxClusterRadius: 50 });
|
||||
clusterRef.current = cluster;
|
||||
|
||||
grouped.forEach((points, key) => {
|
||||
const [lat, lng] = key.split(',').map(Number);
|
||||
const marker = L.marker([lat, lng], { icon: pinIcon(points.length > 1) });
|
||||
|
||||
const site = points[0].site;
|
||||
const location = points[0].location;
|
||||
const rows = points
|
||||
.map((p) => `<tr><td style="padding-right:8px;">${p.cell}</td><td>${p.sector}</td></tr>`)
|
||||
.join('');
|
||||
|
||||
marker.bindPopup(`
|
||||
<div style="font-family: Arial, sans-serif; font-size: 13px; max-width: 240px;">
|
||||
<div style="font-weight:600; margin-bottom:2px;">${site}</div>
|
||||
<div style="color:#666; margin-bottom:6px;">${location} · ${lat.toFixed(5)}, ${lng.toFixed(5)}</div>
|
||||
<table style="width:100%; border-collapse:collapse;">${rows}</table>
|
||||
</div>
|
||||
`);
|
||||
|
||||
markerByKeyRef.current.set(key, marker);
|
||||
cluster.addLayer(marker);
|
||||
});
|
||||
|
||||
map.addLayer(cluster);
|
||||
|
||||
return () => {
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
clusterRef.current = null;
|
||||
markerByKeyRef.current.clear();
|
||||
};
|
||||
}, [grouped]);
|
||||
|
||||
// Simple search: pans/zooms to a matching cell, sector, or site name and
|
||||
// opens its popup.
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
if (!value) {
|
||||
setSelectedCount(sitePoints.length);
|
||||
return;
|
||||
}
|
||||
const q = value.toLowerCase();
|
||||
const matches = sitePoints.filter(
|
||||
(p) =>
|
||||
p.cell.toLowerCase().includes(q) ||
|
||||
p.sector.toLowerCase().includes(q) ||
|
||||
p.site.toLowerCase().includes(q)
|
||||
);
|
||||
setSelectedCount(matches.length);
|
||||
|
||||
if (matches.length > 0 && mapRef.current) {
|
||||
const first = matches[0];
|
||||
const key = `${first.lat.toFixed(6)},${first.lng.toFixed(6)}`;
|
||||
mapRef.current.setView([first.lat, first.lng], 17);
|
||||
const marker = markerByKeyRef.current.get(key);
|
||||
if (marker && clusterRef.current) {
|
||||
clusterRef.current.zoomToShowLayer(marker, () => marker.openPopup());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
Menampilkan <span className="font-semibold">{selectedCount}</span> dari {sitePoints.length} titik
|
||||
(Cell/Sector)
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
placeholder="Cari Cell, Sector, atau nama Site..."
|
||||
className="input input-sm w-full sm:w-72"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full rounded-lg overflow-hidden border border-gray-200" style={{ height: '70vh' }}>
|
||||
<div ref={mapContainerRef} className="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SitesSatelliteMap;
|
||||
1161
src/pages/dashboards/home/sitepoints.ts
Normal file
1161
src/pages/dashboards/home/sitepoints.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user