FleetWorkAPI Docs

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-react

Peer 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

PropTypeRequiredDefaultMô tả
apiKeystringYesAPI key xác thực.
baseUrlstringNohttps://live.fleetwork.vnCustom API URL.
locale'vi' | 'en'No'vi'Ngôn ngữ hiển thị.
themeThemeConfigNoTuỳ chỉnh theme.

3. Cấp độ sử dụng

Cấp độMô tảKhi nào dùng
ComponentsUI có sẵn, import và renderNhanh, giao diện mặc định
HooksTrả về data + loading/errorCustom UI riêng
ControllersAsync functions thuầnNgoà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)}
/>
PropTypeDefaultMô tả
datenumber (Unix ms)Đầu ngày hiện tạiMốc ngày hiển thị cho dashboard.
pollIntervalnumber30000Auto-refresh (ms). 0 = tắt.
showSummaryCardsbooleantrue4 thẻ KPI.
showMemberReportbooleantrueBảng chi tiết nhân viên.
showActivityHeatmapbooleantrueHeatmap giờ hoạt động.
showFuelTrackingbooleantrueBiểu đồ nhiên liệu.
showMonthlyExpensesbooleantrueBiểu đồ chi phí tháng.
classNamestringCSS class.
styleCSSPropertiesInline style.
onError(error: Error) => voidCallback lỗi.

4.2 Widget riêng lẻ

import {
  SummaryCards,
  MemberReport,
  ActivityHeatmap,
  FuelTracking,
  MonthlyExpenses,
} from "@vietmap/fleetwork-tracking-sdk-react";
ComponentMô tả
SummaryCards4 thẻ KPI: nhân viên active, km, thời gian, chi phí nhiên liệu.
MemberReportBảng chi tiết nhân viên có phân trang.
ActivityHeatmapHeatmap ngày × giờ (Mon–Sun × 0h–23h).
FuelTrackingLine chart: km vs lít tiêu thụ theo ngày/tuần/tháng.
MonthlyExpensesStacked bar: chi phí tháng (nhiên liệu, bảo dưỡng, bảo hiểm, khác).

Props chung:

PropTypeDefaultMô tả
datenumberĐầu ngày hiện tạiUnix timestamp (ms), áp dụng cho widget theo ngày.
pollIntervalnumber30000Auto-refresh (ms).
classNamestringCSS class.
styleCSSPropertiesInline style.
onError(error: Error) => voidCallback lỗi.
onDataChange(data: T) => voidGọi khi data cập nhật.

Props riêng:

ComponentPropTypeDefaultMô tả
MemberReportpageSizenumber10Số hàng / trang.
ActivityHeatmapfrom, tonumber14 ngày gần nhấtKhoảng thời gian hiển thị (Unix ms).
FuelTrackingfrom, to, groupBynumber, number, stringNăm hiện tạiKhoảng thời gian và cách nhóm dữ liệu.
MonthlyExpensesfrom, to, currencynumber, number, stringNăm hiện tại / VNDKhoả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

PropTypeDefaultMô tả
heightstring'100%'Chiều cao container.
center[number, number][106.6, 10.8]Tâm bản đồ [lng, lat].
zoomnumber11Zoom mặc định.
defaultTileTileType'terrain''light' / 'dark' / 'terrain' / 'satellite'.
pollIntervalnumber10000Tự refresh vị trí (ms).
showListbooleantruePanel danh sách nhân viên.
showLegendbooleantrueChú thích trạng thái GPS.
legendPositionPosition'top-right'Vị trí chú thích.
showTileSwitcherbooleantrueNút đổi tile.
tileSwitcherPositionPosition'bottom-right'Vị trí tile switcher.
classNamestringCSS class.
styleCSSPropertiesInline 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")}
/>
CallbackTypeMô tả
onMemberClick(member: MemberStatus) => void | booleanClick item danh sách. return false để ngăn default.
onMarkerClick(member: MemberStatus) => void | booleanClick marker trên bản đồ.
onMapClick(lngLat: [number, number]) => voidClick nền bản đồ.
onMapReady(map: MapInstance) => voidBả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>
  )}
/>
PropTypeMô tả
renderMemberItem(member: MemberStatus, defaultRender: ReactNode) => ReactNodeCustom item danh sách.
renderMarkerPopup(member: MemberStatus) => ReactNodeCustom 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)" } },
  }}
/>
SlotProps
listposition?: 'left' | 'right', className?, style?
legendposition?: Position, className?, style?
tileSwitcherposition?: Position, className?, style?
markersclassName?, 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

OptionTypeDefaultMô tả
datenumberĐầu ngày hiện tạiUnix timestamp (ms).
fromnumberUnix timestamp bắt đầu range.
tonumberUnix timestamp kết thúc range.
pollIntervalnumber0Auto-refresh (ms). 0 = tắt.
enabledbooleantruefalse = 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ọi initFleetwork() 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
401API key không hợp lệ hoặc hết hạn.
403Không có quyền truy cập.
429Vượt giới hạn rate limit.
500Lỗ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>
  );
}

On this page