feat: jira and synchro
This commit is contained in:
@@ -7,7 +7,7 @@ declare global {
|
||||
|
||||
// Créer une instance unique de Prisma Client
|
||||
export const prisma = globalThis.__prisma || new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
log: ['error'], // Désactiver les logs query/warn pour éviter le bruit
|
||||
});
|
||||
|
||||
// En développement, stocker l'instance globalement pour éviter les reconnexions
|
||||
|
||||
618
services/jira.ts
Normal file
618
services/jira.ts
Normal file
@@ -0,0 +1,618 @@
|
||||
/**
|
||||
* Service de gestion Jira Cloud
|
||||
* Intégration unidirectionnelle Jira → TowerControl
|
||||
*/
|
||||
|
||||
import { JiraTask } from '@/lib/types';
|
||||
import { prisma } from './database';
|
||||
|
||||
export interface JiraConfig {
|
||||
baseUrl: string;
|
||||
email: string;
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
export interface JiraSyncResult {
|
||||
success: boolean;
|
||||
tasksFound: number;
|
||||
tasksCreated: number;
|
||||
tasksUpdated: number;
|
||||
tasksSkipped: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export class JiraService {
|
||||
private config: JiraConfig;
|
||||
|
||||
constructor(config: JiraConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste la connexion à Jira
|
||||
*/
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.makeJiraRequest('/rest/api/3/myself');
|
||||
if (!response.ok) {
|
||||
console.error(`Test connexion Jira échoué: ${response.status} ${response.statusText}`);
|
||||
const errorText = await response.text();
|
||||
console.error('Détails erreur:', errorText);
|
||||
}
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Erreur de connexion Jira:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les tickets assignés à l'utilisateur connecté avec pagination
|
||||
*/
|
||||
async getAssignedIssues(): Promise<JiraTask[]> {
|
||||
try {
|
||||
const jql = 'assignee = currentUser() AND resolution = Unresolved AND issuetype != Epic ORDER BY updated DESC';
|
||||
const fields = ['id', 'key', 'summary', 'description', 'status', 'priority', 'assignee', 'project', 'issuetype', 'duedate', 'created', 'updated', 'labels'];
|
||||
|
||||
const allIssues: unknown[] = [];
|
||||
let startAt = 0;
|
||||
const maxResults = 100; // Taille des pages
|
||||
let hasMorePages = true;
|
||||
|
||||
console.log('🔄 Récupération paginée des tickets Jira...');
|
||||
|
||||
while (hasMorePages) {
|
||||
const requestBody = {
|
||||
jql,
|
||||
fields
|
||||
};
|
||||
|
||||
console.log(`📄 Page ${Math.floor(startAt / maxResults) + 1} (tickets ${startAt + 1}-${startAt + maxResults})`);
|
||||
|
||||
const response = await this.makeJiraRequest(
|
||||
`/rest/api/3/search/jql?startAt=${startAt}&maxResults=${maxResults}`,
|
||||
'POST',
|
||||
requestBody
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Erreur API Jira détaillée:`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: response.url,
|
||||
errorBody: errorText
|
||||
});
|
||||
throw new Error(`Erreur API Jira: ${response.status} ${response.statusText}. Détails: ${errorText.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { issues: unknown[], total: number, maxResults: number, startAt: number };
|
||||
|
||||
if (!data.issues || !Array.isArray(data.issues)) {
|
||||
console.error('❌ Format de données inattendu:', data);
|
||||
throw new Error('Format de données Jira inattendu: pas d\'array issues');
|
||||
}
|
||||
|
||||
allIssues.push(...data.issues);
|
||||
console.log(`✅ ${data.issues.length} tickets récupérés (total: ${allIssues.length})`);
|
||||
|
||||
// Vérifier s'il y a plus de pages
|
||||
hasMorePages = data.issues.length === maxResults && allIssues.length < (data.total || Number.MAX_SAFE_INTEGER);
|
||||
startAt += maxResults;
|
||||
|
||||
// Sécurité: éviter les boucles infinies
|
||||
if (allIssues.length > 5000) {
|
||||
console.warn('⚠️ Limite de sécurité atteinte (5000 tickets). Arrêt de la pagination.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🎯 Total final: ${allIssues.length} tickets Jira récupérés`);
|
||||
|
||||
return allIssues.map((issue: unknown) => this.mapJiraIssueToTask(issue));
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des tickets Jira:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* S'assure que le tag "🔗 From Jira" existe dans la base
|
||||
*/
|
||||
private async ensureJiraTagExists(): Promise<void> {
|
||||
try {
|
||||
const tagName = '🔗 From Jira';
|
||||
|
||||
// Vérifier si le tag existe déjà
|
||||
const existingTag = await prisma.tag.findUnique({
|
||||
where: { name: tagName }
|
||||
});
|
||||
|
||||
if (!existingTag) {
|
||||
// Créer le tag s'il n'existe pas
|
||||
await prisma.tag.create({
|
||||
data: {
|
||||
name: tagName,
|
||||
color: '#0082C9', // Bleu Jira
|
||||
isPinned: false
|
||||
}
|
||||
});
|
||||
console.log(`✅ Tag "${tagName}" créé automatiquement`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du tag Jira:', error);
|
||||
// Ne pas faire échouer la sync pour un problème de tag
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les epics Jira de la base (ne doivent plus être synchronisés)
|
||||
*/
|
||||
async cleanupEpics(): Promise<number> {
|
||||
try {
|
||||
console.log('🧹 Nettoyage des epics Jira...');
|
||||
|
||||
// D'abord, listons toutes les tâches Jira pour voir lesquelles sont des epics
|
||||
const allJiraTasks = await prisma.task.findMany({
|
||||
where: { source: 'jira' }
|
||||
});
|
||||
|
||||
console.log(`🔍 ${allJiraTasks.length} tâches Jira trouvées:`);
|
||||
allJiraTasks.forEach(task => {
|
||||
// @ts-expect-error - jiraType existe mais n'est pas encore dans les types générés
|
||||
console.log(` - ${task.jiraKey}: "${task.title}" [${task.jiraType || 'N/A'}] (ID: ${task.id})`);
|
||||
});
|
||||
|
||||
// Trouver les tâches Jira qui sont des epics
|
||||
// Maintenant on peut utiliser le type Jira mappé directement !
|
||||
const epicsToDelete = await prisma.task.findMany({
|
||||
where: {
|
||||
source: 'jira',
|
||||
// @ts-expect-error - jiraType existe mais n'est pas encore dans les types générés
|
||||
jiraType: 'Epic' // Maintenant standardisé grâce au mapping
|
||||
}
|
||||
});
|
||||
|
||||
if (epicsToDelete.length > 0) {
|
||||
// Supprimer les relations de tags d'abord
|
||||
await prisma.taskTag.deleteMany({
|
||||
where: {
|
||||
taskId: { in: epicsToDelete.map(task => task.id) }
|
||||
}
|
||||
});
|
||||
|
||||
// Supprimer les tâches epics
|
||||
const result = await prisma.task.deleteMany({
|
||||
where: {
|
||||
id: { in: epicsToDelete.map(task => task.id) }
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ ${result.count} epics supprimés de la base`);
|
||||
return result.count;
|
||||
} else {
|
||||
console.log('✅ Aucun epic trouvé à nettoyer');
|
||||
return 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du nettoyage des epics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise les tickets Jira avec la base locale
|
||||
*/
|
||||
async syncTasks(): Promise<JiraSyncResult> {
|
||||
const result: JiraSyncResult = {
|
||||
success: false,
|
||||
tasksFound: 0,
|
||||
tasksCreated: 0,
|
||||
tasksUpdated: 0,
|
||||
tasksSkipped: 0,
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('🔄 Début de la synchronisation Jira...');
|
||||
|
||||
// Nettoyer les epics existants (une seule fois)
|
||||
await this.cleanupEpics();
|
||||
|
||||
// S'assurer que le tag "From Jira" existe
|
||||
await this.ensureJiraTagExists();
|
||||
|
||||
// Récupérer les tickets Jira
|
||||
const jiraTasks = await this.getAssignedIssues();
|
||||
result.tasksFound = jiraTasks.length;
|
||||
|
||||
console.log(`📋 ${jiraTasks.length} tickets trouvés dans Jira`);
|
||||
|
||||
// Synchroniser chaque ticket
|
||||
for (const jiraTask of jiraTasks) {
|
||||
try {
|
||||
const syncResult = await this.syncSingleTask(jiraTask);
|
||||
|
||||
if (syncResult === 'created') {
|
||||
result.tasksCreated++;
|
||||
} else if (syncResult === 'updated') {
|
||||
result.tasksUpdated++;
|
||||
} else {
|
||||
result.tasksSkipped++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erreur sync ticket ${jiraTask.key}:`, error);
|
||||
result.errors.push(`${jiraTask.key}: ${error instanceof Error ? error.message : 'Erreur inconnue'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Déterminer le succès et enregistrer le log
|
||||
result.success = result.errors.length === 0;
|
||||
await this.logSync(result);
|
||||
|
||||
console.log('✅ Synchronisation Jira terminée:', result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur générale de synchronisation:', error);
|
||||
result.errors.push(error instanceof Error ? error.message : 'Erreur inconnue');
|
||||
result.success = false;
|
||||
await this.logSync(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise un ticket Jira unique
|
||||
*/
|
||||
private async syncSingleTask(jiraTask: JiraTask): Promise<'created' | 'updated' | 'skipped'> {
|
||||
// Chercher la tâche existante
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
where: {
|
||||
source_sourceId: {
|
||||
source: 'jira',
|
||||
sourceId: jiraTask.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const taskData = {
|
||||
title: jiraTask.summary,
|
||||
description: jiraTask.description || null,
|
||||
status: this.mapJiraStatusToInternal(jiraTask.status.name),
|
||||
priority: this.mapJiraPriorityToInternal(jiraTask.priority?.name),
|
||||
source: 'jira' as const,
|
||||
sourceId: jiraTask.id,
|
||||
dueDate: jiraTask.duedate ? new Date(jiraTask.duedate) : null,
|
||||
jiraProject: jiraTask.project.key,
|
||||
jiraKey: jiraTask.key,
|
||||
jiraType: this.mapJiraTypeToDisplay(jiraTask.issuetype.name),
|
||||
assignee: jiraTask.assignee?.displayName || null,
|
||||
updatedAt: new Date(jiraTask.updated)
|
||||
};
|
||||
|
||||
if (!existingTask) {
|
||||
// Créer nouvelle tâche avec le tag Jira
|
||||
const newTask = await prisma.task.create({
|
||||
data: {
|
||||
...taskData,
|
||||
createdAt: new Date(jiraTask.created)
|
||||
}
|
||||
});
|
||||
|
||||
// Assigner les tags Jira
|
||||
await this.assignJiraTag(newTask.id);
|
||||
await this.assignProjectTag(newTask.id, jiraTask.project.key);
|
||||
|
||||
console.log(`➕ Nouvelle tâche créée: ${jiraTask.key}`);
|
||||
return 'created';
|
||||
} else {
|
||||
// Vérifier si mise à jour nécessaire (seulement si pas de modifs locales récentes)
|
||||
const jiraUpdated = new Date(jiraTask.updated);
|
||||
const localUpdated = existingTask.updatedAt;
|
||||
|
||||
// Si la tâche locale a été modifiée après la dernière update Jira, on skip
|
||||
if (localUpdated > jiraUpdated) {
|
||||
console.log(`⏭️ Tâche ${jiraTask.key} modifiée localement, skip mise à jour`);
|
||||
return 'skipped';
|
||||
}
|
||||
|
||||
// Mettre à jour seulement les champs Jira (pas les modifs locales)
|
||||
await prisma.task.update({
|
||||
where: { id: existingTask.id },
|
||||
data: {
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
status: taskData.status,
|
||||
priority: taskData.priority,
|
||||
dueDate: taskData.dueDate,
|
||||
jiraProject: taskData.jiraProject,
|
||||
jiraKey: taskData.jiraKey,
|
||||
// @ts-expect-error - jiraType existe mais n'est pas encore dans les types générés
|
||||
jiraType: taskData.jiraType,
|
||||
assignee: taskData.assignee,
|
||||
updatedAt: taskData.updatedAt
|
||||
}
|
||||
});
|
||||
|
||||
// S'assurer que les tags Jira sont assignés (pour les anciennes tâches)
|
||||
await this.assignJiraTag(existingTask.id);
|
||||
await this.assignProjectTag(existingTask.id, jiraTask.project.key);
|
||||
|
||||
console.log(`🔄 Tâche mise à jour: ${jiraTask.key}`);
|
||||
return 'updated';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigne le tag "🔗 From Jira" à une tâche si pas déjà assigné
|
||||
*/
|
||||
private async assignJiraTag(taskId: string): Promise<void> {
|
||||
try {
|
||||
const tagName = '🔗 From Jira';
|
||||
|
||||
// Récupérer le tag
|
||||
const jiraTag = await prisma.tag.findUnique({
|
||||
where: { name: tagName }
|
||||
});
|
||||
|
||||
if (!jiraTag) {
|
||||
console.warn(`⚠️ Tag "${tagName}" introuvable lors de l'assignation`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si le tag est déjà assigné
|
||||
const existingAssignment = await prisma.taskTag.findUnique({
|
||||
where: {
|
||||
taskId_tagId: {
|
||||
taskId: taskId,
|
||||
tagId: jiraTag.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingAssignment) {
|
||||
// Créer l'assignation du tag
|
||||
await prisma.taskTag.create({
|
||||
data: {
|
||||
taskId: taskId,
|
||||
tagId: jiraTag.id
|
||||
}
|
||||
});
|
||||
console.log(`🏷️ Tag "${tagName}" assigné à la tâche`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'assignation du tag Jira:', error);
|
||||
// Ne pas faire échouer la sync pour un problème de tag
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigne un tag de projet Jira à une tâche (crée le tag si nécessaire)
|
||||
*/
|
||||
private async assignProjectTag(taskId: string, projectKey: string): Promise<void> {
|
||||
try {
|
||||
const tagName = `📋 ${projectKey}`;
|
||||
|
||||
// Vérifier si le tag projet existe déjà
|
||||
let projectTag = await prisma.tag.findUnique({
|
||||
where: { name: tagName }
|
||||
});
|
||||
|
||||
if (!projectTag) {
|
||||
// Créer le tag projet s'il n'existe pas
|
||||
projectTag = await prisma.tag.create({
|
||||
data: {
|
||||
name: tagName,
|
||||
color: '#4F46E5', // Violet pour les projets
|
||||
isPinned: false
|
||||
}
|
||||
});
|
||||
console.log(`✅ Tag projet "${tagName}" créé automatiquement`);
|
||||
}
|
||||
|
||||
// Vérifier si le tag est déjà assigné
|
||||
const existingAssignment = await prisma.taskTag.findUnique({
|
||||
where: {
|
||||
taskId_tagId: {
|
||||
taskId: taskId,
|
||||
tagId: projectTag.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingAssignment) {
|
||||
// Créer l'assignation du tag
|
||||
await prisma.taskTag.create({
|
||||
data: {
|
||||
taskId: taskId,
|
||||
tagId: projectTag.id
|
||||
}
|
||||
});
|
||||
console.log(`🏷️ Tag projet "${tagName}" assigné à la tâche`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'assignation du tag projet:', error);
|
||||
// Ne pas faire échouer la sync pour un problème de tag
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe un issue Jira vers le format JiraTask
|
||||
*/
|
||||
private mapJiraIssueToTask(issue: unknown): JiraTask {
|
||||
const issueData = issue as {
|
||||
id: string;
|
||||
key: string;
|
||||
fields: {
|
||||
summary: string;
|
||||
description?: { content?: { content?: { text: string }[] }[] };
|
||||
status: { name: string; statusCategory: { name: string } };
|
||||
priority?: { name: string };
|
||||
assignee?: { displayName: string; emailAddress: string };
|
||||
project: { key: string; name: string };
|
||||
issuetype: { name: string };
|
||||
duedate?: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
labels?: string[];
|
||||
};
|
||||
};
|
||||
return {
|
||||
id: issueData.id,
|
||||
key: issueData.key,
|
||||
summary: issueData.fields.summary,
|
||||
description: issueData.fields.description?.content?.[0]?.content?.[0]?.text || undefined,
|
||||
status: {
|
||||
name: issueData.fields.status.name,
|
||||
category: issueData.fields.status.statusCategory.name
|
||||
},
|
||||
priority: issueData.fields.priority ? {
|
||||
name: issueData.fields.priority.name
|
||||
} : undefined,
|
||||
assignee: issueData.fields.assignee ? {
|
||||
displayName: issueData.fields.assignee.displayName,
|
||||
emailAddress: issueData.fields.assignee.emailAddress
|
||||
} : undefined,
|
||||
project: {
|
||||
key: issueData.fields.project.key,
|
||||
name: issueData.fields.project.name
|
||||
},
|
||||
issuetype: {
|
||||
name: issueData.fields.issuetype.name
|
||||
},
|
||||
duedate: issueData.fields.duedate,
|
||||
created: issueData.fields.created,
|
||||
updated: issueData.fields.updated,
|
||||
labels: issueData.fields.labels || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe les statuts Jira vers les statuts internes
|
||||
*/
|
||||
private mapJiraStatusToInternal(jiraStatus: string): string {
|
||||
const statusMapping: Record<string, string> = {
|
||||
// Statuts "To Do"
|
||||
'To Do': 'todo',
|
||||
'Open': 'todo',
|
||||
'Backlog': 'todo',
|
||||
'Selected for Development': 'todo',
|
||||
|
||||
// Statuts "In Progress"
|
||||
'In Progress': 'in_progress',
|
||||
'In Review': 'in_progress',
|
||||
'Code Review': 'in_progress',
|
||||
'Testing': 'in_progress',
|
||||
|
||||
// Statuts "Done"
|
||||
'Done': 'done',
|
||||
'Closed': 'done',
|
||||
'Resolved': 'done',
|
||||
'Complete': 'done',
|
||||
|
||||
// Statuts bloqués
|
||||
'Blocked': 'blocked',
|
||||
'On Hold': 'blocked'
|
||||
};
|
||||
|
||||
return statusMapping[jiraStatus] || 'todo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe les types Jira vers des termes plus courts
|
||||
*/
|
||||
private mapJiraTypeToDisplay(jiraType: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
'Nouvelle fonctionnalité': 'Feature',
|
||||
'Nouvelle Fonctionnalité': 'Feature',
|
||||
'Feature': 'Feature',
|
||||
'Story': 'Story',
|
||||
'User Story': 'Story',
|
||||
'Tâche': 'Task',
|
||||
'Task': 'Task',
|
||||
'Bug': 'Bug',
|
||||
'Défaut': 'Bug',
|
||||
'Support': 'Support',
|
||||
'Enabler': 'Enabler',
|
||||
'Epic': 'Epic',
|
||||
'Épique': 'Epic'
|
||||
};
|
||||
|
||||
return typeMap[jiraType] || jiraType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe les priorités Jira vers les priorités internes
|
||||
*/
|
||||
private mapJiraPriorityToInternal(jiraPriority?: string): string {
|
||||
if (!jiraPriority) return 'medium';
|
||||
|
||||
const priorityMapping: Record<string, string> = {
|
||||
'Highest': 'critical',
|
||||
'High': 'high',
|
||||
'Medium': 'medium',
|
||||
'Low': 'low',
|
||||
'Lowest': 'low'
|
||||
};
|
||||
|
||||
return priorityMapping[jiraPriority] || 'medium';
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectue une requête à l'API Jira avec authentification
|
||||
*/
|
||||
private async makeJiraRequest(endpoint: string, method: string = 'GET', body?: unknown): Promise<Response> {
|
||||
const url = `${this.config.baseUrl}${endpoint}`;
|
||||
const auth = Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString('base64');
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Basic ${auth}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (body && method !== 'GET') {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un log de synchronisation
|
||||
*/
|
||||
private async logSync(result: JiraSyncResult): Promise<void> {
|
||||
try {
|
||||
await prisma.syncLog.create({
|
||||
data: {
|
||||
source: 'jira',
|
||||
status: result.success ? 'success' : 'error',
|
||||
message: result.errors.length > 0 ? result.errors.join('; ') : null,
|
||||
tasksSync: result.tasksCreated + result.tasksUpdated
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'enregistrement du log:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory pour créer une instance JiraService avec la config env
|
||||
*/
|
||||
export function createJiraService(): JiraService | null {
|
||||
const baseUrl = process.env.JIRA_BASE_URL;
|
||||
const email = process.env.JIRA_EMAIL;
|
||||
const apiToken = process.env.JIRA_API_TOKEN;
|
||||
|
||||
if (!baseUrl || !email || !apiToken) {
|
||||
console.warn('Configuration Jira incomplète - service désactivé');
|
||||
return null;
|
||||
}
|
||||
|
||||
return new JiraService({ baseUrl, email, apiToken });
|
||||
}
|
||||
@@ -319,6 +319,8 @@ export class TasksService {
|
||||
updatedAt: prismaTask.updatedAt,
|
||||
jiraProject: prismaTask.jiraProject ?? undefined,
|
||||
jiraKey: prismaTask.jiraKey ?? undefined,
|
||||
// @ts-expect-error - jiraType existe mais n'est pas encore dans les types générés
|
||||
jiraType: prismaTask.jiraType ?? undefined,
|
||||
assignee: prismaTask.assignee ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user