first commit

This commit is contained in:
Alberto-Audrix committed 2026-08-26 13:46:05 +07:00
commit 367533c02b
171 files changed
+101175

No files matched your search

+220
View File
@@ -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)),
},
};