first commit
This commit is contained in:
commit
367533c02b
171 files changed
+101175
No files matched your search
@@ -0,0 +1,220 @@
|
||||
import type {
|
||||
AIInsight,
|
||||
ChickenCounting,
|
||||
ChickenWeight,
|
||||
CountingWrite,
|
||||
Cycle,
|
||||
CycleFilter,
|
||||
CycleWrite,
|
||||
Flock,
|
||||
InitialBalanceResult,
|
||||
Karung,
|
||||
KarungRequestResult,
|
||||
Kandang,
|
||||
KandangWrite,
|
||||
KPI,
|
||||
LatestAvg,
|
||||
ManualInput,
|
||||
ManualInputWrite,
|
||||
Paginated,
|
||||
PanelIoT,
|
||||
Site,
|
||||
SiteWrite,
|
||||
User,
|
||||
} from '../types/api.ts';
|
||||
|
||||
const API_BASE = (import.meta.env.VITE_API_BASE || '/api/v1').replace(/\/$/, '');
|
||||
|
||||
export const AUTH_UNAUTHORIZED_EVENT = 'dashboard-auth-unauthorized';
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
body: unknown;
|
||||
|
||||
constructor(message: string, status: number, body: unknown) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
export function errorMessage(err: unknown): string {
|
||||
if (err instanceof ApiError) return err.message;
|
||||
if (err instanceof Error) return err.message;
|
||||
return 'Terjadi kesalahan';
|
||||
}
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
|
||||
return match?.[1] ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
function parseDetail(body: unknown): string {
|
||||
if (!body || typeof body !== 'object') return 'Request failed';
|
||||
const detail = (body as { detail?: unknown }).detail;
|
||||
if (typeof detail === 'string') return detail;
|
||||
if (Array.isArray(detail)) return detail.map(String).join(', ');
|
||||
const entries = Object.entries(body as Record<string, unknown>)
|
||||
.filter(([key]) => key !== 'detail')
|
||||
.map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : String(value)}`);
|
||||
return entries.length > 0 ? entries.join('; ') : 'Request failed';
|
||||
}
|
||||
|
||||
async function ensureCsrf(): Promise<string> {
|
||||
const existing = getCookie('csrftoken');
|
||||
if (existing) return existing;
|
||||
const res = await fetch(`${API_BASE}/auth/csrf/`, { credentials: 'include' });
|
||||
if (!res.ok) {
|
||||
throw new ApiError('Gagal mengambil CSRF token', res.status, null);
|
||||
}
|
||||
const data = (await res.json()) as { csrfToken?: string };
|
||||
return data.csrfToken || getCookie('csrftoken') || '';
|
||||
}
|
||||
|
||||
type Query = Record<string, string | number | boolean | null | undefined>;
|
||||
|
||||
function withQuery(path: string, query?: Query): string {
|
||||
if (!query) return path;
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
params.set(key, String(value));
|
||||
}
|
||||
const qs = params.toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const method = (init.method || 'GET').toUpperCase();
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set('Accept', 'application/json');
|
||||
if (init.body && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
const csrf = await ensureCsrf();
|
||||
if (csrf) headers.set('X-CSRFToken', csrf);
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
...init,
|
||||
method,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
let body: unknown = null;
|
||||
if (text) {
|
||||
try {
|
||||
body = JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
body = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
window.dispatchEvent(new CustomEvent(AUTH_UNAUTHORIZED_EVENT));
|
||||
}
|
||||
throw new ApiError(parseDetail(body), res.status, body);
|
||||
}
|
||||
return body as T;
|
||||
}
|
||||
|
||||
function unwrapList<T>(data: Paginated<T> | T[]): T[] {
|
||||
if (Array.isArray(data)) return data;
|
||||
return data.results ?? [];
|
||||
}
|
||||
|
||||
const get = <T>(path: string, query?: Query) => request<T>(withQuery(path, query));
|
||||
const post = <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined });
|
||||
const patch = <T>(path: string, body: unknown) =>
|
||||
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) });
|
||||
const del = (path: string) => request<void>(path, { method: 'DELETE' });
|
||||
|
||||
export const api = {
|
||||
auth: {
|
||||
csrf: () => get<{ csrfToken: string }>('/auth/csrf/'),
|
||||
login: (user_name: string, password: string) =>
|
||||
post<User>('/auth/login/', { user_name, password }),
|
||||
logout: () => post<void>('/auth/logout/'),
|
||||
me: () => get<User>('/auth/me/'),
|
||||
},
|
||||
sites: {
|
||||
list: async (query?: { user_id?: number }) =>
|
||||
unwrapList(await get<Paginated<Site> | Site[]>('/sites/', query)),
|
||||
create: (body: SiteWrite) => post<Site>('/sites/', body),
|
||||
update: (id: number, body: Partial<SiteWrite>) => patch<Site>(`/sites/${id}/`, body),
|
||||
remove: (id: number) => del(`/sites/${id}/`),
|
||||
},
|
||||
kandangs: {
|
||||
list: async (query?: { site_id?: number }) =>
|
||||
unwrapList(await get<Paginated<Kandang> | Kandang[]>('/kandangs/', query)),
|
||||
create: (body: KandangWrite) => post<Kandang>('/kandangs/', body),
|
||||
update: (id: number, body: Partial<KandangWrite>) => patch<Kandang>(`/kandangs/${id}/`, body),
|
||||
remove: (id: number) => del(`/kandangs/${id}/`),
|
||||
},
|
||||
flocks: {
|
||||
list: async (query?: { cycle_id?: number }) =>
|
||||
unwrapList(await get<Paginated<Flock> | Flock[]>('/flocks/', query)),
|
||||
},
|
||||
cycles: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<Cycle> | Cycle[]>('/cycles/', query)),
|
||||
create: (body: CycleWrite) => post<Cycle>('/cycles/', body),
|
||||
update: (id: number, body: Partial<CycleWrite>) => patch<Cycle>(`/cycles/${id}/`, body),
|
||||
remove: (id: number) => del(`/cycles/${id}/`),
|
||||
initialBalance: (id: number, body: { date: string; feed_in_manual: number }) =>
|
||||
post<InitialBalanceResult>(`/cycles/${id}/initial-balance/`, body),
|
||||
},
|
||||
kpis: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<KPI> | KPI[]>('/kpis/', query)),
|
||||
},
|
||||
manualInputs: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<ManualInput> | ManualInput[]>('/manual-inputs/', query)),
|
||||
create: (body: ManualInputWrite) => post<ManualInput>('/manual-inputs/', body),
|
||||
update: (id: number, body: Partial<ManualInputWrite>) =>
|
||||
patch<ManualInput>(`/manual-inputs/${id}/`, body),
|
||||
},
|
||||
countings: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(
|
||||
await get<Paginated<ChickenCounting> | ChickenCounting[]>('/chicken-countings/', query)
|
||||
),
|
||||
create: (body: CountingWrite) => post<ChickenCounting>('/chicken-countings/', body),
|
||||
update: (id: number, body: Partial<CountingWrite>) =>
|
||||
patch<ChickenCounting>(`/chicken-countings/${id}/`, body),
|
||||
remove: (id: number) => del(`/chicken-countings/${id}/`),
|
||||
},
|
||||
weights: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<ChickenWeight> | ChickenWeight[]>('/chicken-weights/', query)),
|
||||
latestAvg: (query?: CycleFilter) => get<LatestAvg>('/chicken-weights/latest-avg/', query),
|
||||
},
|
||||
karungs: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<Karung> | Karung[]>('/karungs/', query)),
|
||||
latest: (query?: CycleFilter) => get<Karung>('/karungs/latest/', query),
|
||||
request: (body: { cycle_id: number; section?: 'today' | 'yesterday_history' }) =>
|
||||
post<KarungRequestResult>('/karungs/request/', body),
|
||||
},
|
||||
panelIot: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<PanelIoT> | PanelIoT[]>('/panel-iot/', query)),
|
||||
dates: async (query?: CycleFilter) => get<string[]>('/panel-iot/dates/', query),
|
||||
},
|
||||
insights: {
|
||||
list: async (query?: CycleFilter) =>
|
||||
unwrapList(await get<Paginated<AIInsight> | AIInsight[]>('/ai-insights/', query)),
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user