turash/bugulma/frontend/services/organizations-api.ts

272 lines
8.8 KiB
TypeScript

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<typeof searchOrganizationsParamsSchema>;
/**
* Similar organization schema
* Backend returns full Organization objects, so we use the backend organization schema
*/
export const similarOrganizationSchema = backendOrganizationSchema;
export type SimilarOrganization = BackendOrganization;
/**
* 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<typeof searchOrganizationsResponseSchema>;
/**
* Similar organizations response schema
* Backend returns an array directly, so we accept either array or object format
*/
export const similarOrganizationsResponseSchema = z.union([
z.array(similarOrganizationSchema), // Backend returns array directly
z.object({
organizations: z.array(similarOrganizationSchema),
}),
]);
export type SimilarOrganizationsResponse = z.infer<typeof similarOrganizationsResponseSchema>;
/**
* Organizations Service
* Handles all organization-related API operations with improved reliability and type safety
*/
class OrganizationsService extends CrudService<BackendOrganization, CreateOrganizationRequest> {
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<BackendOrganization[]> {
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<BackendOrganization[]> {
if (!query.trim()) {
return [];
}
// Use searchOrganizationsResponseSchema since the API returns {organizations, count, total}
const result = await this.get<SearchOrganizationsResponse>(
'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<string[]> {
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<SearchOrganizationsResponse> {
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<{ organizations: SimilarOrganization[] }> {
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;
}
// Normalize response: backend returns array, but we want consistent object format
const data = result.data;
if (Array.isArray(data)) {
return { organizations: data };
}
return data as { organizations: SimilarOrganization[] };
}
/**
* Get organizations for the current user
*/
async getUserOrganizations(): Promise<BackendOrganization[]> {
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);