import axios from 'axios'; export class LeantimeService { private apiUrl: string; private apiToken: string; constructor() { this.apiUrl = process.env.LEANTIME_API_URL || ''; this.apiToken = process.env.LEANTIME_TOKEN || ''; console.log('LeantimeService initialized with URL:', this.apiUrl); } /** * Get the properly formatted API endpoint * @returns The formatted API endpoint */ private getApiEndpoint(): string { return this.apiUrl.endsWith('/api/jsonrpc') ? this.apiUrl : this.apiUrl.endsWith('/') ? `${this.apiUrl}api/jsonrpc` : `${this.apiUrl}/api/jsonrpc`; } /** * Create a new project in Leantime * @param mission The mission data * @returns Project ID or throws error */ async createProject(mission: any): Promise { try { // Determine client ID based on mission type const clientId = mission.niveau.toLowerCase() === 'a' ? await this.getClientIdByName('Enkun') : await this.getClientIdByName('ONG'); if (!clientId) { throw new Error(`Leantime client not found for mission type ${mission.niveau}`); } // Create the project const response = await axios.post( this.getApiEndpoint(), { method: 'leantime.rpc.Projects.Projects.addProject', jsonrpc: '2.0', id: 1, params: { values: { name: mission.name, details: mission.intention || '', clientId: clientId, type: 'project', psettings: 'restricted', } } }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken } } ); if (!response.data || !response.data.result) { throw new Error(`Failed to create Leantime project: ${JSON.stringify(response.data)}`); } const projectId = response.data.result; console.log(`Created Leantime project with ID: ${projectId}`); // If the mission has a logo, set it as project avatar if (mission.logo) { await this.setProjectAvatar(projectId, mission.logo); } // Assign users to the project if (mission.missionUsers && mission.missionUsers.length > 0) { await this.assignUsersToProject(projectId, mission.missionUsers); } return projectId; } catch (error) { console.error('Error creating Leantime project:', error); throw new Error(`Leantime integration failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Set project avatar using mission logo * @param projectId The Leantime project ID * @param logoPath The mission logo URL */ async setProjectAvatar(projectId: number, logoPath: string): Promise { try { // Get the logo file from the storage const logoResponse = await fetch(`/api/missions/image/${logoPath}`); if (!logoResponse.ok) { throw new Error(`Failed to fetch logo file: ${logoResponse.statusText}`); } const formData = new FormData(); const logoBlob = await logoResponse.blob(); // Add the file to form data formData.append('file', logoBlob, 'logo.png'); formData.append('project', JSON.stringify({ id: projectId })); // Get base URL by removing any API path const baseUrl = this.apiUrl .replace('/api/jsonrpc', '') .replace('/api/jsonrpc.php', ''); // Construct proper avatar endpoint const avatarEndpoint = baseUrl.endsWith('/') ? `${baseUrl}api/v1/projects.setProjectAvatar` : `${baseUrl}/api/v1/projects.setProjectAvatar`; console.log('Using avatar endpoint:', avatarEndpoint); // Upload the avatar const response = await axios.post( avatarEndpoint, formData, { headers: { 'Content-Type': 'multipart/form-data', 'X-API-Key': this.apiToken } } ); if (!response.data || !response.data.success) { throw new Error(`Failed to set project avatar: ${JSON.stringify(response.data)}`); } console.log(`Set avatar for Leantime project ${projectId}`); } catch (error) { console.error('Error setting project avatar:', error); // Don't fail the entire process if avatar upload fails } } /** * Assign mission users to the Leantime project * @param projectId The Leantime project ID * @param missionUsers The mission users with roles */ async assignUsersToProject(projectId: number, missionUsers: any[]): Promise { try { for (const missionUser of missionUsers) { // Get or create the user in Leantime const leantimeUserId = await this.getUserByEmail(missionUser.user.email); if (!leantimeUserId) { console.warn(`User not found in Leantime: ${missionUser.user.email}`); continue; } // Determine role (Gardien du Temps gets editor, others get commenter) const role = missionUser.role === 'gardien-temps' ? 'editor' : 'commenter'; // Assign the user to the project await this.assignUserToProject(projectId, leantimeUserId, role); } } catch (error) { console.error('Error assigning users to project:', error); // Continue even if some user assignments fail } } /** * Assign a user to a project with the specified role * @param projectId The Leantime project ID * @param userId The Leantime user ID * @param role The role to assign */ async assignUserToProject(projectId: number, userId: string, role: string): Promise { try { const response = await axios.post( this.getApiEndpoint(), { method: 'leantime.rpc.Projects.Projects.assignUserToProject', jsonrpc: '2.0', id: 1, params: { projectId, userId, role } }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken } } ); if (!response.data || !response.data.result) { throw new Error(`Failed to assign user to project: ${JSON.stringify(response.data)}`); } console.log(`Assigned user ${userId} to project ${projectId} with role ${role}`); } catch (error) { console.error(`Error assigning user ${userId} to project ${projectId}:`, error); // Don't fail if individual user assignment fails } } /** * Get a client ID by name * @param clientName The client name to search for * @returns The client ID or null if not found */ async getClientIdByName(clientName: string): Promise { try { // Log the API URL to debug console.log('Leantime API URL:', this.apiUrl); const apiEndpoint = this.getApiEndpoint(); console.log('Using endpoint:', apiEndpoint); const response = await axios.post( apiEndpoint, { method: 'leantime.rpc.Clients.Clients.getAll', jsonrpc: '2.0', id: 1 }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken } } ); if (!response.data || !response.data.result) { throw new Error(`Failed to get clients: ${JSON.stringify(response.data)}`); } const clients = response.data.result; const client = clients.find((c: any) => c.name.toLowerCase() === clientName.toLowerCase() ); if (client) { return parseInt(client.id); } else { console.log(`Client "${clientName}" not found. Creating it...`); return await this.createClient(clientName); } } catch (error) { console.error('Error getting client by name:', error); // Try to create the client if we couldn't find it try { console.log(`Attempting to create client "${clientName}" after error...`); return await this.createClient(clientName); } catch (createError) { console.error('Error creating client:', createError); return null; } } } /** * Create a new client in Leantime * @param clientName The name of the client to create * @returns The ID of the created client or null if failed */ async createClient(clientName: string): Promise { try { const response = await axios.post( this.getApiEndpoint(), { method: 'leantime.rpc.Clients.Clients.addClient', jsonrpc: '2.0', id: 1, params: { values: { name: clientName, street: '', zip: '', city: '', state: '', country: '', phone: '', internet: '', email: '' } } }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken } } ); if (!response.data || !response.data.result) { throw new Error(`Failed to create client: ${JSON.stringify(response.data)}`); } const clientId = parseInt(response.data.result); console.log(`Created client "${clientName}" with ID: ${clientId}`); return clientId; } catch (error) { console.error(`Error creating client "${clientName}":`, error); return null; } } /** * Get a user ID by email * @param email The user email to search for * @returns The user ID or null if not found */ async getUserByEmail(email: string): Promise { try { const response = await axios.post( this.getApiEndpoint(), { method: 'leantime.rpc.Users.Users.getAll', jsonrpc: '2.0', id: 1, }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken } } ); if (!response.data || !response.data.result) { throw new Error(`Failed to get users: ${JSON.stringify(response.data)}`); } const users = response.data.result; const user = users.find((u: any) => u.email === email); return user ? user.id : null; } catch (error) { console.error('Error getting user by email:', error); return null; } } /** * Delete a project from Leantime * @param projectId The Leantime project ID to delete * @returns True if successful, false otherwise */ async deleteProject(projectId: number): Promise { try { const response = await axios.post( this.getApiEndpoint(), { method: 'leantime.rpc.Projects.Projects.deleteProject', jsonrpc: '2.0', id: 1, params: { id: projectId } }, { headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiToken } } ); return response.data && response.data.result === true; } catch (error) { console.error(`Error deleting Leantime project ${projectId}:`, error); return false; } } }