font, map markers templates

This commit is contained in:
2025-09-11 11:25:10 +10:00
parent 2a0e9df0f3
commit d6a5f13f89
11 changed files with 187 additions and 62 deletions

View File

@ -6,6 +6,15 @@ model Device {
@@map("devices") @@map("devices")
} }
model CommandTemplate {
id Int @id @default(autoincrement())
name String // e.g., "Lights On", "Reboot Device"
type String // e.g., "lights", "reboot"
payload Json // The JSON payload, e.g., {"on": true}
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Telemetry { model Telemetry {
id String @id @default(uuid()) id String @id @default(uuid())
deviceId String deviceId String

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/leaflet/layers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@ -0,0 +1,121 @@
// In file: src/app/_components/CommandManager.tsx
"use client";
import { useState } from "react";
import { api } from "~/trpc/react";
import { postCommand, type DeviceCommandType } from "~/lib/api"; // Your Laravel API helper
interface CommandManagerProps {
selectedImei: string;
}
export function CommandManager({ selectedImei }: CommandManagerProps) {
const [name, setName] = useState("");
const [type, setType] = useState<DeviceCommandType>("reboot");
const [payload, setPayload] = useState(""); // Stored as a string for the textarea
const utils = api.useUtils();
const templatesQuery = api.commandTemplate.getAll.useQuery();
const createTemplateMutation = api.commandTemplate.create.useMutation({
onSuccess: () => {
utils.commandTemplate.getAll.invalidate(); // Invalidate cache to refetch list
setName("");
setPayload("");
},
});
const deleteTemplateMutation = api.commandTemplate.delete.useMutation({
onSuccess: () => {
utils.commandTemplate.getAll.invalidate(); // Refetch list after deleting
},
});
const handleCreateTemplate = () => {
try {
const parsedPayload = payload ? JSON.parse(payload) : {};
createTemplateMutation.mutate({ name, type, payload: parsedPayload });
} catch {
alert("Invalid JSON in payload!");
}
};
const handleSendCommand = async (template: { type: string; payload: any }) => {
try {
await postCommand(selectedImei, {
type: template.type as DeviceCommandType,
payload: template.payload,
});
alert(`Command "${template.type}" sent to ${selectedImei}!`);
} catch (e) {
alert(`Failed to send command: ${e instanceof Error ? e.message : "Unknown error"}`);
}
};
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{/* Form to add new templates */}
<div className="flex flex-col gap-2 p-4 rounded-md bg-white/10">
<h3 className="text-lg font-bold">Create New Command Template</h3>
<input
type="text"
placeholder="Template Name (e.g., Lights On)"
value={name}
onChange={(e) => setName(e.target.value)}
className="p-2 text-white rounded-md bg-white/10"
/>
<select
value={type}
onChange={(e) => setType(e.target.value as DeviceCommandType)}
className="p-2 text-white rounded-md bg-white/10"
>
<option value="reboot">Reboot Device</option>
<option value="lights">Lights On/Off</option>
{/* Add other types */}
</select>
<textarea
placeholder='JSON Payload, e.g., {"on": true}'
value={payload}
onChange={(e) => setPayload(e.target.value)}
className="p-2 text-white rounded-md bg-white/10"
rows={3}
/>
<button
onClick={handleCreateTemplate}
className="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700"
disabled={createTemplateMutation.isPending}
>
{createTemplateMutation.isPending ? "Saving..." : "Save Template"}
</button>
</div>
{/* List of saved templates */}
<div className="flex flex-col gap-2 p-4 rounded-md bg-white/10">
<h3 className="text-lg font-bold">Saved Commands</h3>
{templatesQuery.isLoading && <p>Loading templates...</p>}
{templatesQuery.data?.map((template) => (
<div key={template.id} className="flex items-center justify-between p-2 rounded-md bg-black/20">
<div>
<p className="font-semibold">{template.name}</p>
<p className="text-sm text-gray-400">Type: {template.type}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => handleSendCommand(template)}
className="px-3 py-1 text-white bg-green-600 rounded-md hover:bg-green-700"
>
Send
</button>
<button
onClick={() => deleteTemplateMutation.mutate({ id: template.id })}
className="px-3 py-1 text-white bg-red-600 rounded-md hover:bg-red-700"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -10,9 +10,9 @@ import "leaflet/dist/leaflet.css";
// Fix default Leaflet icon issue in React // Fix default Leaflet icon issue in React
delete (L.Icon.Default as any)._getIconUrl; delete (L.Icon.Default as any)._getIconUrl;
L.Icon.Default.mergeOptions({ L.Icon.Default.mergeOptions({
iconRetinaUrl: 'leaflet/marker-icon-2x.png', iconRetinaUrl: '/leaflet/marker-icon-2x.png',
iconUrl: 'leaflet/marker-icon.png', iconUrl: '/leaflet/marker-icon.png',
shadowUrl: 'leaflet/marker-shadow.png', shadowUrl: '/leaflet/marker-shadow.png',
}); });

View File

@ -5,9 +5,12 @@ import { useState, useEffect } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getCommands, getLatestGPS, DeviceCommand, TelemetryPost, DeviceCommandType, postReceipt } from "~/lib/api"; // Assuming postCommand will be added later import { getCommands, getLatestGPS, DeviceCommand, TelemetryPost, DeviceCommandType, postReceipt } from "~/lib/api"; // Assuming postCommand will be added later
import { CommandManager } from "~/app/_components/CommandManager";
// Dynamically import the map component with SSR turned OFF to prevent "window is not defined" errors // Dynamically import the map component with SSR turned OFF to prevent "window is not defined" errors
const DeviceMap = dynamic( const DeviceMap = dynamic(
() => import("~/_components/DeviceMapClient").then((mod) => mod.DeviceMap), () => import("~/app/_components/DynamicDeviceMap").then((mod) => mod.DeviceMap),
{ {
ssr: false, ssr: false,
loading: () => <p>Loading map...</p>, loading: () => <p>Loading map...</p>,
@ -41,19 +44,8 @@ export default function Dashboard() {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [selectedImei]); }, [selectedImei]);
const sendCommand = async () => {
// This is a placeholder until you provide the Laravel routes.
try {
const parsedPayload = payload ? JSON.parse(payload) : {};
console.log("WAITING FOR LARAVEL ROUTE: Would send command:", { type: commandType, payload: parsedPayload });
alert("Command sending is not implemented yet. See console for details.");
} catch (e) {
alert("Invalid JSON in payload!");
}
};
return ( return (
<main className="p-4"> <main className="flex min-h-screen flex-col gap-4 bg-gray-900 p-4 text-white">
<h1 className="text-2xl font-bold">Device Dashboard ({selectedImei})</h1> <h1 className="text-2xl font-bold">Device Dashboard ({selectedImei})</h1>
<section className="grid grid-cols-1 gap-4 mt-4 md:grid-cols-2"> <section className="grid grid-cols-1 gap-4 mt-4 md:grid-cols-2">
<div> <div>
@ -86,51 +78,13 @@ export default function Dashboard() {
)} )}
</section> </section>
<section className="mt-6"> <section className="mt-6">
<h2 className="text-xl font-bold">Send Command</h2> <h2 className="text-xl font-bold">Command Center</h2>
<div className="flex flex-col gap-2 mt-2"> <div className="mt-2">
<select <CommandManager selectedImei={selectedImei} /> {/* <-- ADD THE NEW COMPONENT HERE */}
className="p-2 text-white rounded-md bg-white/10"
value={commandType}
onChange={(e) => setCommandType(e.target.value as DeviceCommandType)}
>
<option value="reboot">Reboot Device</option>
<option value="lights">Lights On/Off</option>
<option value="camera">Camera On/Off</option>
<option value="sleep">Set Sleep Interval</option>
<option value="telemetry_sec">Set Telemetry Interval</option>
<option value="reboot">Reboot</option>
</select>
<textarea
className="p-2 text-white rounded-md bg-white/10"
value={payload}
onChange={(e) => setPayload(e.target.value)}
placeholder='Enter JSON payload, e.g., {"on": true}'
/>
<button
className="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700"
onClick={sendCommand}>
Send Command
</button>
</div> </div>
</section> </section>
</main> </main>
); );
const sendCommand = async () => {
try {
const parsedPayload = payload ? JSON.parse(payload) : {};
const response = await postCommand(selectedImei, {
type: commandType,
payload: parsedPayload,
});
console.log("Command sent successfully, ID:", response.command_id);
alert("Command sent successfully!");
setPayload(""); // Clear the input field
} catch (e) {
console.error("Failed to send command:", e);
alert(`Failed to send command: ${e instanceof Error ? e.message : String(e)}`);
}
};
} }

View File

@ -1,14 +1,17 @@
import { postRouter } from "~/server/api/routers/post"; import { postRouter } from "~/server/api/routers/post";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
import { commandTemplateRouter } from "./routers/commandTemplate";
export const appRouter = createTRPCRouter({
post: postRouter,
commandTemplate: commandTemplateRouter, // <-- ADD THIS LINE
});
/** /**
* This is the primary router for your server. * This is the primary router for your server.
* *
* All routers added in /api/routers should be manually added here. * All routers added in /api/routers should be manually added here.
*/ */
export const appRouter = createTRPCRouter({
post: postRouter,
});
// export type definition of API // export type definition of API
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

View File

@ -0,0 +1,38 @@
// In file: src/server/api/routers/commandTemplate.ts
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
export const commandTemplateRouter = createTRPCRouter({
// Procedure to get all templates
getAll: publicProcedure.query(({ ctx }) => {
return ctx.db.commandTemplate.findMany({
orderBy: { createdAt: "desc" },
});
}),
// Procedure to create a new template
create: publicProcedure
.input(z.object({
name: z.string().min(1),
type: z.string().min(1),
payload: z.record(z.unknown()), // Accepts any JSON object
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.commandTemplate.create({
data: {
name: input.name,
type: input.type,
payload: input.payload,
},
});
}),
// Procedure to delete a template
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.commandTemplate.delete({
where: { id: input.id },
});
}),
});