feat(dashboard): add transactions and users management pages with CRUD functionality

feat(auth): implement login page and authentication middleware
feat(sidebar): create sidebar component with user info and navigation links
feat(api): establish API client for interacting with backend services
This commit is contained in:
2026-03-10 15:17:32 +08:00
parent 9a2668fae5
commit 2cb89c74b3
32 changed files with 4648 additions and 83 deletions

View File

@@ -0,0 +1,92 @@
import { Hono } from "hono";
import { desc, eq, sql } from "drizzle-orm";
import { useDrizzle } from "@/lib/db.js";
import { chargePoint, connector } from "@/db/schema.js";
const app = new Hono();
/** GET /api/charge-points — list all charge points with connectors */
app.get("/", async (c) => {
const db = useDrizzle();
const cps = await db.select().from(chargePoint).orderBy(desc(chargePoint.createdAt));
// Attach connectors (connectorId > 0 only, excludes the main-controller row)
const connectors = cps.length
? await db
.select()
.from(connector)
.where(
sql`${connector.chargePointId} = any(${sql.raw(`array[${cps.map((cp) => `'${cp.id}'`).join(",")}]`)}) and ${connector.connectorId} > 0`,
)
: [];
const connectorsByCP: Record<string, typeof connectors> = {};
for (const conn of connectors) {
if (!connectorsByCP[conn.chargePointId]) connectorsByCP[conn.chargePointId] = [];
connectorsByCP[conn.chargePointId].push(conn);
}
return c.json(
cps.map((cp) => ({
...cp,
connectors: connectorsByCP[cp.id] ?? [],
})),
);
});
/** GET /api/charge-points/:id — single charge point */
app.get("/:id", async (c) => {
const db = useDrizzle();
const id = c.req.param("id");
const [cp] = await db.select().from(chargePoint).where(eq(chargePoint.id, id)).limit(1);
if (!cp) return c.json({ error: "Not found" }, 404);
const connectors = await db.select().from(connector).where(eq(connector.chargePointId, id));
return c.json({ ...cp, connectors });
});
/** PATCH /api/charge-points/:id — update feePerKwh */
app.patch("/:id", async (c) => {
const db = useDrizzle();
const id = c.req.param("id");
const body = await c.req.json<{ feePerKwh?: number }>();
if (
typeof body.feePerKwh !== "number" ||
body.feePerKwh < 0 ||
!Number.isInteger(body.feePerKwh)
) {
return c.json({ error: "feePerKwh must be a non-negative integer (unit: fen/kWh)" }, 400);
}
const [updated] = await db
.update(chargePoint)
.set({ feePerKwh: body.feePerKwh, updatedAt: new Date() })
.where(eq(chargePoint.id, id))
.returning();
if (!updated) return c.json({ error: "Not found" }, 404);
return c.json({ feePerKwh: updated.feePerKwh });
});
/** DELETE /api/charge-points/:id — delete a charge point (cascades to connectors, transactions, meter values) */
app.delete("/:id", async (c) => {
const db = useDrizzle();
const id = c.req.param("id");
const [deleted] = await db
.delete(chargePoint)
.where(eq(chargePoint.id, id))
.returning({ id: chargePoint.id });
if (!deleted) return c.json({ error: "Not found" }, 404);
return c.json({ success: true });
});
export default app;