188 lines
4.3 KiB
TypeScript
188 lines
4.3 KiB
TypeScript
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<T>(path: string, options: ApiFetchOptions = {}): Promise<T> {
|
|
const { data } = await supabase.auth.getSession();
|
|
const token = data.session?.access_token;
|
|
if (!token) {
|
|
throw new ApiError(401, 'Not signed in');
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
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;
|
|
}
|