font, map markers templates
This commit is contained in:
@ -6,6 +6,15 @@ model Device {
|
||||
@@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 {
|
||||
id String @id @default(uuid())
|
||||
deviceId String
|
||||
|
||||
BIN
public/leaflet/layers-2x.png
Normal file
BIN
public/leaflet/layers-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/leaflet/layers.png
Normal file
BIN
public/leaflet/layers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
public/leaflet/marker-icon-2x.png
Normal file
BIN
public/leaflet/marker-icon-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/leaflet/marker-icon.png
Normal file
BIN
public/leaflet/marker-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/leaflet/marker-shadow.png
Normal file
BIN
public/leaflet/marker-shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
121
src/app/_components/CommandManager.tsx
Normal file
121
src/app/_components/CommandManager.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -10,9 +10,9 @@ import "leaflet/dist/leaflet.css";
|
||||
// Fix default Leaflet icon issue in React
|
||||
delete (L.Icon.Default as any)._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: 'leaflet/marker-icon-2x.png',
|
||||
iconUrl: 'leaflet/marker-icon.png',
|
||||
shadowUrl: 'leaflet/marker-shadow.png',
|
||||
iconRetinaUrl: '/leaflet/marker-icon-2x.png',
|
||||
iconUrl: '/leaflet/marker-icon.png',
|
||||
shadowUrl: '/leaflet/marker-shadow.png',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -5,9 +5,12 @@ import { useState, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
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
|
||||
const DeviceMap = dynamic(
|
||||
() => import("~/_components/DeviceMapClient").then((mod) => mod.DeviceMap),
|
||||
() => import("~/app/_components/DynamicDeviceMap").then((mod) => mod.DeviceMap),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <p>Loading map...</p>,
|
||||
@ -41,19 +44,8 @@ export default function Dashboard() {
|
||||
return () => clearInterval(interval);
|
||||
}, [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 (
|
||||
<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>
|
||||
<section className="grid grid-cols-1 gap-4 mt-4 md:grid-cols-2">
|
||||
<div>
|
||||
@ -87,50 +79,12 @@ export default function Dashboard() {
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="text-xl font-bold">Send Command</h2>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<select
|
||||
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>
|
||||
<h2 className="text-xl font-bold">Command Center</h2>
|
||||
<div className="mt-2">
|
||||
<CommandManager selectedImei={selectedImei} /> {/* <-- ADD THE NEW COMPONENT HERE */}
|
||||
</div>
|
||||
</section>
|
||||
</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)}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { postRouter } from "~/server/api/routers/post";
|
||||
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.
|
||||
*
|
||||
* All routers added in /api/routers should be manually added here.
|
||||
*/
|
||||
export const appRouter = createTRPCRouter({
|
||||
post: postRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
38
src/server/api/routers/commandTemplate.ts
Normal file
38
src/server/api/routers/commandTemplate.ts
Normal 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 },
|
||||
});
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user