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(path: string, init?: RequestInit): Promise { 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; } // ── 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; createdAt?: string; updatedAt?: string; }; export type Ocpp16jSettings = { heartbeatInterval: number; }; export type SystemSettings = { ocpp16j: Ocpp16jSettings; }; // ── 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("/api/stats"), chart: (range: ChartRange) => apiFetch(`/api/stats/chart?range=${range}`), }, chargePoints: { list: () => apiFetch("/api/charge-points"), get: (id: string) => apiFetch(`/api/charge-points/${id}`), connections: () => apiFetch("/api/charge-points/connections"), create: (data: { chargePointIdentifier: string; chargePointVendor?: string; chargePointModel?: string; registrationStatus?: "Accepted" | "Pending" | "Rejected"; feePerKwh?: number; pricingMode?: "fixed" | "tou"; deviceName?: string; }) => apiFetch("/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(`/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(`/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(`/api/transactions${qs ? "?" + qs : ""}`); }, get: (id: number) => apiFetch(`/api/transactions/${id}`), stop: (id: number) => apiFetch(`/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("/api/id-tags"), get: (idTag: string) => apiFetch(`/api/id-tags/${idTag}`), claim: () => apiFetch("/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("/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(`/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("/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(`/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("/api/tariff"), put: (data: { slots: TariffSlot[]; prices: Record }) => apiFetch("/api/tariff", { method: "PUT", body: JSON.stringify(data) }), }, settings: { get: () => apiFetch("/api/settings"), put: (data: SystemSettings) => apiFetch("/api/settings", { method: "PUT", body: JSON.stringify(data) }), }, };