forked from sagnik/Project_Velocity
331 lines
8.8 KiB
TypeScript
331 lines
8.8 KiB
TypeScript
import { API_URL } from '@/lib/api';
|
|
import {
|
|
buildVelocityHeaders,
|
|
setVelocityToken,
|
|
} from '@/lib/velocitySession';
|
|
export {
|
|
VELOCITY_TOKEN_KEY,
|
|
clearVelocityToken,
|
|
getVelocityToken,
|
|
setVelocityToken,
|
|
} from '@/lib/velocitySession';
|
|
|
|
export interface VelocityUserProfile {
|
|
user_id: string;
|
|
role: string;
|
|
tenant_id?: string;
|
|
full_name?: string | null;
|
|
email?: string | null;
|
|
avatar_url?: string | null;
|
|
}
|
|
|
|
export interface VelocityActiveUser {
|
|
user_id: string;
|
|
role: string;
|
|
tenant_id?: string;
|
|
full_name?: string | null;
|
|
email?: string | null;
|
|
avatar_url?: string | null;
|
|
}
|
|
|
|
export interface VelocityLoginResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
expires_in: number;
|
|
}
|
|
|
|
export interface AdminHealthSnapshot {
|
|
status: string;
|
|
timestamp: string;
|
|
database: {
|
|
connected: boolean;
|
|
latency_ms: number;
|
|
};
|
|
queues: {
|
|
pending_transcriptions: number;
|
|
pending_synthetic_jobs: number;
|
|
pending_admin_actions: number;
|
|
pending_inventory_batches: number;
|
|
};
|
|
active_sessions: {
|
|
total: number;
|
|
by_surface: Record<string, number>;
|
|
};
|
|
}
|
|
|
|
export interface AdminQueueSnapshot {
|
|
transcription_jobs: Record<string, number>;
|
|
synthetic_jobs: Record<string, number>;
|
|
inventory_batches: Record<string, number>;
|
|
admin_actions: Record<string, number>;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface AdminInstallSnapshot {
|
|
installs: Array<{
|
|
surface_type: string;
|
|
app_version: string;
|
|
session_count: number;
|
|
last_seen: string | null;
|
|
}>;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface AdminActionRecord {
|
|
action_event_id: string;
|
|
action_id: string;
|
|
action_type: string;
|
|
target_type: string;
|
|
target_id: string;
|
|
requested_by: string;
|
|
status: string;
|
|
result_message?: string | null;
|
|
executed_at?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AdminActionRequest {
|
|
action_type: string;
|
|
target_type: string;
|
|
target_id: string;
|
|
payload?: Record<string, unknown>;
|
|
idempotency_key?: string;
|
|
}
|
|
|
|
export interface MobileEdgeAlertSnapshot {
|
|
pending_insights: number;
|
|
upcoming_calendar_events_24h: number;
|
|
pending_transcriptions: number;
|
|
generated_at: string;
|
|
}
|
|
|
|
export interface MobileCalendarEvent {
|
|
calendar_event_id: string;
|
|
lead_id?: string | null;
|
|
title: string;
|
|
description?: string | null;
|
|
start_at: string;
|
|
end_at: string;
|
|
all_day: boolean;
|
|
status: string;
|
|
reminder_minutes: number[];
|
|
created_by: string;
|
|
location?: string | null;
|
|
metadata: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface MobileCommunicationEvent {
|
|
event_id: string;
|
|
lead_id: string;
|
|
channel: string;
|
|
direction: string;
|
|
provider?: string | null;
|
|
capture_mode: string;
|
|
consent_state: string;
|
|
timestamp: string;
|
|
duration_seconds?: number | null;
|
|
summary?: string | null;
|
|
raw_reference?: string | null;
|
|
recording_ref?: string | null;
|
|
provider_metadata: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface InventoryImportBatchSummary {
|
|
batch_id: string;
|
|
source_type: string;
|
|
submitted_by: string;
|
|
status: string;
|
|
total_rows: number;
|
|
accepted_rows: number;
|
|
rejected_rows: number;
|
|
created_at: string;
|
|
completed_at?: string | null;
|
|
}
|
|
|
|
export interface InventoryPropertySummary {
|
|
property_id: string;
|
|
project_name: string;
|
|
developer_name: string;
|
|
property_type: string;
|
|
location: Record<string, unknown>;
|
|
price_bands: Array<Record<string, unknown>>;
|
|
unit_mix: Array<Record<string, unknown>>;
|
|
status: string;
|
|
ingested_at?: string | null;
|
|
created_at?: string | null;
|
|
}
|
|
|
|
function buildHeaders(init?: HeadersInit, includeJson = true): Headers {
|
|
return buildVelocityHeaders(init, includeJson);
|
|
}
|
|
|
|
async function platformFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(`${API_URL}${path}`, {
|
|
...init,
|
|
headers: buildHeaders(init?.headers, init?.body !== undefined),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
typeof body?.detail === 'string'
|
|
? body.detail
|
|
: typeof body?.message === 'string'
|
|
? body.message
|
|
: `Request failed: ${response.status}`,
|
|
);
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export function normalizeVelocityRole(role: string | null | undefined): string {
|
|
return (role ?? '').trim().toUpperCase();
|
|
}
|
|
|
|
export function resolveVelocityFullName(profile: Pick<VelocityUserProfile, 'full_name' | 'email' | 'user_id'>): string {
|
|
const fullName = profile.full_name?.trim();
|
|
if (fullName) {
|
|
return fullName;
|
|
}
|
|
|
|
const email = profile.email?.trim();
|
|
if (email) {
|
|
return email;
|
|
}
|
|
|
|
return profile.user_id;
|
|
}
|
|
|
|
export function resolveVelocityFirstName(profile: Pick<VelocityUserProfile, 'full_name' | 'email' | 'user_id'>): string {
|
|
const fullName = profile.full_name?.trim();
|
|
if (fullName) {
|
|
return fullName.split(/\s+/)[0] ?? fullName;
|
|
}
|
|
|
|
const email = profile.email?.trim();
|
|
if (email) {
|
|
const local = email.split('@')[0]?.trim() ?? '';
|
|
if (local) {
|
|
const normalized = local.replace(/[._-]+/g, ' ').trim();
|
|
const firstToken = normalized.split(/\s+/)[0] ?? normalized;
|
|
return firstToken.charAt(0).toUpperCase() + firstToken.slice(1);
|
|
}
|
|
}
|
|
|
|
return profile.user_id;
|
|
}
|
|
|
|
export function isAdminRole(role: string | null | undefined): boolean {
|
|
const normalized = normalizeVelocityRole(role);
|
|
return normalized === 'ADMIN' || normalized === 'SUPERADMIN';
|
|
}
|
|
|
|
export async function loginVelocity(email: string, password: string): Promise<VelocityUserProfile> {
|
|
const auth = await platformFetch<VelocityLoginResponse>('/api/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
setVelocityToken(auth.access_token);
|
|
return getVelocityMe();
|
|
}
|
|
|
|
export async function getVelocityMe(): Promise<VelocityUserProfile> {
|
|
return platformFetch<VelocityUserProfile>('/api/auth/me', {
|
|
method: 'GET',
|
|
});
|
|
}
|
|
|
|
export async function listVelocityUsers(): Promise<VelocityActiveUser[]> {
|
|
return platformFetch<VelocityActiveUser[]>('/api/auth/users', {
|
|
method: 'GET',
|
|
});
|
|
}
|
|
|
|
export async function uploadVelocityAvatar(file: File): Promise<{ avatar_url: string }> {
|
|
const form = new FormData();
|
|
form.append('file', file);
|
|
|
|
const response = await fetch(`${API_URL}/api/auth/profile/avatar`, {
|
|
method: 'POST',
|
|
headers: buildHeaders(undefined, false),
|
|
body: form,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
typeof body?.detail === 'string'
|
|
? body.detail
|
|
: typeof body?.message === 'string'
|
|
? body.message
|
|
: `Request failed: ${response.status}`,
|
|
);
|
|
}
|
|
|
|
return response.json() as Promise<{ avatar_url: string }>;
|
|
}
|
|
|
|
export async function getAdminHealth(): Promise<AdminHealthSnapshot> {
|
|
return platformFetch<AdminHealthSnapshot>('/api/admin-surface/health');
|
|
}
|
|
|
|
export async function getAdminQueues(): Promise<AdminQueueSnapshot> {
|
|
return platformFetch<AdminQueueSnapshot>('/api/admin-surface/queues');
|
|
}
|
|
|
|
export async function getAdminInstalls(): Promise<AdminInstallSnapshot> {
|
|
return platformFetch<AdminInstallSnapshot>('/api/admin-surface/installs');
|
|
}
|
|
|
|
export async function listAdminActions(limit = 20): Promise<{ actions: AdminActionRecord[] }> {
|
|
return platformFetch<{ actions: AdminActionRecord[] }>(
|
|
`/api/admin-surface/actions?limit=${encodeURIComponent(String(limit))}`,
|
|
);
|
|
}
|
|
|
|
export async function submitAdminAction(body: AdminActionRequest): Promise<{
|
|
action_event_id: string;
|
|
action_id: string;
|
|
status: string;
|
|
created_at: string;
|
|
}> {
|
|
return platformFetch('/api/admin-surface/actions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
export async function getMobileAlerts(): Promise<MobileEdgeAlertSnapshot> {
|
|
return platformFetch<MobileEdgeAlertSnapshot>('/api/mobile-edge/alerts');
|
|
}
|
|
|
|
export async function getMobileCalendarEvents(): Promise<{ events: MobileCalendarEvent[] }> {
|
|
return platformFetch<{ events: MobileCalendarEvent[] }>('/api/mobile-edge/calendar');
|
|
}
|
|
|
|
export async function getMobileEventsByLead(
|
|
leadId: string,
|
|
limit = 20,
|
|
): Promise<{ events: MobileCommunicationEvent[] }> {
|
|
const params = new URLSearchParams({
|
|
lead_id: leadId,
|
|
limit: String(limit),
|
|
});
|
|
return platformFetch<{ events: MobileCommunicationEvent[] }>(`/api/mobile-edge/events?${params.toString()}`);
|
|
}
|
|
|
|
export async function listInventoryImportBatches(limit = 10): Promise<{ batches: InventoryImportBatchSummary[] }> {
|
|
return platformFetch<{ batches: InventoryImportBatchSummary[] }>(
|
|
`/api/inventory/import-batches?limit=${encodeURIComponent(String(limit))}`,
|
|
);
|
|
}
|
|
|
|
export async function listInventoryProperties(limit = 100): Promise<{ properties: InventoryPropertySummary[] }> {
|
|
return platformFetch<{ properties: InventoryPropertySummary[] }>(
|
|
`/api/inventory/properties?limit=${encodeURIComponent(String(limit))}`,
|
|
);
|
|
}
|