Files
helios-evcs/apps/web/lib/api.ts
Timothy Yin f7ee298060 feat(charge-points): add pricing mode for charge points with validation
feat(pricing): implement tariff management with peak, valley, and flat pricing
feat(api): add tariff API for fetching and updating pricing configurations
feat(tariff-schema): create database schema for tariff configuration
feat(pricing-page): create UI for displaying and managing pricing tiers
fix(sidebar): update sidebar to include pricing settings link
2026-03-12 17:23:06 +08:00

325 lines
9.3 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 ChargePoint = {
id: string;
chargePointIdentifier: string;
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;
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;
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;
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 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}`),
create: (data: {
chargePointIdentifier: string;
chargePointVendor?: string;
chargePointModel?: string;
registrationStatus?: "Accepted" | "Pending" | "Rejected";
feePerKwh?: number;
pricingMode?: "fixed" | "tou";
}) =>
apiFetch<ChargePoint>("/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;
},
) =>
apiFetch<ChargePoint>(`/api/charge-points/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
}),
delete: (id: string) =>
apiFetch<{ success: true }>(`/api/charge-points/${id}`, { method: "DELETE" }),
},
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) }),
},
};