prvi kod za live uporabu
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-09 21:21:17 +02:00
parent 857bb65d52
commit 5d39e048ba
239 changed files with 25151 additions and 0 deletions

112
frontend/src/lib/api.ts Normal file
View 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
View 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);
}

View 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 || '';
}