feat(api): add stats chart endpoint for admin access with time series data
feat(dayjs): integrate dayjs for date handling and formatting across the application refactor(routes): update date handling in id-tags, transactions, users, and dashboard routes to use dayjs style(globals): improve CSS variable definitions for better readability and consistency deps: add dayjs as a dependency for date manipulation
This commit is contained in:
@@ -9,6 +9,7 @@ import { showRoutes } from 'hono/dev'
|
||||
import { auth } from './lib/auth.ts'
|
||||
import { createOcppHandler } from './ocpp/handler.ts'
|
||||
import statsRoutes from './routes/stats.ts'
|
||||
import statsChartRoutes from './routes/stats-chart.ts'
|
||||
import chargePointRoutes from './routes/charge-points.ts'
|
||||
import transactionRoutes from './routes/transactions.ts'
|
||||
import idTagRoutes from './routes/id-tags.ts'
|
||||
@@ -51,6 +52,7 @@ app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw))
|
||||
|
||||
// REST API routes
|
||||
app.route('/api/stats', statsRoutes)
|
||||
app.route('/api/stats/chart', statsChartRoutes)
|
||||
app.route('/api/charge-points', chargePointRoutes)
|
||||
app.route('/api/transactions', transactionRoutes)
|
||||
app.route('/api/id-tags', idTagRoutes)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { idTag } from "@/db/schema.js";
|
||||
import type {
|
||||
@@ -24,7 +25,7 @@ export async function resolveIdTagInfo(
|
||||
|
||||
if (!tag) return { status: "Invalid" };
|
||||
if (tag.status === "Blocked") return { status: "Blocked" };
|
||||
if (tag.expiryDate && tag.expiryDate < new Date()) {
|
||||
if (tag.expiryDate && dayjs(tag.expiryDate).isBefore(dayjs())) {
|
||||
return { status: "Expired", expiryDate: tag.expiryDate.toISOString() };
|
||||
}
|
||||
if (tag.status !== "Accepted") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDrizzle } from '@/lib/db.js'
|
||||
import dayjs from 'dayjs'
|
||||
import { chargePoint } from '@/db/schema.js'
|
||||
import type {
|
||||
BootNotificationRequest,
|
||||
@@ -30,7 +31,7 @@ export async function handleBootNotification(
|
||||
// New, unknown devices start as Pending — admin must manually accept them
|
||||
registrationStatus: 'Pending',
|
||||
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
||||
lastBootNotificationAt: new Date(),
|
||||
lastBootNotificationAt: dayjs().toDate(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: chargePoint.chargePointIdentifier,
|
||||
@@ -45,8 +46,8 @@ export async function handleBootNotification(
|
||||
meterSerialNumber: payload.meterSerialNumber ?? null,
|
||||
// Do NOT override registrationStatus — preserve whatever the admin set
|
||||
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
||||
lastBootNotificationAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
lastBootNotificationAt: dayjs().toDate(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
@@ -57,7 +58,7 @@ export async function handleBootNotification(
|
||||
console.log(`[OCPP] BootNotification ${ctx.chargePointIdentifier} status=${status}`)
|
||||
|
||||
return {
|
||||
currentTime: new Date().toISOString(),
|
||||
currentTime: dayjs().toISOString(),
|
||||
interval: DEFAULT_HEARTBEAT_INTERVAL,
|
||||
status,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import dayjs from 'dayjs'
|
||||
import { useDrizzle } from '@/lib/db.js'
|
||||
import { chargePoint } from '@/db/schema.js'
|
||||
import type {
|
||||
@@ -15,10 +16,10 @@ export async function handleHeartbeat(
|
||||
|
||||
await db
|
||||
.update(chargePoint)
|
||||
.set({ lastHeartbeatAt: new Date() })
|
||||
.set({ lastHeartbeatAt: dayjs().toDate() })
|
||||
.where(eq(chargePoint.chargePointIdentifier, ctx.chargePointIdentifier))
|
||||
|
||||
return {
|
||||
currentTime: new Date().toISOString(),
|
||||
currentTime: dayjs().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { chargePoint, connector, meterValue } from "@/db/schema.js";
|
||||
import type { MeterValuesRequest, MeterValuesResponse, OcppConnectionContext } from "../types.ts";
|
||||
@@ -38,7 +39,7 @@ export async function handleMeterValues(
|
||||
connectorId: conn.id,
|
||||
chargePointId: cp.id,
|
||||
connectorNumber: payload.connectorId,
|
||||
timestamp: new Date(mv.timestamp),
|
||||
timestamp: dayjs(mv.timestamp).toDate(),
|
||||
sampledValues:
|
||||
mv.sampledValue as unknown as (typeof meterValue.$inferInsert)["sampledValues"],
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { chargePoint, connector, transaction } from "@/db/schema.js";
|
||||
import type {
|
||||
@@ -35,16 +36,16 @@ export async function handleStartTransaction(
|
||||
connectorId: payload.connectorId,
|
||||
status: "Charging",
|
||||
errorCode: "NoError",
|
||||
lastStatusAt: new Date(),
|
||||
lastStatusAt: dayjs().toDate(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [connector.chargePointId, connector.connectorId],
|
||||
set: { status: "Charging", updatedAt: new Date() },
|
||||
set: { status: "Charging", updatedAt: dayjs().toDate() },
|
||||
})
|
||||
.returning({ id: connector.id });
|
||||
|
||||
const rejected = idTagInfo.status !== "Accepted";
|
||||
const now = new Date();
|
||||
const now = dayjs();
|
||||
|
||||
// Insert transaction record regardless of auth status (OCPP spec requirement)
|
||||
const [tx] = await db
|
||||
@@ -55,12 +56,12 @@ export async function handleStartTransaction(
|
||||
connectorNumber: payload.connectorId,
|
||||
idTag: payload.idTag,
|
||||
idTagStatus: idTagInfo.status,
|
||||
startTimestamp: new Date(payload.timestamp),
|
||||
startTimestamp: dayjs(payload.timestamp).toDate(),
|
||||
startMeterValue: payload.meterStart,
|
||||
reservationId: payload.reservationId ?? null,
|
||||
// If rejected, immediately close the transaction so it doesn't appear as in-progress
|
||||
...(rejected && {
|
||||
stopTimestamp: now,
|
||||
stopTimestamp: now.toDate(),
|
||||
stopMeterValue: payload.meterStart,
|
||||
chargeAmount: 0,
|
||||
stopReason: "DeAuthorized",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import dayjs from 'dayjs'
|
||||
import { useDrizzle } from '@/lib/db.js'
|
||||
import { chargePoint, connector, connectorStatusHistory } from '@/db/schema.js'
|
||||
import type {
|
||||
@@ -54,7 +55,7 @@ export async function handleStatusNotification(
|
||||
throw new Error(`ChargePoint not found: ${ctx.chargePointIdentifier}`)
|
||||
}
|
||||
|
||||
const statusTimestamp = payload.timestamp ? new Date(payload.timestamp) : new Date()
|
||||
const statusTimestamp = payload.timestamp ? dayjs(payload.timestamp).toDate() : dayjs().toDate()
|
||||
const connStatus = payload.status as ConnectorStatus
|
||||
const connErrorCode = payload.errorCode as ConnectorErrorCode
|
||||
|
||||
@@ -81,7 +82,7 @@ export async function handleStatusNotification(
|
||||
vendorId: payload.vendorId ?? null,
|
||||
vendorErrorCode: payload.vendorErrorCode ?? null,
|
||||
lastStatusAt: statusTimestamp,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
},
|
||||
})
|
||||
.returning({ id: connector.id })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { chargePoint, connector, idTag, meterValue, transaction } from "@/db/schema.js";
|
||||
import type {
|
||||
@@ -18,11 +19,11 @@ export async function handleStopTransaction(
|
||||
const [tx] = await db
|
||||
.update(transaction)
|
||||
.set({
|
||||
stopTimestamp: new Date(payload.timestamp),
|
||||
stopTimestamp: dayjs(payload.timestamp).toDate(),
|
||||
stopMeterValue: payload.meterStop,
|
||||
stopIdTag: payload.idTag ?? null,
|
||||
stopReason: (payload.reason as (typeof transaction.$inferSelect)["stopReason"]) ?? null,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
})
|
||||
.where(eq(transaction.id, payload.transactionId))
|
||||
.returning();
|
||||
@@ -35,7 +36,7 @@ export async function handleStopTransaction(
|
||||
// Set connector back to Available
|
||||
await db
|
||||
.update(connector)
|
||||
.set({ status: "Available", updatedAt: new Date() })
|
||||
.set({ status: "Available", updatedAt: dayjs().toDate() })
|
||||
.where(eq(connector.id, tx.connectorId));
|
||||
|
||||
// Store embedded meter values (transactionData)
|
||||
@@ -49,7 +50,7 @@ export async function handleStopTransaction(
|
||||
connectorId: tx.connectorId,
|
||||
chargePointId: tx.chargePointId,
|
||||
connectorNumber: tx.connectorNumber,
|
||||
timestamp: new Date(mv.timestamp),
|
||||
timestamp: dayjs(mv.timestamp).toDate(),
|
||||
sampledValues:
|
||||
mv.sampledValue as unknown as (typeof meterValue.$inferInsert)["sampledValues"],
|
||||
},
|
||||
@@ -76,7 +77,7 @@ export async function handleStopTransaction(
|
||||
// Always record the charge amount (0 if free)
|
||||
await db
|
||||
.update(transaction)
|
||||
.set({ chargeAmount: feeFen, updatedAt: new Date() })
|
||||
.set({ chargeAmount: feeFen, updatedAt: dayjs().toDate() })
|
||||
.where(eq(transaction.id, tx.id));
|
||||
|
||||
if (feeFen > 0) {
|
||||
@@ -84,7 +85,7 @@ export async function handleStopTransaction(
|
||||
.update(idTag)
|
||||
.set({
|
||||
balance: sql`${idTag.balance} - ${feeFen}`,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
})
|
||||
.where(eq(idTag.idTag, tx.idTag));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Hono } from "hono";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { chargePoint, connector } from "@/db/schema.js";
|
||||
import type { HonoEnv } from "@/types/hono.ts";
|
||||
@@ -135,7 +136,7 @@ app.patch("/:id", async (c) => {
|
||||
chargePointVendor?: string;
|
||||
chargePointModel?: string;
|
||||
updatedAt: Date;
|
||||
} = { updatedAt: new Date() };
|
||||
} = { updatedAt: dayjs().toDate() };
|
||||
|
||||
if (body.feePerKwh !== undefined) {
|
||||
if (!Number.isInteger(body.feePerKwh) || body.feePerKwh < 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Hono } from "hono";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { idTag } from "@/db/schema.js";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
@@ -69,7 +70,7 @@ app.post("/", async (c) => {
|
||||
.insert(idTag)
|
||||
.values({
|
||||
...parsed.data,
|
||||
expiryDate: parsed.data.expiryDate ? new Date(parsed.data.expiryDate) : null,
|
||||
expiryDate: parsed.data.expiryDate ? dayjs(parsed.data.expiryDate).toDate() : null,
|
||||
})
|
||||
.returning();
|
||||
return c.json(created, 201);
|
||||
@@ -120,8 +121,8 @@ app.patch("/:id", async (c) => {
|
||||
.update(idTag)
|
||||
.set({
|
||||
...parsed.data,
|
||||
expiryDate: parsed.data.expiryDate ? new Date(parsed.data.expiryDate) : undefined,
|
||||
updatedAt: new Date(),
|
||||
expiryDate: parsed.data.expiryDate ? dayjs(parsed.data.expiryDate).toDate() : undefined,
|
||||
updatedAt: dayjs().toDate(),
|
||||
})
|
||||
.where(eq(idTag.idTag, tagId))
|
||||
.returning();
|
||||
|
||||
168
apps/csms/src/routes/stats-chart.ts
Normal file
168
apps/csms/src/routes/stats-chart.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Hono } from "hono";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { transaction, connector } from "@/db/schema.js";
|
||||
import type { HonoEnv } from "@/types/hono.ts";
|
||||
|
||||
const app = new Hono<HonoEnv>();
|
||||
|
||||
/**
|
||||
* GET /api/stats/chart?range=30d|7d|24h
|
||||
* 返回时序图表数据,按日(30d/7d)或小时(24h)分组
|
||||
* 仅管理员可访问
|
||||
*/
|
||||
app.get("/", async (c) => {
|
||||
const currentUser = c.get("user");
|
||||
if (!currentUser || currentUser.role !== "admin") {
|
||||
return c.json({ error: "Forbidden" }, 403);
|
||||
}
|
||||
|
||||
const range = c.req.query("range") ?? "7d";
|
||||
const db = useDrizzle();
|
||||
|
||||
// 真实插口数量(connectorId >= 1),至少为 1 防止除零
|
||||
const [{ cnt }] = await db
|
||||
.select({ cnt: sql<number>`greatest(count(*)::int, 1)` })
|
||||
.from(connector)
|
||||
.where(sql`${connector.connectorId} >= 1`);
|
||||
|
||||
if (range === "24h") {
|
||||
// 按小时分组,最近 24 小时
|
||||
const rows = await db.execute(sql`
|
||||
SELECT
|
||||
to_char(
|
||||
date_trunc('hour', generate_series),
|
||||
'YYYY-MM-DD"T"HH24:MI:SS"Z"'
|
||||
) AS bucket,
|
||||
coalesce(
|
||||
(
|
||||
SELECT coalesce(sum(${transaction.stopMeterValue} - ${transaction.startMeterValue}), 0)::float / 1000
|
||||
FROM ${transaction}
|
||||
WHERE date_trunc('hour', ${transaction.stopTimestamp} AT TIME ZONE 'UTC') = date_trunc('hour', generate_series)
|
||||
AND ${transaction.stopTimestamp} IS NOT NULL
|
||||
), 0
|
||||
) AS energy_kwh,
|
||||
coalesce(
|
||||
(
|
||||
SELECT coalesce(sum(${transaction.chargeAmount}), 0)::float / 100
|
||||
FROM ${transaction}
|
||||
WHERE date_trunc('hour', ${transaction.stopTimestamp} AT TIME ZONE 'UTC') = date_trunc('hour', generate_series)
|
||||
AND ${transaction.stopTimestamp} IS NOT NULL
|
||||
), 0
|
||||
) AS revenue,
|
||||
coalesce(
|
||||
(
|
||||
SELECT count(*)::int
|
||||
FROM ${transaction}
|
||||
WHERE date_trunc('hour', ${transaction.stopTimestamp} AT TIME ZONE 'UTC') = date_trunc('hour', generate_series)
|
||||
AND ${transaction.stopTimestamp} IS NOT NULL
|
||||
), 0
|
||||
) AS tx_count,
|
||||
round(coalesce(
|
||||
(
|
||||
SELECT extract(epoch from sum(
|
||||
least(coalesce(t.stop_timestamp, now()), date_trunc('hour', generate_series) + interval '1 hour')
|
||||
- greatest(t.start_timestamp, date_trunc('hour', generate_series))
|
||||
)) / (${sql.raw(String(cnt))} * 3600.0) * 100
|
||||
FROM "transaction" t
|
||||
WHERE t.start_timestamp < date_trunc('hour', generate_series) + interval '1 hour'
|
||||
AND (t.stop_timestamp IS NULL OR t.stop_timestamp > date_trunc('hour', generate_series))
|
||||
), 0
|
||||
)::numeric, 1) AS utilization_pct
|
||||
FROM generate_series(
|
||||
date_trunc('hour', now() AT TIME ZONE 'UTC') - interval '23 hours',
|
||||
date_trunc('hour', now() AT TIME ZONE 'UTC'),
|
||||
interval '1 hour'
|
||||
) AS generate_series
|
||||
ORDER BY bucket ASC
|
||||
`);
|
||||
|
||||
type Row24h = {
|
||||
bucket: string;
|
||||
energy_kwh: number;
|
||||
revenue: number;
|
||||
tx_count: number;
|
||||
utilization_pct: number;
|
||||
};
|
||||
return c.json(
|
||||
(rows.rows as Row24h[]).map((r) => ({
|
||||
bucket: r.bucket,
|
||||
energyKwh: Number(r.energy_kwh),
|
||||
revenue: Number(r.revenue),
|
||||
transactions: Number(r.tx_count),
|
||||
utilizationPct: Number(r.utilization_pct),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// 按天分组,7d 或 30d
|
||||
const days = range === "30d" ? 29 : 6;
|
||||
|
||||
const rows = await db.execute(sql`
|
||||
SELECT
|
||||
to_char(
|
||||
date_trunc('day', generate_series),
|
||||
'YYYY-MM-DD"T"HH24:MI:SS"Z"'
|
||||
) AS bucket,
|
||||
coalesce(
|
||||
(
|
||||
SELECT coalesce(sum(${transaction.stopMeterValue} - ${transaction.startMeterValue}), 0)::float / 1000
|
||||
FROM ${transaction}
|
||||
WHERE date_trunc('day', ${transaction.stopTimestamp} AT TIME ZONE 'UTC') = date_trunc('day', generate_series)
|
||||
AND ${transaction.stopTimestamp} IS NOT NULL
|
||||
), 0
|
||||
) AS energy_kwh,
|
||||
coalesce(
|
||||
(
|
||||
SELECT coalesce(sum(${transaction.chargeAmount}), 0)::float / 100
|
||||
FROM ${transaction}
|
||||
WHERE date_trunc('day', ${transaction.stopTimestamp} AT TIME ZONE 'UTC') = date_trunc('day', generate_series)
|
||||
AND ${transaction.stopTimestamp} IS NOT NULL
|
||||
), 0
|
||||
) AS revenue,
|
||||
coalesce(
|
||||
(
|
||||
SELECT count(*)::int
|
||||
FROM ${transaction}
|
||||
WHERE date_trunc('day', ${transaction.stopTimestamp} AT TIME ZONE 'UTC') = date_trunc('day', generate_series)
|
||||
AND ${transaction.stopTimestamp} IS NOT NULL
|
||||
), 0
|
||||
) AS tx_count,
|
||||
round(coalesce(
|
||||
(
|
||||
SELECT extract(epoch from sum(
|
||||
least(coalesce(t.stop_timestamp, now()), date_trunc('day', generate_series) + interval '1 day')
|
||||
- greatest(t.start_timestamp, date_trunc('day', generate_series))
|
||||
)) / (${sql.raw(String(cnt))} * 86400.0) * 100
|
||||
FROM "transaction" t
|
||||
WHERE t.start_timestamp < date_trunc('day', generate_series) + interval '1 day'
|
||||
AND (t.stop_timestamp IS NULL OR t.stop_timestamp > date_trunc('day', generate_series))
|
||||
), 0
|
||||
)::numeric, 1) AS utilization_pct
|
||||
FROM generate_series(
|
||||
date_trunc('day', now() AT TIME ZONE 'UTC') - interval '${sql.raw(String(days))} days',
|
||||
date_trunc('day', now() AT TIME ZONE 'UTC'),
|
||||
interval '1 day'
|
||||
) AS generate_series
|
||||
ORDER BY bucket ASC
|
||||
`);
|
||||
|
||||
type RowDay = {
|
||||
bucket: string;
|
||||
energy_kwh: number;
|
||||
revenue: number;
|
||||
tx_count: number;
|
||||
utilization_pct: number;
|
||||
};
|
||||
return c.json(
|
||||
(rows.rows as RowDay[]).map((r) => ({
|
||||
bucket: r.bucket,
|
||||
energyKwh: Number(r.energy_kwh),
|
||||
revenue: Number(r.revenue),
|
||||
transactions: Number(r.tx_count),
|
||||
utilizationPct: Number(r.utilization_pct),
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Hono } from "hono";
|
||||
import { and, desc, eq, isNull, isNotNull, sql } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { transaction, chargePoint, connector, idTag } from "@/db/schema.js";
|
||||
import { user } from "@/db/auth-schema.js";
|
||||
@@ -206,7 +207,7 @@ app.post("/:id/stop", async (c) => {
|
||||
if (!row) return c.json({ error: "Not found" }, 404);
|
||||
if (row.transaction.stopTimestamp) return c.json({ error: "Transaction already stopped" }, 409);
|
||||
|
||||
const now = new Date();
|
||||
const now = dayjs();
|
||||
|
||||
// Try to send RemoteStopTransaction via OCPP if the charge point is online
|
||||
const ws = row.chargePointIdentifier ? ocppConnections.get(row.chargePointIdentifier) : null;
|
||||
@@ -233,11 +234,11 @@ app.post("/:id/stop", async (c) => {
|
||||
const [updated] = await db
|
||||
.update(transaction)
|
||||
.set({
|
||||
stopTimestamp: now,
|
||||
stopTimestamp: now.toDate(),
|
||||
stopMeterValue,
|
||||
stopReason: "Remote",
|
||||
chargeAmount: feeFen,
|
||||
updatedAt: now,
|
||||
updatedAt: now.toDate(),
|
||||
})
|
||||
.where(eq(transaction.id, id))
|
||||
.returning();
|
||||
@@ -247,7 +248,7 @@ app.post("/:id/stop", async (c) => {
|
||||
.update(idTag)
|
||||
.set({
|
||||
balance: sql`GREATEST(0, ${idTag.balance} - ${feeFen})`,
|
||||
updatedAt: now,
|
||||
updatedAt: now.toDate(),
|
||||
})
|
||||
.where(eq(idTag.idTag, row.transaction.idTag));
|
||||
}
|
||||
@@ -278,7 +279,7 @@ app.delete("/:id", async (c) => {
|
||||
if (!row.transaction.stopTimestamp) {
|
||||
await db
|
||||
.update(connector)
|
||||
.set({ status: "Available", updatedAt: new Date() })
|
||||
.set({ status: "Available", updatedAt: dayjs().toDate() })
|
||||
.where(eq(connector.id, row.transaction.connectorId));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Hono } from "hono";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { user } from "@/db/schema.js";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
@@ -57,7 +58,7 @@ app.patch("/:id", zValidator("json", userUpdateSchema), async (c) => {
|
||||
.update(user)
|
||||
.set({
|
||||
...body,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
})
|
||||
.where(eq(user.id, userId))
|
||||
.returning({
|
||||
|
||||
Reference in New Issue
Block a user