feat: add Condo owner desk frontend and backend
This commit is contained in:
142
api-client.js
Normal file
142
api-client.js
Normal file
@@ -0,0 +1,142 @@
|
||||
(function initializeCondoApi(windowObject) {
|
||||
"use strict";
|
||||
|
||||
const config = windowObject.CONDO_RUNTIME_CONFIG || {};
|
||||
const requestedMode = new URLSearchParams(windowObject.location.search).get("mode");
|
||||
const mode = requestedMode === "api"
|
||||
? "api"
|
||||
: requestedMode === "demo"
|
||||
? "demo"
|
||||
: config.defaultMode === "api"
|
||||
? "api"
|
||||
: "demo";
|
||||
const periodYear = Number.isInteger(config.periodYear) ? config.periodYear : new Date().getUTCFullYear();
|
||||
const baseUrl = new URL(config.apiBaseUrl || "http://127.0.0.1:3000", windowObject.location.origin);
|
||||
|
||||
if (!["http:", "https:"].includes(baseUrl.protocol)) {
|
||||
throw new Error("CONDO API base URL must use HTTP or HTTPS.");
|
||||
}
|
||||
|
||||
class CondoApiError extends Error {
|
||||
constructor(message, { status = 0, code = "NETWORK_ERROR" } = {}) {
|
||||
super(message);
|
||||
this.name = "CondoApiError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function apiUrl(path, query = {}) {
|
||||
const url = new URL(path, `${baseUrl.href.replace(/\/$/, "")}/`);
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
});
|
||||
return url;
|
||||
}
|
||||
|
||||
async function request(path, { method = "GET", query, body, signal, timeoutMs = 12_000 } = {}) {
|
||||
const controller = new AbortController();
|
||||
const abortFromCaller = () => controller.abort(signal.reason);
|
||||
if (signal?.aborted) abortFromCaller();
|
||||
else signal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
const timeout = windowObject.setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await windowObject.fetch(apiUrl(path, query), {
|
||||
method,
|
||||
credentials: "include",
|
||||
headers: body === undefined ? { Accept: "application/json" } : {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: controller.signal
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new CondoApiError(
|
||||
payload?.error?.message || `API request failed with status ${response.status}`,
|
||||
{ status: response.status, code: payload?.error?.code || "API_ERROR" }
|
||||
);
|
||||
}
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error instanceof CondoApiError) throw error;
|
||||
const aborted = controller.signal.aborted;
|
||||
throw new CondoApiError(
|
||||
aborted ? "The API request timed out or was cancelled." : "The API is unavailable.",
|
||||
{ code: aborted ? "REQUEST_ABORTED" : "NETWORK_ERROR" }
|
||||
);
|
||||
} finally {
|
||||
windowObject.clearTimeout(timeout);
|
||||
signal?.removeEventListener("abort", abortFromCaller);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectPages(loader, options = {}) {
|
||||
const first = await loader({ ...options, page: 1, pageSize: 100 });
|
||||
const items = [...first.items];
|
||||
const pageCount = Math.ceil(first.total / first.pageSize);
|
||||
for (let page = 2; page <= pageCount; page += 1) {
|
||||
const next = await loader({ ...options, page, pageSize: 100 });
|
||||
items.push(...next.items);
|
||||
}
|
||||
return { items, total: first.total };
|
||||
}
|
||||
|
||||
const client = Object.freeze({
|
||||
getSession: options => request("auth/session", options),
|
||||
login: (credentials, options = {}) => request("auth/login", {
|
||||
...options,
|
||||
method: "POST",
|
||||
body: credentials
|
||||
}),
|
||||
logout: options => request("auth/logout", {
|
||||
...options,
|
||||
method: "POST"
|
||||
}),
|
||||
health: options => request("health", options),
|
||||
listRoomTypes: options => request("room-types", options),
|
||||
listOwnerAccounts: ({ q, roomType, year = periodYear, page = 1, pageSize = 100, ...options } = {}) => request("owner-accounts", {
|
||||
...options,
|
||||
query: { q, roomType, year, page, pageSize }
|
||||
}),
|
||||
listAllOwnerAccounts: ({ q, roomType, year = periodYear, ...options } = {}) => collectPages(
|
||||
pageOptions => client.listOwnerAccounts(pageOptions),
|
||||
{ q, roomType, year, ...options }
|
||||
),
|
||||
getOwnerAccount: (id, { year = periodYear, ...options } = {}) => request(`owner-accounts/${encodeURIComponent(id)}`, {
|
||||
...options,
|
||||
query: { year }
|
||||
}),
|
||||
listUsageRecords: ({ ownerAccountId, confirmationNo, usedRoomType, page = 1, pageSize = 100, ...options } = {}) => request("usage-records", {
|
||||
...options,
|
||||
query: { ownerAccountId, confirmationNo, usedRoomType, page, pageSize }
|
||||
}),
|
||||
listAllUsageRecords: ({ ownerAccountId, confirmationNo, usedRoomType, ...options } = {}) => collectPages(
|
||||
pageOptions => client.listUsageRecords(pageOptions),
|
||||
{ ownerAccountId, confirmationNo, usedRoomType, ...options }
|
||||
),
|
||||
getDashboard: ({ year = periodYear, ...options } = {}) => request("dashboard", {
|
||||
...options,
|
||||
query: { year }
|
||||
}),
|
||||
createUsageRecord: (input, options = {}) => request("usage-records", {
|
||||
...options,
|
||||
method: "POST",
|
||||
body: input
|
||||
})
|
||||
});
|
||||
|
||||
windowObject.CONDO_API = Object.freeze({
|
||||
runtime: Object.freeze({
|
||||
mode,
|
||||
apiBaseUrl: baseUrl.href.replace(/\/$/, ""),
|
||||
periodYear
|
||||
}),
|
||||
client,
|
||||
CondoApiError
|
||||
});
|
||||
})(window);
|
||||
Reference in New Issue
Block a user