feat(csms, web): add charge point status and error code to charge point details, hide the connector 0 from connectors view

This commit is contained in:
2026-03-11 11:00:16 +08:00
parent 48d9580d36
commit f74939917b
4 changed files with 98 additions and 15 deletions

View File

@@ -17,15 +17,25 @@ app.get("/", async (c) => {
.where(isAdmin ? undefined : eq(chargePoint.registrationStatus, "Accepted")) .where(isAdmin ? undefined : eq(chargePoint.registrationStatus, "Accepted"))
.orderBy(desc(chargePoint.createdAt)); .orderBy(desc(chargePoint.createdAt));
// Attach connectors (connectorId > 0 only, excludes the main-controller row) if (!cps.length) return c.json([]);
const connectors = cps.length
? await db const cpIdList = sql.raw(`array[${cps.map((cp) => `'${cp.id}'`).join(",")}]`);
// connectorId > 0: real connectors to display
const connectors = await db
.select() .select()
.from(connector) .from(connector)
.where( .where(sql`${connector.chargePointId} = any(${cpIdList}) and ${connector.connectorId} > 0`);
sql`${connector.chargePointId} = any(${sql.raw(`array[${cps.map((cp) => `'${cp.id}'`).join(",")}]`)}) and ${connector.connectorId} > 0`,
) // connectorId = 0: whole-station status row
: []; const cpStatusRows = await db
.select({
chargePointId: connector.chargePointId,
status: connector.status,
errorCode: connector.errorCode,
})
.from(connector)
.where(sql`${connector.chargePointId} = any(${cpIdList}) and ${connector.connectorId} = 0`);
const connectorsByCP: Record<string, typeof connectors> = {}; const connectorsByCP: Record<string, typeof connectors> = {};
for (const conn of connectors) { for (const conn of connectors) {
@@ -33,10 +43,17 @@ app.get("/", async (c) => {
connectorsByCP[conn.chargePointId].push(conn); connectorsByCP[conn.chargePointId].push(conn);
} }
const cpStatusByCP: Record<string, { status: string; errorCode: string }> = {};
for (const row of cpStatusRows) {
cpStatusByCP[row.chargePointId] = { status: row.status, errorCode: row.errorCode };
}
return c.json( return c.json(
cps.map((cp) => ({ cps.map((cp) => ({
...cp, ...cp,
connectors: connectorsByCP[cp.id] ?? [], connectors: connectorsByCP[cp.id] ?? [],
chargePointStatus: cpStatusByCP[cp.id]?.status ?? null,
chargePointErrorCode: cpStatusByCP[cp.id]?.errorCode ?? null,
})), })),
); );
}); });
@@ -88,9 +105,16 @@ app.get("/:id", async (c) => {
if (!cp) return c.json({ error: "Not found" }, 404); if (!cp) return c.json({ error: "Not found" }, 404);
const connectors = await db.select().from(connector).where(eq(connector.chargePointId, id)); const allConnectors = await db.select().from(connector).where(eq(connector.chargePointId, id));
const cpStatus = allConnectors.find((conn) => conn.connectorId === 0);
const displayConnectors = allConnectors.filter((conn) => conn.connectorId > 0);
return c.json({ ...cp, connectors }); return c.json({
...cp,
connectors: displayConnectors,
chargePointStatus: cpStatus?.status ?? null,
chargePointErrorCode: cpStatus?.errorCode ?? null,
});
}); });
/** PATCH /api/charge-points/:id — update charge point fields */ /** PATCH /api/charge-points/:id — update charge point fields */
@@ -136,8 +160,16 @@ app.patch("/:id", async (c) => {
if (!updated) return c.json({ error: "Not found" }, 404); if (!updated) return c.json({ error: "Not found" }, 404);
const connectors = await db.select().from(connector).where(eq(connector.chargePointId, id)); const allConnectors = await db.select().from(connector).where(eq(connector.chargePointId, id));
return c.json({ ...updated, connectors }); const cpStatus = allConnectors.find((conn) => conn.connectorId === 0);
const displayConnectors = allConnectors.filter((conn) => conn.connectorId > 0);
return c.json({
...updated,
connectors: displayConnectors,
chargePointStatus: cpStatus?.status ?? null,
chargePointErrorCode: cpStatus?.errorCode ?? null,
});
}); });
/** DELETE /api/charge-points/:id — delete a charge point (cascades to connectors, transactions, meter values) */ /** DELETE /api/charge-points/:id — delete a charge point (cascades to connectors, transactions, meter values) */

