Hướng dẫn tích hợp React SDK
Hướng dẫn tích hợp Dashboard GPS Tracking vào ứng dụng React web.
Package: @vietmap/tracking-sdk-react
SDK này sử dụng VietMap Maps APIs để hỗ trợ các tính năng liên quan đến bản đồ: hiển thị tilemap, vẽ lộ trình, tìm kiếm địa chỉ, dẫn đường,...
Yêu cầu
| Version | |
|---|---|
| React | ≥ 16.8 |
| TypeScript | ≥ 5.0 (khuyến nghị) |
| Node.js | ≥ 18 |
1. Cài đặt
npm install @vietmap/fleetwork-tracking-sdk-react
# hoặc
yarn add @vietmap/fleetwork-tracking-sdk-react
pnpm add @vietmap/fleetwork-tracking-sdk-reactPeer dependencies: react >= 16.8.0, react-dom >= 16.8.0
2. Provider Setup
Bọc ứng dụng bằng FleetworkProvider:
import { FleetworkProvider } from "@vietmap/fleetwork-tracking-sdk-react";
function App() {
return (
<FleetworkProvider
apiKey="YOUR_API_KEY"
baseUrl="https://live.fleetwork.vn" // tuỳ chọn
locale="vi" // 'vi' | 'en'
theme={{ colors: { primary: "#1677ff" } }} // tuỳ chọn
>
<YourApp />
</FleetworkProvider>
);
}Provider Props
| Prop | Type | Required | Default | Mô tả |
|---|---|---|---|---|
apiKey | string | Yes | — | API key xác thực. |
baseUrl | string | No | https://live.fleetwork.vn | Custom API URL. |
locale | 'vi' | 'en' | No | 'vi' | Ngôn ngữ hiển thị. |
theme | ThemeConfig | No | — | Tuỳ chỉnh theme. |
3. Cấp độ sử dụng
| Cấp độ | Mô tả | Khi nào dùng |
|---|---|---|
| Components | UI có sẵn, import và render | Nhanh, giao diện mặc định |
| Hooks | Trả về data + loading/error | Custom UI riêng |
| Controllers | Async functions thuần | Ngoài React (Redux, Zustand...) |
4. Components
4.1 Dashboard (All-in-One)
Render toàn bộ dashboard trong 1 component:
import { Dashboard } from "@vietmap/fleetwork-tracking-sdk-react";
<Dashboard />;Toggle từng widget:
<Dashboard
date={1713225600000}
showSummaryCards={true}
showMemberReport={true}
showActivityHeatmap={true}
showFuelTracking={true}
showMonthlyExpenses={true}
pollInterval={30000}
onError={(err) => console.error(err)}
/>| Prop | Type | Default | Mô tả |
|---|---|---|---|
date | number (Unix ms) | Đầu ngày hiện tại | Mốc ngày hiển thị cho dashboard. |
pollInterval | number | 30000 | Auto-refresh (ms). 0 = tắt. |
showSummaryCards | boolean | true | 4 thẻ KPI. |
showMemberReport | boolean | true | Bảng chi tiết nhân viên. |
showActivityHeatmap | boolean | true | Heatmap giờ hoạt động. |
showFuelTracking | boolean | true | Biểu đồ nhiên liệu. |
showMonthlyExpenses | boolean | true | Biểu đồ chi phí tháng. |
className | string | — | CSS class. |
style | CSSProperties | — | Inline style. |
onError | (error: Error) => void | — | Callback lỗi. |
4.2 Widget riêng lẻ
import {
SummaryCards,
MemberReport,
ActivityHeatmap,
FuelTracking,
MonthlyExpenses,
} from "@vietmap/fleetwork-tracking-sdk-react";| Component | Mô tả |
|---|---|
SummaryCards | 4 thẻ KPI: nhân viên active, km, thời gian, chi phí nhiên liệu. |
MemberReport | Bảng chi tiết nhân viên có phân trang. |
ActivityHeatmap | Heatmap ngày × giờ (Mon–Sun × 0h–23h). |
FuelTracking | Line chart: km vs lít tiêu thụ theo ngày/tuần/tháng. |
MonthlyExpenses | Stacked bar: chi phí tháng (nhiên liệu, bảo dưỡng, bảo hiểm, khác). |
Props chung:
| Prop | Type | Default | Mô tả |
|---|---|---|---|
date | number | Đầu ngày hiện tại | Unix timestamp (ms), áp dụng cho widget theo ngày. |
pollInterval | number | 30000 | Auto-refresh (ms). |
className | string | — | CSS class. |
style | CSSProperties | — | Inline style. |
onError | (error: Error) => void | — | Callback lỗi. |
onDataChange | (data: T) => void | — | Gọi khi data cập nhật. |
Props riêng:
| Component | Prop | Type | Default | Mô tả |
|---|---|---|---|---|
MemberReport | pageSize | number | 10 | Số hàng / trang. |
ActivityHeatmap | from, to | number | 14 ngày gần nhất | Khoảng thời gian hiển thị (Unix ms). |
FuelTracking | from, to, groupBy | number, number, string | Năm hiện tại | Khoảng thời gian và cách nhóm dữ liệu. |
MonthlyExpenses | from, to, currency | number, number, string | Năm hiện tại / VND | Khoảng thời gian và đơn vị tiền tệ. |
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
<FuelTracking from={1704067200000} to={1735689599000} groupBy="week" />
<MonthlyExpenses from={1704067200000} to={1735689599000} currency="VND" />
</div>4.3 LiveMap
Bản đồ real-time theo dõi vị trí nhân viên.
import { LiveMap } from "@vietmap/fleetwork-tracking-sdk-react";
import type { LiveMapRef } from "@vietmap/fleetwork-tracking-sdk-react";
const mapRef = useRef<LiveMapRef>(null);
<LiveMap
height="650px"
center={[106.6, 10.8]} // [lng, lat]
zoom={11}
defaultTile="terrain"
pollInterval={10000}
showList
showLegend
showTileSwitcher
ref={mapRef}
/>;Props
| Prop | Type | Default | Mô tả |
|---|---|---|---|
height | string | '100%' | Chiều cao container. |
center | [number, number] | [106.6, 10.8] | Tâm bản đồ [lng, lat]. |
zoom | number | 11 | Zoom mặc định. |
defaultTile | TileType | 'terrain' | 'light' / 'dark' / 'terrain' / 'satellite'. |
pollInterval | number | 10000 | Tự refresh vị trí (ms). |
showList | boolean | true | Panel danh sách nhân viên. |
showLegend | boolean | true | Chú thích trạng thái GPS. |
legendPosition | Position | 'top-right' | Vị trí chú thích. |
showTileSwitcher | boolean | true | Nút đổi tile. |
tileSwitcherPosition | Position | 'bottom-right' | Vị trí tile switcher. |
className | string | — | CSS class. |
style | CSSProperties | — | Inline style. |
Callbacks
<LiveMap
height="650px"
onMemberClick={(member) => {
console.log("Member clicked:", member.name);
return false; // return false để ngăn hành vi mặc định (focus + detail panel)
}}
onMarkerClick={(member) => {
router.push(`/drivers/${member.userId}`);
return false;
}}
onMapClick={(lngLat) => console.log("Map clicked:", lngLat)}
onMapReady={(map) => console.log("Map ready")}
/>| Callback | Type | Mô tả |
|---|---|---|
onMemberClick | (member: MemberStatus) => void | boolean | Click item danh sách. return false để ngăn default. |
onMarkerClick | (member: MemberStatus) => void | boolean | Click marker trên bản đồ. |
onMapClick | (lngLat: [number, number]) => void | Click nền bản đồ. |
onMapReady | (map: MapInstance) => void | Bản đồ sẵn sàng. |
Custom Rendering
<LiveMap
height="650px"
renderMemberItem={(member, defaultRender) => (
<div className="my-row" key={member.userId}>
<span>{member.name}</span>
<span className={`badge ${member.status}`}>{member.statusLabel}</span>
</div>
)}
renderMarkerPopup={(member) => (
<div className="my-popup">
<strong>{member.name}</strong>
<p>{member.lastAddress}</p>
</div>
)}
/>| Prop | Type | Mô tả |
|---|---|---|
renderMemberItem | (member: MemberStatus, defaultRender: ReactNode) => ReactNode | Custom item danh sách. |
renderMarkerPopup | (member: MemberStatus) => ReactNode | Custom popup marker. |
slotProps — Tuỳ chỉnh vị trí / style các thành phần bên trong
<LiveMap
height="650px"
slotProps={{
list: { position: "right", style: { width: 320 } },
legend: { position: "bottom-left" },
tileSwitcher: { position: "top-right" },
markers: { style: { filter: "hue-rotate(45deg)" } },
}}
/>| Slot | Props |
|---|---|
list | position?: 'left' | 'right', className?, style? |
legend | position?: Position, className?, style? |
tileSwitcher | position?: Position, className?, style? |
markers | className?, style? |
LiveMapRef — Điều khiển bản đồ qua ref
// Zoom đến tọa độ
mapRef.current?.flyTo([106.6, 10.8], 14);
// Fit bounds
mapRef.current?.fitBounds([
[105.8, 21.0],
[106.7, 10.7],
]);
// Focus 1 nhân viên (zoom + highlight)
mapRef.current?.focusMember("user-001");
// Lấy danh sách members hiện tại
const members = mapRef.current?.getMembers();
// Lấy MapInstance (VietMap GL JS)
const map = mapRef.current?.getMap();5. Hooks
Hook trả về { data, isLoading, error, refetch }.
Dashboard Hooks
import {
useSummaryCards,
useMemberReport,
useActivityHeatmap,
useFuelTracking,
useMonthlyExpenses,
} from "@vietmap/fleetwork-tracking-sdk-react";
// KPI overview — GET /api/v1/dashboard/gps-manager/summary
const { data, isLoading, error, refetch } = useSummaryCards({
date: 1713225600000,
pollInterval: 30000,
});
// Danh sách nhân viên — GET /api/v1/dashboard/gps-manager/employees
const { data } = useMemberReport({ date: 1713225600000, pageSize: 10 });
// Heatmap — GET /api/v1/dashboard/gps-manager/activity-heatmap
const { data } = useActivityHeatmap({ from: 1712793600000, to: 1713398399000 });
// Biểu đồ nhiên liệu — GET /api/v1/dashboard/gps-manager/fuel-tracking
const { data } = useFuelTracking({
from: 1704067200000,
to: 1735689599000,
groupBy: "week",
});
// Chi phí tháng — GET /api/v1/dashboard/gps-manager/monthly-costs
const { data } = useMonthlyExpenses({
from: 1704067200000,
to: 1735689599000,
currency: "VND",
});LiveMap Hooks
import {
useMembers,
useMember,
useHistoryRoute,
} from "@vietmap/fleetwork-tracking-sdk-react";
// Tất cả members — GET /api/v1/dashboard/gps-manager/employees (live refresh)
const { data } = useMembers({ pollInterval: 10000 });
// 1 member — GET /api/v1/gps-tracking/latest/users/{userId}
const { data } = useMember("user-001");
// Lịch sử hành trình — GET /api/v1/gps-tracking/history?VehicleId=...
const { data } = useHistoryRoute({
vehicleId: "vehicle-id",
startTime: 1713225600000, // Unix ms
endTime: 1713311999000,
});
// data: GpsPoint[]Common Hook Options
| Option | Type | Default | Mô tả |
|---|---|---|---|
date | number | Đầu ngày hiện tại | Unix timestamp (ms). |
from | number | — | Unix timestamp bắt đầu range. |
to | number | — | Unix timestamp kết thúc range. |
pollInterval | number | 0 | Auto-refresh (ms). 0 = tắt. |
enabled | boolean | true | false = bỏ qua fetch. |
6. Controllers
Dùng ngoài React, không phụ thuộc state.
Bên trong React (có
FleetworkProvider): controllers tự lấy apiKey từ Provider. Bên ngoài React: gọiinitFleetwork()trước.
import { initFleetwork } from "@vietmap/fleetwork-tracking-sdk-react";
initFleetwork({ apiKey: "YOUR_API_KEY" });DashboardController
import { DashboardController } from "@vietmap/fleetwork-tracking-sdk-react";
// Static methods — không cần instantiate
// GET /api/v1/dashboard/gps-manager/summary
const summary = await DashboardController.getSummaryCards();
const summary = await DashboardController.getSummaryCards({
date: 1713225600000,
});
// GET /api/v1/dashboard/gps-manager/employees
const report = await DashboardController.getMemberReport(1713225600000, {
pageSize: 10,
});
// GET /api/v1/dashboard/gps-manager/activity-heatmap
const heatmap = await DashboardController.getActivityHeatmap(
1712793600000,
1713398399000,
);
// GET /api/v1/dashboard/gps-manager/fuel-tracking
const fuel = await DashboardController.getFuelTracking(
1704067200000,
1735689599000,
{ groupBy: "week" },
);
// GET /api/v1/dashboard/gps-manager/monthly-costs
const expenses = await DashboardController.getMonthlyExpenses(
1704067200000,
1735689599000,
{ currency: "VND" },
);LiveMapController
import { LiveMapController } from "@vietmap/fleetwork-tracking-sdk-react";
// Static methods
// GET /api/v1/dashboard/gps-manager/employees
const members = await LiveMapController.getMembers();
const member = await LiveMapController.getMember("user-001");
// GET /api/v1/gps-tracking/latest/users/{userId}
const location = await LiveMapController.getLastLocation("user-001");
// GET /api/v1/gps-tracking/latest (tất cả members)
const locations = await LiveMapController.getAllLastLocations();
// GET /api/v1/gps-tracking/history?VehicleId=...&FromTime=...&ToTime=...
const route = await LiveMapController.getHistoryRoute(
"vehicle-id",
startTimeMs,
endTimeMs,
);Ví dụ với Zustand
import { create } from "zustand";
import { DashboardController } from "@vietmap/fleetwork-tracking-sdk-react";
import type { SummaryCardsData } from "@vietmap/fleetwork-tracking-sdk-react";
const useDashboardStore = create<{
summary: SummaryCardsData | null;
loading: boolean;
fetchSummary: () => Promise<void>;
}>((set) => ({
summary: null,
loading: false,
fetchSummary: async () => {
set({ loading: true });
const summary = await DashboardController.getSummaryCards();
set({ summary, loading: false });
},
}));Parallel Fetch
const [summary, report, heatmap] = await Promise.all([
DashboardController.getSummaryCards(),
DashboardController.getMemberReport(1713225600000),
DashboardController.getActivityHeatmap(1712966400000, 1713484799000),
]);7. TypeScript Types
import type {
// Config
ThemeConfig,
Position,
TileType,
// Dashboard
SummaryCardsData,
MemberReportData,
MemberRow,
ActivityHeatmapData,
FuelTrackingData,
MonthlyExpensesData,
// LiveMap
MemberStatus,
GpsPoint,
} from "@vietmap/fleetwork-tracking-sdk-react";// GET /api/v1/dashboard/gps-manager/summary
interface SummaryCardsData {
date: number; // Unix ms
activeEmployees: { active: number; total: number };
totalDistance: { value: number; unit: string };
totalTravelTime: { totalSeconds: number; formatted: string };
totalFuelCost: { value: number; currency: string; formatted: string };
generatedAt: number; // Unix ms
}
// GET /api/v1/dashboard/gps-manager/employees
interface MemberReportData {
date: number; // Unix ms
summary: {
total: number;
moving: number;
stopped: number;
signalLost: number;
};
members: MemberRow[]; // key: members (không phải employees)
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
}
interface MemberRow {
userId: string;
name: string;
avatarUrl?: string | null;
groupName?: string | null;
distance: { value: number; unit: string };
travelTime: { totalSeconds: number; formatted: string };
fuel: {
consumedLiters: number;
costVnd: number;
costFormatted: string;
efficiencyKmPerL: number;
};
status: "moving" | "stopped" | "signal_lost";
statusLabel: string;
lastLocation: {
lat: number;
lng: number;
address: string;
speed: number; // km/h
timestamp: number; // Unix ms
} | null;
lastSeenAt: number; // Unix ms
}
// GET /api/v1/dashboard/gps-manager/activity-heatmap
interface ActivityHeatmapData {
from: number;
to: number;
metric: "distance" | "points";
resolution: "hour" | "30min";
cells: {
dayOfWeek: string; // "Mon", ..., "Sun"
date: number; // Unix ms đầu ngày
hour: number; // 0–23
value: number;
label: string; // "60 km"
}[];
maxValue: number;
minValue: number;
totalCells: number;
}
// GET /api/v1/dashboard/gps-manager/fuel-tracking
interface FuelTrackingData {
from: number;
to: number;
groupBy: "day" | "week" | "month";
fuelEfficiency: { value: number; unit: string; trend: string };
series: {
period: string; // "2026-W01" | "2026-01" | "2026-04-01"
label: string;
distanceKm: number;
fuelLiters: number;
efficiencyKmPerL: number;
}[];
totals: {
totalDistanceKm: number;
totalFuelLiters: number;
avgEfficiencyKmPerL: number;
};
}
// GET /api/v1/dashboard/gps-manager/monthly-costs
interface MonthlyExpensesData {
from: number;
to: number;
currency: string; // "VND"
months: {
month: number; // 1–12
label: string; // "Jan", "Feb", ...
costs: {
fuel: number;
maintenance: number;
insurance: number;
other: number;
total: number;
};
}[];
totals: {
fuel: number;
maintenance: number;
insurance: number;
other: number;
grandTotal: number;
};
categories: { key: string; label: string; color: string }[];
}
// LiveMap member (GET /api/v1/dashboard/gps-manager/employees — 1 phần tử)
interface MemberStatus {
userId: string;
name: string;
avatarUrl?: string | null;
groupName?: string | null;
status: "moving" | "stopped" | "signal_lost";
statusLabel: string;
lat: number; // lastLocation.lat
lng: number; // lastLocation.lng
lastAddress?: string; // lastLocation.address
speed?: number; // lastLocation.speed (km/h)
lastSeenAt?: number; // Unix ms
}
// GET /api/v1/gps-tracking/history — 1 phần tử trong trackingData[]
interface GpsPoint {
lat: number;
lng: number;
speed: number; // km/h
heading: number; // 0–360
altitude?: number;
accuracy?: number;
timestamp: number; // Unix ms
}
type TileType = "light" | "dark" | "terrain" | "satellite";
type Position = "top-left" | "top-right" | "bottom-left" | "bottom-right";
interface ThemeConfig {
colors?: {
primary?: string;
success?: string;
warning?: string;
danger?: string;
background?: string;
text?: string;
border?: string;
};
borderRadius?: number;
fontFamily?: string;
}8. Error Handling
// Hooks — error object
const { data, error } = useSummaryCards();
if (error) return <div>Lỗi: {error.message}</div>;
// Components — onError callback
<Dashboard onError={(err) => console.error(err)} />;
// Controllers — throw errors
try {
const data = await DashboardController.getSummaryCards();
} catch (error) {
// error.status: 401 | 403 | 429 | 500
// error.message: string
}| Status | Ý nghĩa |
|---|---|
401 | API key không hợp lệ hoặc hết hạn. |
403 | Không có quyền truy cập. |
429 | Vượt giới hạn rate limit. |
500 | Lỗi server. |
9. Ví dụ tích hợp hoàn chỉnh
Dashboard mặc định
import {
FleetworkProvider,
Dashboard,
} from "@vietmap/fleetwork-tracking-sdk-react";
export default function DashboardPage() {
const today = new Date();
today.setHours(0, 0, 0, 0);
return (
<FleetworkProvider apiKey="YOUR_API_KEY" locale="vi">
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">GPS Dashboard</h1>
<Dashboard date={today.getTime()} />
</div>
</FleetworkProvider>
);
}Custom layout với Hooks
import {
useSummaryCards,
useMemberReport,
} from "@vietmap/fleetwork-tracking-sdk-react";
function CustomDashboard({ date }: { date: number }) {
const { data: summary, isLoading: s1 } = useSummaryCards({ date });
const { data: report, isLoading: s2 } = useMemberReport({
date,
pageSize: 5,
});
if (s1 || s2) return <p>Đang tải...</p>;
return (
<div>
<div style={{ display: "flex", gap: 16 }}>
<div>
{summary?.activeEmployees.active} / {summary?.activeEmployees.total}{" "}
nhân viên
</div>
<div>{summary?.totalDistance.value} km</div>
<div>{summary?.totalFuelCost.formatted}</div>
</div>
<table>
<tbody>
{report?.members.map((m) => (
<tr key={m.userId}>
<td>{m.name}</td>
<td>{m.distance.value} km</td>
<td>{m.statusLabel}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}LiveMap với Ref
import { useRef } from "react";
import {
FleetworkProvider,
LiveMap,
} from "@vietmap/fleetwork-tracking-sdk-react";
import type { LiveMapRef } from "@vietmap/fleetwork-tracking-sdk-react";
export default function MapPage() {
const mapRef = useRef<LiveMapRef>(null);
return (
<FleetworkProvider apiKey="YOUR_API_KEY">
<div style={{ padding: 8, display: "flex", gap: 8 }}>
<button
onClick={() =>
mapRef.current?.fitBounds([
[105.8, 21.0],
[106.7, 10.7],
])
}
>
Fit All
</button>
<button onClick={() => mapRef.current?.flyTo([106.6, 10.8], 12)}>
Fly to Hà Nội
</button>
</div>
<LiveMap
ref={mapRef}
height="calc(100vh - 48px)"
center={[106.6, 10.8]}
zoom={12}
pollInterval={10000}
onMarkerClick={(member) => {
alert(`${member.name} — ${member.statusLabel}`);
return false;
}}
renderMarkerPopup={(member) => (
<div style={{ minWidth: 180 }}>
<strong>{member.name}</strong>
<div>{member.lastAddress ?? "—"}</div>
<div>Tốc độ: {member.speed ?? 0} km/h</div>
</div>
)}
/>
</FleetworkProvider>
);
}