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,85 @@
import { eq } from "drizzle-orm";
import { useDrizzle } from "@/lib/db.js";
import { chargePoint, connector, transaction } from "@/db/schema.js";
import type {
StartTransactionRequest,
StartTransactionResponse,
OcppConnectionContext,
} from "../types.ts";
import { resolveIdTagInfo } from "./authorize.ts";
export async function handleStartTransaction(
payload: StartTransactionRequest,
ctx: OcppConnectionContext,
): Promise<StartTransactionResponse> {
const db = useDrizzle();
// Resolve idTag authorization
const idTagInfo = await resolveIdTagInfo(payload.idTag);
// Find charge point
const [cp] = await db
.select({ id: chargePoint.id })
.from(chargePoint)
.where(eq(chargePoint.chargePointIdentifier, ctx.chargePointIdentifier))
.limit(1);
if (!cp) throw new Error(`ChargePoint not found: ${ctx.chargePointIdentifier}`);
// Upsert connector — it may not exist yet if StatusNotification was skipped
const [conn] = await db
.insert(connector)
.values({
id: crypto.randomUUID(),
chargePointId: cp.id,
connectorId: payload.connectorId,
status: "Charging",
errorCode: "NoError",
lastStatusAt: new Date(),
})
.onConflictDoUpdate({
target: [connector.chargePointId, connector.connectorId],
set: { status: "Charging", updatedAt: new Date() },
})
.returning({ id: connector.id });
const rejected = idTagInfo.status !== "Accepted";
const now = new Date();
// Insert transaction record regardless of auth status (OCPP spec requirement)
const [tx] = await db
.insert(transaction)
.values({
chargePointId: cp.id,
connectorId: conn.id,
connectorNumber: payload.connectorId,
idTag: payload.idTag,
idTagStatus: idTagInfo.status,
startTimestamp: new Date(payload.timestamp),
startMeterValue: payload.meterStart,
reservationId: payload.reservationId ?? null,
// If rejected, immediately close the transaction so it doesn't appear as in-progress
...(rejected && {
stopTimestamp: now,
stopMeterValue: payload.meterStart,
chargeAmount: 0,
stopReason: "DeAuthorized",
}),
})
.returning({ id: transaction.id });
// If rejected, reset connector back to Available
if (rejected) {
await db
.update(connector)
.set({ status: "Available", updatedAt: now })
.where(eq(connector.id, conn.id));
}
console.log(
`[OCPP] StartTransaction cp=${ctx.chargePointIdentifier} connector=${payload.connectorId} ` +
`idTag=${payload.idTag} status=${idTagInfo.status} txId=${tx.id}`,
);
return { transactionId: tx.id, idTagInfo };
}