Added map and dashboard
This commit is contained in:
108
lib/api.ts
108
lib/api.ts
@@ -0,0 +1,108 @@
|
||||
// lib/api.ts - API helpers with Zod validation for your Laravel endpoints
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export type DeviceCommandType =
|
||||
| "wifi"
|
||||
| "sleep"
|
||||
| "telemetry_sec"
|
||||
| "poll_sec"
|
||||
| "ota"
|
||||
| "ring_fence"
|
||||
| "lights"
|
||||
| "camera";
|
||||
|
||||
export interface DeviceCommand {
|
||||
id: number;
|
||||
type: DeviceCommandType;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TelemetryPost {
|
||||
recorded_at?: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
altitude_m?: number;
|
||||
speed_kmh?: number;
|
||||
heading_deg?: number;
|
||||
accuracy_m?: number;
|
||||
battery_percent?: number;
|
||||
is_car_on?: boolean;
|
||||
raw?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CommandReceiptPost {
|
||||
command_id: number;
|
||||
acked_at?: string;
|
||||
executed_at?: string;
|
||||
result?: "ok" | "error";
|
||||
result_detail?: string;
|
||||
}
|
||||
|
||||
// Zod schemas for validation
|
||||
const telemetrySchema = z.object({
|
||||
recorded_at: z.string().optional(),
|
||||
lat: z.number(),
|
||||
lng: z.number(),
|
||||
altitude_m: z.number().optional(),
|
||||
speed_kmh: z.number().optional(),
|
||||
heading_deg: z.number().optional(),
|
||||
accuracy_m: z.number().optional(),
|
||||
battery_percent: z.number().optional(),
|
||||
is_car_on: z.boolean().optional(),
|
||||
raw: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const receiptSchema = z.object({
|
||||
command_id: z.number(),
|
||||
acked_at: z.string().optional(),
|
||||
executed_at: z.string().optional(),
|
||||
result: z.enum(["ok", "error"]).optional(),
|
||||
result_detail: z.string().optional(),
|
||||
});
|
||||
|
||||
// API base from हुए env (add to .env: NEXT_PUBLIC_API_BASE=https://laravel-server.lab.audasmedia.com.au)
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "";
|
||||
|
||||
async function http<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: "GET",
|
||||
headers: { "Accept": "application/json", ...(init?.headers ?? {}) },
|
||||
cache: "no-store",
|
||||
...init,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
|
||||
}
|
||||
const text = await res.text();
|
||||
return (text ? JSON.parse(text) : null) as T;
|
||||
}
|
||||
|
||||
export async function postTelemetry(imei: string, body: TelemetryPost) {
|
||||
const validatedBody = telemetrySchema.parse(body);
|
||||
return http<{ ok: true }>(`/api/device/${imei}/telemetry`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(validatedBody),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommands(imei: string, since?: string) {
|
||||
const q = since ? `?since=${encodeURIComponent(since)}` : "";
|
||||
return http<DeviceCommand[]>(`/api/device/${imei}/commands${q}`);
|
||||
}
|
||||
|
||||
export async function postReceipt(imei: string, body: CommandReceiptPost) {
|
||||
const validatedBody = receiptSchema.parse(body);
|
||||
return http<{ ok: true }>(`/api/device/${imei}/command-receipts`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(validatedBody),
|
||||
});
|
||||
}
|
||||
|
||||
// Optional: Get latest GPS (adjust if your endpoint is /api/gps/latest-any)
|
||||
export async function getLatestGPS(imei: string) {
|
||||
return http<{ lat: number; lng: number; /* other fields */ }>(`/api/gps/latest-any?imei=${imei}`);
|
||||
}
|
||||
45
lib/map.tsx
Normal file
45
lib/map.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// lib/map.tsx - Custom叶 map component with Leaflet for GPS location and geo-fence drawing
|
||||
import { useState } from "react";
|
||||
import { MapContainer, TileLayer, Marker, Polyline, useMapEvents } from "react-leaflet";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Fix default Leaflet icon issue in React
|
||||
delete (L.Icon.Default as any)._getIconUrl;
|
||||
|
||||
interface MapProps {
|
||||
latestGPS?: { lat: number; lng: number };
|
||||
onFenceChange: (fence: Array<{ lat: number; lng: number }>) => void;
|
||||
}
|
||||
|
||||
const FenceMap = ({ latestGPS, onFenceChange }: MapProps) => {
|
||||
const [fence, setFence] = useState<Array<{ lat: number; lng: number }>>([]);
|
||||
|
||||
const map = useMapEvents({
|
||||
click: (e) => {
|
||||
const newFence = [...fence, { lat: e.latlng.lat, lng: e.latlng.lng }];
|
||||
setFence(newFence);
|
||||
onFenceChange(newFence); // Update parent
|
||||
},
|
||||
});
|
||||
|
||||
const center = latestGPS ? [latestGPS.lat, latestGPS.lng] : [0, 0]; // Default center
|
||||
|
||||
return (
|
||||
<>
|
||||
{fence.length > 1 && <Polyline pathOptions={{ color: "red" }} positions={fence} />}
|
||||
{latestGPS && <Marker position={[latestGPS.lat, latestGPS.lng]} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeviceMap = ({ latestGPS }: { latestGPS?: { lat: number; lng: number } }) => {
|
||||
const [fence, setFence] = useState<Array<{ lat: number; lng: number }>>([]);
|
||||
|
||||
return (
|
||||
<MapContainer center={[0, 0]} zoom={2} style={{ height: "300px", width: "100%" }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<FenceMap latestGPS={latestGPS} onFenceChange={setFence} />
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
36
lib/types.ts
Normal file
36
lib/types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export type DeviceCommandType =
|
||||
| "wifi"
|
||||
| "sleep"
|
||||
| "telemetry_sec"
|
||||
| "poll_sec"
|
||||
| "ota"
|
||||
| "ring_fence"
|
||||
| "lights"
|
||||
| "camera";
|
||||
|
||||
export interface DeviceCommand {
|
||||
id: number;
|
||||
type: DeviceCommandType;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TelemetryPost {
|
||||
recorded_at?: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
altitude_m?: number;
|
||||
speed_kmh?: number;
|
||||
heading_deg?: number;
|
||||
accuracy_m?: number;
|
||||
battery_percent?: number;
|
||||
is_car_on?: boolean;
|
||||
raw?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CommandReceiptPost {
|
||||
command_id: number;
|
||||
acked_at?: string;
|
||||
executed_at?: string;
|
||||
result?: "ok" | "error";
|
||||
result_detail?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user