import { supabase } from './supabase'; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001'; export const ACTIVE_TENANT_STORAGE_KEY = 'ceo-os.active-tenant'; export class ApiError extends Error { constructor( public readonly status: number, message: string, ) { super(message); } } interface ApiFetchOptions { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; body?: unknown; /** Rutele de bootstrap (/v1/me, /v1/tenants) nu trimit tenant header. */ tenantId?: string | null; } /** * Toate apelurile catre ceo-api trec pe aici: Bearer din sesiunea Supabase, * tenantul activ in x-tenant-id (niciodata in body/query — TenantGuard pe * server respinge exact acel pattern). */ export async function apiFetch(path: string, options: ApiFetchOptions = {}): Promise { const { data } = await supabase.auth.getSession(); const token = data.session?.access_token; if (!token) { throw new ApiError(401, 'Not signed in'); } const headers: Record = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }; if (options.tenantId) { headers['x-tenant-id'] = options.tenantId; } const response = await fetch(`${API_URL}${path}`, { method: options.method ?? 'GET', headers, body: options.body !== undefined ? JSON.stringify(options.body) : undefined, }); if (!response.ok) { let message = `Request failed (${response.status})`; try { const payload = (await response.json()) as { message?: string | string[] }; if (payload.message) { message = Array.isArray(payload.message) ? payload.message.join('; ') : payload.message; } } catch { // corpul nu e JSON — pastram mesajul generic } throw new ApiError(response.status, message); } return (await response.json()) as T; } // --- Tipuri aliniate la contractele ceo-api --- export interface TenantMembership { tenantId: string; tenantName: string; role: 'owner' | 'admin' | 'member'; } export interface MeResponse { userId: string; email: string | null; tenants: TenantMembership[]; } export interface Organization { id: string; name: string; legalName: string | null; country: string | null; registryId: string | null; domain: string | null; createdAt: string; } export type TaskStatus = 'open' | 'in_progress' | 'blocked' | 'done' | 'cancelled'; export interface Task { id: string; title: string; priority: number; dueAt: string | null; status: TaskStatus; createdAt: string; } export interface TenantMember { userId: string; email: string | null; role: 'owner' | 'admin' | 'member'; createdAt: string; } export interface CompanySearchResult { organization_id: string; organization_name: string; normalized_domain: string; hq_city: string; hq_country: string; industries: string[]; num_current_employees: number | null; revenue_in_thousands: number | null; } export interface CompanySearchResponse { results: CompanySearchResult[]; count: number; } export interface SavedSegment { id: string; name: string; query: string; resultLimit: number; createdAt: string; } export interface Source { label: string; url: string; } export interface ResearchBrief { id: string; organizationId: string; organizationName: string | null; title: string; summary: string; sources: Source[]; createdAt: string; } export interface Briefing { generatedAt: string; overdueTasks: Task[]; upcomingTasks: Task[]; weekInReview: { newOrganizations: number; newSegments: number; newResearchBriefs: number; }; aiExplanation: string | null; } export type GoalStatus = 'not_started' | 'on_track' | 'at_risk' | 'achieved' | 'abandoned'; export interface Goal { id: string; tenantId: string; ownerUserId: string; horizon: string; metric: string; target: string; milestones: unknown[]; status: GoalStatus; createdAt: string; updatedAt: string; } export type EvidenceStatus = 'missing' | 'partial' | 'complete' | 'not_required'; export interface Transaction { id: string; tenantId: string; organizationId: string; type: string; /** Stored as string from DB (numeric type). */ amountMinorUnits: string; currency: string; transactionDate: string; evidenceStatus: EvidenceStatus; source: string | null; createdAt: string; updatedAt: string; }