This commit is contained in:
112
frontend/src/lib/api.ts
Normal file
112
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
details?: unknown;
|
||||
|
||||
constructor(message: string, status: number, details?: unknown) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FetchJsonOptions extends Omit<RequestInit, 'body'> {
|
||||
body?: unknown;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
const API_BASE = import.meta.env.PUBLIC_API_URL || 'http://localhost:8001/api/';
|
||||
|
||||
function buildUrl(endpoint: string): string {
|
||||
return new URL(endpoint.replace(/^\/+/, ''), API_BASE).toString();
|
||||
}
|
||||
|
||||
function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
||||
if (!headers) return {};
|
||||
if (headers instanceof Headers) {
|
||||
return Object.fromEntries(headers.entries());
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
return Object.fromEntries(headers);
|
||||
}
|
||||
return { ...headers };
|
||||
}
|
||||
|
||||
function createTimeoutSignal(signal: AbortSignal | null | undefined, timeoutMs: number): {
|
||||
signal: AbortSignal;
|
||||
clear: () => void;
|
||||
} {
|
||||
const timeoutController = new AbortController();
|
||||
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
||||
|
||||
const clear = () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
clear();
|
||||
timeoutController.abort();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
return { signal: timeoutController.signal, clear };
|
||||
}
|
||||
|
||||
export async function fetchJson<T>(endpoint: string, options: FetchJsonOptions = {}): Promise<T> {
|
||||
const {
|
||||
body,
|
||||
headers,
|
||||
timeoutMs = 10000,
|
||||
signal,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
const requestHeaders: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
...normalizeHeaders(headers),
|
||||
};
|
||||
|
||||
let requestBody: BodyInit | undefined;
|
||||
if (body !== undefined && body !== null) {
|
||||
requestHeaders['Content-Type'] = 'application/json';
|
||||
requestBody = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const timeout = createTimeoutSignal(signal, timeoutMs);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(buildUrl(endpoint), {
|
||||
...rest,
|
||||
headers: requestHeaders,
|
||||
body: requestBody,
|
||||
signal: timeout.signal,
|
||||
});
|
||||
} finally {
|
||||
timeout.clear();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let details: unknown = null;
|
||||
try {
|
||||
details = await response.json();
|
||||
} catch {
|
||||
details = await response.text().catch(() => null);
|
||||
}
|
||||
const message =
|
||||
typeof details === 'object' && details && 'detail' in details
|
||||
? String((details as { detail: unknown }).detail)
|
||||
: `API request failed (${response.status})`;
|
||||
throw new ApiError(message, response.status, details);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
53
frontend/src/lib/db.ts
Normal file
53
frontend/src/lib/db.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
|
||||
|
||||
const DB_NAME = 'erp-cache-db';
|
||||
const DB_VERSION = 1;
|
||||
const CACHE_STORE = 'api_cache';
|
||||
|
||||
interface CacheEntry<T = unknown> {
|
||||
key: string;
|
||||
payload: T;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ERPDBSchema extends DBSchema {
|
||||
api_cache: {
|
||||
key: string;
|
||||
value: CacheEntry;
|
||||
};
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<ERPDBSchema>> | null = null;
|
||||
|
||||
function getDB(): Promise<IDBPDatabase<ERPDBSchema>> {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<ERPDBSchema>(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains(CACHE_STORE)) {
|
||||
db.createObjectStore(CACHE_STORE, { keyPath: 'key' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
export async function setCachedData<T>(key: string, payload: T): Promise<void> {
|
||||
const db = await getDB();
|
||||
await db.put(CACHE_STORE, {
|
||||
key,
|
||||
payload,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCachedData<T>(key: string): Promise<CacheEntry<T> | null> {
|
||||
const db = await getDB();
|
||||
const row = await db.get(CACHE_STORE, key);
|
||||
return (row as CacheEntry<T> | undefined) ?? null;
|
||||
}
|
||||
|
||||
export async function deleteCachedData(key: string): Promise<void> {
|
||||
const db = await getDB();
|
||||
await db.delete(CACHE_STORE, key);
|
||||
}
|
||||
22
frontend/src/lib/displayIds.js
Normal file
22
frontend/src/lib/displayIds.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export function shortUuid(id, size = 8) {
|
||||
if (id == null) return '';
|
||||
const raw = String(id).trim();
|
||||
if (!raw) return '';
|
||||
const compact = raw.split('-')[0] || raw;
|
||||
return compact.slice(0, size).toUpperCase();
|
||||
}
|
||||
|
||||
export function formatEntityCode(prefix, id, size = 8) {
|
||||
const short = shortUuid(id, size);
|
||||
if (!short) return `${prefix}-`;
|
||||
return `${prefix}-${short}`;
|
||||
}
|
||||
|
||||
export function formatPurposeLabel(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (!normalized) return '';
|
||||
if (normalized === 'defektaza') return 'Defektaža';
|
||||
if (normalized === 'kontrola') return 'Kontrola';
|
||||
if (normalized === 'redovni_pregled') return 'Redovni pregled';
|
||||
return value || '';
|
||||
}
|
||||
Reference in New Issue
Block a user