import { z } from 'zod'; import { reportError } from '@/lib/error-handling'; import { CrudService } from '@/lib/service-base'; import { SERVICE_CONFIGS } from '@/lib/service-config'; import { backendOrganizationSchema, createOrganizationRequestSchema, type BackendOrganization, type CreateOrganizationRequest, } from '@/schemas/backend/organization'; /** * Search organizations query parameters schema */ export const searchOrganizationsParamsSchema = z.object({ q: z.string().optional(), sectors: z.array(z.string()).optional(), sort: z.enum(['name_asc', 'name_desc', 'sector_asc', 'sector_desc']).optional(), limit: z.coerce.number().positive().optional(), offset: z.coerce.number().nonnegative().optional(), }); export type SearchOrganizationsParams = z.infer; /** * Similar organization schema */ export const similarOrganizationSchema = z.object({ id: z.string(), name: z.string(), sector: z.string(), similarity_score: z.number().min(0).max(1), }); export type SimilarOrganization = z.infer; /** * Search organizations response schema */ export const searchOrganizationsResponseSchema = z.object({ organizations: z.array(backendOrganizationSchema), count: z.number().nonnegative(), total: z.number().nonnegative(), }); export type SearchOrganizationsResponse = z.infer; /** * Similar organizations response schema */ export const similarOrganizationsResponseSchema = z.object({ organizations: z.array(similarOrganizationSchema), }); export type SimilarOrganizationsResponse = z.infer; /** * Organizations Service * Handles all organization-related API operations with improved reliability and type safety */ class OrganizationsService extends CrudService { protected entitySchema = backendOrganizationSchema; protected createSchema = createOrganizationRequestSchema; protected updateSchema = createOrganizationRequestSchema.partial(); // Allow partial updates constructor() { super(SERVICE_CONFIGS.ORGANIZATIONS); } /** * Get all organizations with enhanced error handling */ async getAllOrganizations(): Promise { const result = await this.getAll({ context: 'getAllOrganizations', useArrayFallback: true, }); if (!result.success) { console.error('[DEBUG] Organization validation failed:', result.error); // Temporarily return empty array to see if data flows through return []; } return result.data; } /** * Search organizations using fuzzy search (backend endpoint) * @param query - Search query string * @param limit - Maximum number of results (default: 50, max: 200) */ async search(query: string, limit: number = 50): Promise { if (!query.trim()) { return []; } // Use searchOrganizationsResponseSchema since the API returns {organizations, count, total} const result = await this.get( 'search', searchOrganizationsResponseSchema, { q: query.trim(), limit: Math.min(limit, 200) }, { context: 'searchOrganizations' } ); if (!result.success) { reportError(result.error, 'Failed to search organizations'); return []; } return result.data?.organizations || []; } /** * Get search suggestions/autocomplete * @param query - Search query string * @param limit - Maximum number of suggestions (default: 10, max: 50) */ async getSearchSuggestions(query: string, limit: number = 10): Promise { if (!query.trim()) { return []; } try { const endpoint = this.buildEndpoint('suggestions'); const url = new URL(endpoint, this.config.baseURL); url.searchParams.set('q', query.trim()); url.searchParams.set('limit', Math.min(limit, 50).toString()); const response = await fetch(url.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); const suggestions = z.array(z.string()).parse(data); return suggestions; } catch (error) { console.error('[OrganizationsService] Failed to get search suggestions:', error); return []; } } /** * Search organizations with query parameters */ async searchOrganizations(params?: SearchOrganizationsParams): Promise { const validatedParams = params ? this.validateStrict(searchOrganizationsParamsSchema, params, 'search params') : {}; // Build query using the new QueryBuilder const query = this.createQuery() .whenDefined('q', validatedParams.q) .when(validatedParams.sectors && validatedParams.sectors.length > 0, (qb) => { validatedParams.sectors!.forEach(sector => qb.param('sectors', sector)); return qb; }) .whenDefined('sort', validatedParams.sort) .whenDefined('limit', validatedParams.limit) .whenDefined('offset', validatedParams.offset); const result = await this.get('search', searchOrganizationsResponseSchema, query.toParams(), { context: 'searchOrganizations', }); if (!result.success) { const error = reportError( new Error(result.error?.message || 'Failed to search organizations'), { operation: 'searchOrganizations', params: validatedParams } ); throw error; } return result.data; } /** * Get similar organizations */ async getSimilarOrganizations(orgId: string, limit?: number): Promise { const query = this.createQuery().whenDefined('limit', limit); const result = await this.get(`${orgId}/similar`, similarOrganizationsResponseSchema, query.toParams(), { context: `getSimilarOrganizations(${orgId})`, }); if (!result.success) { const error = reportError( new Error(result.error?.message || 'Failed to fetch similar organizations'), { operation: 'getSimilarOrganizations', orgId, limit } ); throw error; } return result.data; } /** * Get organizations for the current user */ async getUserOrganizations(): Promise { const result = await this.get('/users/me/organizations', this.entitySchema.array(), undefined, { context: 'getUserOrganizations', useArrayFallback: true, }); if (!result.success) { const error = reportError( new Error(result.error?.message || 'Failed to fetch user organizations'), { operation: 'getUserOrganizations' } ); throw error; } return result.data; } } // Create and export service instance const organizationsService = new OrganizationsService(); // Export service instance for direct usage export { organizationsService }; // Export service methods directly for cleaner imports export const getOrganizations = organizationsService.getAllOrganizations.bind(organizationsService); export const getOrganizationById = (id: string) => organizationsService.getById(id).then(result => { if (!result.success) { const error = reportError( new Error(result.error?.message || 'Failed to fetch organization'), { operation: 'getOrganizationById', id } ); throw error; } return result.data; }); export const createOrganization = (request: CreateOrganizationRequest) => organizationsService.create(request).then(result => { if (!result.success) { const error = reportError( new Error(result.error?.message || 'Failed to create organization'), { operation: 'createOrganization', request } ); throw error; } return result.data; }); export const deleteOrganization = (id: string) => organizationsService.remove(id).then(result => { if (!result.success) { const error = reportError( new Error(result.error?.message || 'Failed to delete organization'), { operation: 'deleteOrganization', id } ); throw error; } }); export const searchOrganizations = organizationsService.searchOrganizations.bind(organizationsService); export const getSimilarOrganizations = organizationsService.getSimilarOrganizations.bind(organizationsService); export const getUserOrganizations = organizationsService.getUserOrganizations.bind(organizationsService);