View File

@@ -240,6 +240,35 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
</div> </div>
</div> </div>
{/* Info grid */}
{cp.chargePointStatus && (
<div
className={`flex items-center gap-3 rounded-xl border px-4 py-3 ${
cp.chargePointStatus === "Available"
? "border-success/30 bg-success/5"
: cp.chargePointStatus === "Faulted" || cp.chargePointStatus === "Unavailable"
? "border-danger/30 bg-danger/5"
: "border-warning/30 bg-warning/5"
}`}
>
<span
className={`size-2.5 shrink-0 rounded-full ${statusDotClass[cp.chargePointStatus] ?? "bg-warning"}`}
/>
<span className="text-sm font-semibold text-foreground">
{cp.chargePointStatus === "Available"
? "正常"
: (statusLabelMap[cp.chargePointStatus] ?? cp.chargePointStatus)}
</span>
{cp.chargePointErrorCode && cp.chargePointErrorCode !== "NoError" && (
<>
<span className="text-muted">·</span>
<span className="text-xs text-danger">{cp.chargePointErrorCode}</span>
</>
)}
<span className="ml-auto text-xs text-muted"></span>
</div>
)}
{/* Info grid */} {/* Info grid */}
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{/* Device info — admin only */} {/* Device info — admin only */}

View File

@@ -282,8 +282,9 @@ export default function ChargePointsPage() {
{isAdmin && <Table.Column> / </Table.Column>} {isAdmin && <Table.Column> / </Table.Column>}
{isAdmin && <Table.Column></Table.Column>} {isAdmin && <Table.Column></Table.Column>}
<Table.Column>/kWh</Table.Column> <Table.Column>/kWh</Table.Column>
<Table.Column></Table.Column> <Table.Column></Table.Column>
<Table.Column></Table.Column> <Table.Column></Table.Column>
<Table.Column></Table.Column>
{isAdmin && <Table.Column>{""}</Table.Column>} {isAdmin && <Table.Column>{""}</Table.Column>}
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
@@ -297,6 +298,7 @@ export default function ChargePointsPage() {
<Table.Cell>{""}</Table.Cell> <Table.Cell>{""}</Table.Cell>
<Table.Cell>{""}</Table.Cell> <Table.Cell>{""}</Table.Cell>
<Table.Cell>{""}</Table.Cell> <Table.Cell>{""}</Table.Cell>
<Table.Cell>{""}</Table.Cell>
{isAdmin && <Table.Cell>{""}</Table.Cell>} {isAdmin && <Table.Cell>{""}</Table.Cell>}
</Table.Row> </Table.Row>
)} )}
@@ -349,6 +351,22 @@ export default function ChargePointsPage() {
<span className="text-muted"></span> <span className="text-muted"></span>
)} )}
</Table.Cell> </Table.Cell>
<Table.Cell>
{cp.chargePointStatus ? (
<div className="flex items-center gap-1.5">
<span
className={`size-1.5 shrink-0 rounded-full ${statusDotClass[cp.chargePointStatus] ?? "bg-warning"}`}
/>
<span className="text-sm text-foreground text-nowrap">
{cp.chargePointStatus === "Available"
? "正常"
: (statusLabelMap[cp.chargePointStatus] ?? cp.chargePointStatus)}
</span>
</div>
) : (
<span className="text-muted"></span>
)}
</Table.Cell>
<Table.Cell> <Table.Cell>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{cp.connectors.length === 0 ? ( {cp.connectors.length === 0 ? (

View File

@@ -68,6 +68,8 @@ export type ChargePoint = {
lastBootNotificationAt: string | null; lastBootNotificationAt: string | null;
feePerKwh: number; feePerKwh: number;
connectors: ConnectorSummary[]; connectors: ConnectorSummary[];
chargePointStatus: string | null;
chargePointErrorCode: string | null;
}; };
export type ChargePointDetail = { export type ChargePointDetail = {
@@ -89,6 +91,8 @@ export type ChargePointDetail = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
connectors: ConnectorDetail[]; connectors: ConnectorDetail[];
chargePointStatus: string | null;
chargePointErrorCode: string | null;
}; };
export type Transaction = { export type Transaction = {