Files
helios-evcs/apps/web/lib/api.ts

353 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export class APIError extends Error {
constructor(
public readonly status: number,
message: string,
) {
super(message);
this.name = "APIError";
}
}
const CSMS_URL = process.env.NEXT_PUBLIC_CSMS_URL ?? "http://localhost:3001";
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${CSMS_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...init?.headers,
},
credentials: "include",
});
if (!res.ok) {
const text = await res.text().catch(() => res.statusText);
throw new APIError(res.status, `API ${path} failed (${res.status}): ${text}`);
}
return res.json() as Promise<T>;
}
// ── Types ──────────────────────────────────────────────────────────────────
export type Stats = {
totalChargePoints: number;
onlineChargePoints: number;
activeTransactions: number;
totalIdTags: number;
todayEnergyWh: number;
todayRevenue: number;
totalUsers: number;
todayTransactions: number;
};
export type UserStats = {
totalIdTags: number;
totalBalance: number;
activeTransactions: number;
totalTransactions: number;
todayEnergyWh: number;
todayTransactions: number;
};
export type ConnectorSummary = {
id: string;
connectorId: number;
status: string;
lastStatusAt: string | null;
};
export type ConnectorDetail = {
id: string;
connectorId: number;
status: string;
errorCode: string;
info: string | null;
vendorId: string | null;
vendorErrorCode: string | null;
lastStatusAt: string;
createdAt: string;
updatedAt: string;
};
export type ConnectionsStatus = {
connectedIdentifiers: string[];
};
export type ChargePoint = {
id: string;
chargePointIdentifier: string;
deviceName: string | null;
chargePointVendor: string | null;
chargePointModel: string | null;
registrationStatus: string;
lastHeartbeatAt: string | null;
lastBootNotificationAt: string | null;
feePerKwh: number;
pricingMode: "fixed" | "tou";
connectors: ConnectorSummary[];
chargePointStatus: string | null;
chargePointErrorCode: string | null;
};
export type ChargePointDetail = {
id: string;
chargePointIdentifier: string;
deviceName: string | null;
chargePointVendor: string | null;
chargePointModel: string | null;
chargePointSerialNumber: string | null;
firmwareVersion: string | null;
iccid: string | null;
imsi: string | null;
meterSerialNumber: string | null;
meterType: string | null;
registrationStatus: string;
heartbeatInterval: number | null;
lastHeartbeatAt: string | null;
lastBootNotificationAt: string | null;
feePerKwh: number;
pricingMode: "fixed" | "tou";
createdAt: string;
updatedAt: string;
connectors: ConnectorDetail[];
chargePointStatus: string | null;
chargePointErrorCode: string | null;
};
export type Transaction = {
id: number;
chargePointIdentifier: string | null;
chargePointDeviceName: string | null;
connectorNumber: number | null;
idTag: string;
idTagStatus: string | null;
idTagUserId: string | null;
idTagUserName: string | null;
startTimestamp: string;
stopTimestamp: string | null;
startMeterValue: number | null;
stopMeterValue: number | null;
energyWh: number | null;
liveEnergyWh: number | null;
estimatedCost: number | null;
stopIdTag: string | null;
stopReason: string | null;
chargeAmount: number | null;
electricityFee: number | null;
serviceFee: number | null;
};
export type IdTag = {
idTag: string;
status: string;
expiryDate: string | null;
parentIdTag: string | null;
userId: string | null;
balance: number;
cardLayout: "center" | "around" | null;
cardSkin: "line" | "circles" | "glow" | "vip" | "redeye" | null;
createdAt: string;
};
export type UserRow = {
id: string;
name: string | null;
email: string;
emailVerified: boolean;
username: string | null;
role: string | null;
banned: boolean | null;
banReason: string | null;
createdAt: string;
};
export type ChargePointCreated = ChargePoint & {
/** 仅在创建时返回一次的明文密码,之后不可再查 */
plainPassword: string;
};
export type ChargePointPasswordReset = {
id: string;
chargePointIdentifier: string;
/** 仅在重置时返回一次的新明文密码 */
plainPassword: string;
};
export type PaginatedTransactions = {
data: Transaction[];
total: number;
page: number;
totalPages: number;
};
export type PriceTier = "peak" | "valley" | "flat";
export type TariffSlot = {
start: number;
end: number;
tier: PriceTier;
};
export type TierPricing = {
/** 电价(元/kWh */
electricityPrice: number;
/** 服务费(元/kWh */
serviceFee: number;
};
export type TariffConfig = {
id?: string;
slots: TariffSlot[];
prices: Record<PriceTier, TierPricing>;
createdAt?: string;
updatedAt?: string;
};
// ── API functions ──────────────────────────────────────────────────────────
export type ChartRange = "30d" | "7d" | "24h";
export type ChartDataPoint = {
bucket: string;
energyKwh: number;
revenue: number;
transactions: number;
utilizationPct: number;
};
export const api = {
stats: {
get: () => apiFetch<Stats | UserStats>("/api/stats"),
chart: (range: ChartRange) =>
apiFetch<ChartDataPoint[]>(`/api/stats/chart?range=${range}`),
},
chargePoints: {
list: () => apiFetch<ChargePoint[]>("/api/charge-points"),
get: (id: string) => apiFetch<ChargePointDetail>(`/api/charge-points/${id}`),
connections: () => apiFetch<ConnectionsStatus>("/api/charge-points/connections"),
create: (data: {
chargePointIdentifier: string;
chargePointVendor?: string;
chargePointModel?: string;
registrationStatus?: "Accepted" | "Pending" | "Rejected";
feePerKwh?: number;
pricingMode?: "fixed" | "tou";
deviceName?: string;
}) =>
apiFetch<ChargePointCreated>("/api/charge-points", {
method: "POST",
body: JSON.stringify(data),
}),
update: (
id: string,
data: {
feePerKwh?: number;
pricingMode?: "fixed" | "tou";
registrationStatus?: "Accepted" | "Pending" | "Rejected";
chargePointVendor?: string;
chargePointModel?: string;
deviceName?: string | null;
},
) =>
apiFetch<ChargePoint>(`/api/charge-points/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
}),
delete: (id: string) =>
apiFetch<{ success: true }>(`/api/charge-points/${id}`, { method: "DELETE" }),
resetPassword: (id: string) =>
apiFetch<ChargePointPasswordReset>(`/api/charge-points/${id}/reset-password`, {
method: "POST",
}),
},
transactions: {
list: (params?: {
page?: number;
limit?: number;
status?: "active" | "completed";
chargePointId?: string;
}) => {
const q = new URLSearchParams();
if (params?.page) q.set("page", String(params.page));
if (params?.limit) q.set("limit", String(params.limit));
if (params?.status) q.set("status", params.status);
if (params?.chargePointId) q.set("chargePointId", params.chargePointId);
const qs = q.toString();
return apiFetch<PaginatedTransactions>(`/api/transactions${qs ? "?" + qs : ""}`);
},
get: (id: number) => apiFetch<Transaction>(`/api/transactions/${id}`),
stop: (id: number) =>
apiFetch<Transaction & { online: boolean }>(`/api/transactions/${id}/stop`, {
method: "POST",
}),
remoteStart: (data: { chargePointIdentifier: string; connectorId: number; idTag: string }) =>
apiFetch<{ success: true }>("/api/transactions/remote-start", {
method: "POST",
body: JSON.stringify(data),
}),
delete: (id: number) =>
apiFetch<{ success: true }>(`/api/transactions/${id}`, { method: "DELETE" }),
},
idTags: {
list: () => apiFetch<IdTag[]>("/api/id-tags"),
get: (idTag: string) => apiFetch<IdTag>(`/api/id-tags/${idTag}`),
claim: () => apiFetch<IdTag>("/api/id-tags/claim", { method: "POST" }),
create: (data: {
idTag: string;
status?: string;
expiryDate?: string;
parentIdTag?: string;
userId?: string | null;
balance?: number;
cardLayout?: "center" | "around";
cardSkin?: "line" | "circles" | "glow" | "vip" | "redeye";
}) => apiFetch<IdTag>("/api/id-tags", { method: "POST", body: JSON.stringify(data) }),
update: (
idTag: string,
data: {
status?: string;
expiryDate?: string | null;
parentIdTag?: string | null;
userId?: string | null;
balance?: number;
cardLayout?: "center" | "around" | null;
cardSkin?: "line" | "circles" | "glow" | "vip" | "redeye" | null;
},
) => apiFetch<IdTag>(`/api/id-tags/${idTag}`, { method: "PATCH", body: JSON.stringify(data) }),
delete: (idTag: string) =>
apiFetch<{ success: true }>(`/api/id-tags/${idTag}`, { method: "DELETE" }),
},
users: {
list: () => apiFetch<UserRow[]>("/api/users"),
create: (data: {
name: string;
email: string;
password: string;
username?: string;
role?: string;
}) =>
apiFetch<{ user: UserRow }>("/api/auth/admin/create-user", {
method: "POST",
body: JSON.stringify(data),
}),
update: (
id: string,
data: {
name?: string;
username?: string | null;
role?: string;
banned?: boolean;
banReason?: string | null;
},
) => apiFetch<UserRow>(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
},
setup: {
create: (data: { name: string; email: string; username: string; password: string }) =>
apiFetch<{ success: boolean }>("/api/setup", { method: "POST", body: JSON.stringify(data) }),
},
tariff: {
get: () => apiFetch<TariffConfig | null>("/api/tariff"),
put: (data: { slots: TariffSlot[]; prices: Record<PriceTier, TierPricing> }) =>
apiFetch<TariffConfig>("/api/tariff", { method: "PUT", body: JSON.stringify(data) }),
},
};