export function normalizeApiBase(value: string): string { const base = value.replace(/\/+$/, '') if (/\/api$/.test(base)) return `${base}/v1` return /\/api\/v1$/.test(base) ? base : `${base}/api/v1` } const DASHBOARD_PROXY_API_BASE = '/dashboard/api/v1' function isLocalBrowserHost(hostname: string): boolean { return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' } function shouldUseDashboardProxy(configuredApiBase?: string): boolean { if (typeof window === 'undefined') return false if (!configuredApiBase) return true if (configuredApiBase.startsWith('/')) return false try { const apiUrl = new URL(configuredApiBase) const currentOrigin = window.location?.origin const currentHostname = window.location?.hostname if (!currentOrigin || !currentHostname) return false if (isLocalBrowserHost(currentHostname) && isLocalBrowserHost(apiUrl.hostname)) { return apiUrl.origin !== currentOrigin } if (isLocalBrowserHost(currentHostname)) return false return apiUrl.origin !== currentOrigin } catch { return false } } export function resolveApiBase(): string { if (typeof window === 'undefined') { return normalizeApiBase(process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1') } const configuredApiBase = process.env.NEXT_PUBLIC_API_URL return normalizeApiBase(shouldUseDashboardProxy(configuredApiBase) ? DASHBOARD_PROXY_API_BASE : (configuredApiBase || DASHBOARD_PROXY_API_BASE)) } export function resolveApiOrigin(): string | null { if (typeof window === 'undefined') return null try { return new URL(resolveApiBase(), window.location.origin).origin } catch { return window.location.origin } } export function resolveRealtimeSocketTarget(): { origin: string; path: string } | null { if (typeof window === 'undefined') return null const configuredApiBase = process.env.NEXT_PUBLIC_API_URL if (configuredApiBase && !configuredApiBase.startsWith('/')) { try { return { origin: new URL(normalizeApiBase(configuredApiBase)).origin, path: '/socket.io', } } catch { return null } } const currentHostname = window.location?.hostname if (currentHostname && isLocalBrowserHost(currentHostname)) { return { origin: 'http://localhost:4000', path: '/socket.io', } } return { origin: resolveApiOrigin() ?? window.location.origin, path: '/socket.io', } } export const API_BASE = resolveApiBase() export const EMPLOYEE_PROFILE_KEY = 'employee_profile' export async function apiFetch(path: string, options?: RequestInit): Promise { const isFormData = typeof FormData !== 'undefined' && options?.body instanceof FormData const headers: Record = { ...(options?.headers as Record ?? {}), } if (!isFormData && options?.body !== undefined) { headers['Content-Type'] = 'application/json' } const res = await fetch(`${resolveApiBase()}${path}`, { ...options, headers, credentials: 'include', }) let json: any try { json = await res.json() } catch { throw new Error(`HTTP ${res.status}: ${res.statusText}`) } if (!res.ok) { const issues = Array.isArray(json?.issues) ? json.issues .map((issue: any) => { const path = Array.isArray(issue?.path) && issue.path.length ? `${issue.path.join('.')}: ` : '' return issue?.message ? `${path}${issue.message}` : null }) .filter(Boolean) : [] const message = issues.length ? `${json?.message ?? 'Invalid request'}: ${issues.join('; ')}` : (json?.message ?? json?.error ?? `Request failed with status ${res.status}`) const err = new Error(message) as any err.code = json?.error err.statusCode = res.status err.issues = json?.issues throw err } return (json?.data ?? json) as T } export async function apiFetchServer(path: string, token: string, options?: RequestInit): Promise { const isFormData = typeof FormData !== 'undefined' && options?.body instanceof FormData const res = await fetch(`${resolveApiBase()}${path}`, { ...options, headers: { ...(isFormData ? {} : { 'Content-Type': 'application/json' }), 'Authorization': `Bearer ${token}`, ...(options?.headers as Record ?? {}), }, cache: 'no-store', }) let json: any try { json = await res.json() } catch { throw new Error(`HTTP ${res.status}: ${res.statusText}`) } if (!res.ok) { const err = new Error(json?.message ?? `Request failed with status ${res.status}`) as any err.statusCode = res.status throw err } return (json?.data ?? json) as T }