feat: add project key support for Jira analytics
- Introduced `projectKey` and `ignoredProjects` fields in Jira configuration to enhance analytics capabilities. - Implemented project validation logic in `JiraConfigClient` and integrated it into the `JiraConfigForm` for user input. - Updated `IntegrationsSettingsPageClient` to display analytics dashboard link based on the configured project key. - Enhanced API routes to handle project key in Jira sync and user preferences. - Marked related tasks as complete in `TODO.md`.
This commit is contained in:
@@ -73,16 +73,15 @@ export class BackupService {
|
||||
await prisma.userPreferences.upsert({
|
||||
where: { id: 'default' },
|
||||
update: {
|
||||
viewPreferences: {
|
||||
viewPreferences: JSON.parse(JSON.stringify({
|
||||
...(await userPreferencesService.getViewPreferences()),
|
||||
// Cast pour contourner la restriction de type temporairement
|
||||
...(({ backupConfig: this.config } as any))
|
||||
}
|
||||
backupConfig: this.config
|
||||
}))
|
||||
},
|
||||
create: {
|
||||
id: 'default',
|
||||
kanbanFilters: {},
|
||||
viewPreferences: { backupConfig: this.config } as any,
|
||||
viewPreferences: JSON.parse(JSON.stringify({ backupConfig: this.config })),
|
||||
columnVisibility: {},
|
||||
jiraConfig: {}
|
||||
}
|
||||
|
||||
461
services/jira-analytics.ts
Normal file
461
services/jira-analytics.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* Service d'analytics Jira pour la surveillance d'équipe
|
||||
* Calcule des métriques avancées sur un projet spécifique
|
||||
*/
|
||||
|
||||
import { JiraService } from './jira';
|
||||
import {
|
||||
JiraAnalytics,
|
||||
JiraTask,
|
||||
AssigneeDistribution,
|
||||
SprintVelocity,
|
||||
CycleTimeByType,
|
||||
StatusDistribution,
|
||||
AssigneeWorkload
|
||||
} from '@/lib/types';
|
||||
|
||||
export interface JiraAnalyticsConfig {
|
||||
baseUrl: string;
|
||||
email: string;
|
||||
apiToken: string;
|
||||
projectKey: string;
|
||||
}
|
||||
|
||||
export class JiraAnalyticsService {
|
||||
private jiraService: JiraService;
|
||||
private projectKey: string;
|
||||
|
||||
constructor(config: JiraAnalyticsConfig) {
|
||||
this.jiraService = new JiraService(config);
|
||||
this.projectKey = config.projectKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les analytics du projet
|
||||
*/
|
||||
async getProjectAnalytics(): Promise<JiraAnalytics> {
|
||||
try {
|
||||
console.log(`📊 Début de l'analyse du projet ${this.projectKey}...`);
|
||||
|
||||
// Récupérer les informations du projet
|
||||
const projectInfo = await this.getProjectInfo();
|
||||
|
||||
// Récupérer tous les tickets du projet (pas seulement assignés)
|
||||
const allIssues = await this.getAllProjectIssues();
|
||||
console.log(`📋 ${allIssues.length} tickets récupérés pour l'analyse`);
|
||||
|
||||
// Calculer les différentes métriques
|
||||
const [
|
||||
teamMetrics,
|
||||
velocityMetrics,
|
||||
cycleTimeMetrics,
|
||||
workInProgress
|
||||
] = await Promise.all([
|
||||
this.calculateTeamMetrics(allIssues),
|
||||
this.calculateVelocityMetrics(allIssues),
|
||||
this.calculateCycleTimeMetrics(allIssues),
|
||||
this.calculateWorkInProgress(allIssues)
|
||||
]);
|
||||
|
||||
return {
|
||||
project: {
|
||||
key: this.projectKey,
|
||||
name: projectInfo.name,
|
||||
totalIssues: allIssues.length
|
||||
},
|
||||
teamMetrics,
|
||||
velocityMetrics,
|
||||
cycleTimeMetrics,
|
||||
workInProgress
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du calcul des analytics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les informations de base du projet
|
||||
*/
|
||||
private async getProjectInfo(): Promise<{ name: string }> {
|
||||
const validation = await this.jiraService.validateProject(this.projectKey);
|
||||
if (!validation.exists) {
|
||||
throw new Error(`Projet ${this.projectKey} introuvable`);
|
||||
}
|
||||
return { name: validation.name || this.projectKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère TOUS les tickets du projet (pas seulement assignés à l'utilisateur)
|
||||
*/
|
||||
private async getAllProjectIssues(): Promise<JiraTask[]> {
|
||||
try {
|
||||
const jql = `project = "${this.projectKey}" ORDER BY created DESC`;
|
||||
|
||||
// Utiliser la nouvelle méthode searchIssues qui gère la pagination correctement
|
||||
const jiraTasks = await this.jiraService.searchIssues(jql);
|
||||
|
||||
// Retourner les tâches mappées (elles sont déjà converties par searchIssues)
|
||||
return jiraTasks;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des tickets du projet:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les métriques d'équipe (répartition par assignee)
|
||||
*/
|
||||
private async calculateTeamMetrics(issues: JiraTask[]): Promise<{
|
||||
totalAssignees: number;
|
||||
activeAssignees: number;
|
||||
issuesDistribution: AssigneeDistribution[];
|
||||
}> {
|
||||
const assigneeStats = new Map<string, {
|
||||
displayName: string;
|
||||
total: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
}>();
|
||||
|
||||
// Analyser chaque ticket
|
||||
issues.forEach(issue => {
|
||||
const assignee = issue.assignee;
|
||||
const status = issue.status?.name || 'Unknown';
|
||||
|
||||
// Utiliser "Unassigned" si pas d'assignee
|
||||
const assigneeKey = assignee?.emailAddress || 'unassigned';
|
||||
const displayName = assignee?.displayName || 'Non assigné';
|
||||
|
||||
if (!assigneeStats.has(assigneeKey)) {
|
||||
assigneeStats.set(assigneeKey, {
|
||||
displayName,
|
||||
total: 0,
|
||||
completed: 0,
|
||||
inProgress: 0
|
||||
});
|
||||
}
|
||||
|
||||
const stats = assigneeStats.get(assigneeKey)!;
|
||||
stats.total++;
|
||||
|
||||
// Catégoriser par statut (logique simplifiée)
|
||||
const statusLower = status.toLowerCase();
|
||||
if (statusLower.includes('done') || statusLower.includes('closed') || statusLower.includes('resolved')) {
|
||||
stats.completed++;
|
||||
} else if (statusLower.includes('progress') || statusLower.includes('review') || statusLower.includes('testing')) {
|
||||
stats.inProgress++;
|
||||
}
|
||||
});
|
||||
|
||||
// Convertir en tableau et calculer les pourcentages
|
||||
const distribution: AssigneeDistribution[] = Array.from(assigneeStats.entries()).map(([assignee, stats]) => ({
|
||||
assignee,
|
||||
displayName: stats.displayName,
|
||||
totalIssues: stats.total,
|
||||
completedIssues: stats.completed,
|
||||
inProgressIssues: stats.inProgress,
|
||||
percentage: Math.round((stats.total / issues.length) * 100)
|
||||
})).sort((a, b) => b.totalIssues - a.totalIssues);
|
||||
|
||||
const activeAssignees = distribution.filter(d => d.inProgressIssues > 0).length;
|
||||
|
||||
return {
|
||||
totalAssignees: assigneeStats.size,
|
||||
activeAssignees,
|
||||
issuesDistribution: distribution
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les métriques de vélocité (basées sur les story points)
|
||||
*/
|
||||
private async calculateVelocityMetrics(issues: JiraTask[]): Promise<{
|
||||
currentSprintPoints: number;
|
||||
averageVelocity: number;
|
||||
sprintHistory: SprintVelocity[];
|
||||
}> {
|
||||
// Pour l'instant, implémentation basique
|
||||
// TODO: Intégrer avec l'API Jira Agile pour les vrais sprints
|
||||
|
||||
|
||||
const completedIssues = issues.filter(issue => {
|
||||
const statusCategory = issue.status?.category?.toLowerCase();
|
||||
const statusName = issue.status?.name?.toLowerCase() || '';
|
||||
|
||||
// Support Jira français ET anglais
|
||||
const isCompleted = statusCategory === 'done' ||
|
||||
statusCategory === 'terminé' ||
|
||||
statusName.includes('done') ||
|
||||
statusName.includes('closed') ||
|
||||
statusName.includes('resolved') ||
|
||||
statusName.includes('complete') ||
|
||||
statusName.includes('fait') ||
|
||||
statusName.includes('clôturé') ||
|
||||
statusName.includes('cloturé') ||
|
||||
statusName.includes('en production') ||
|
||||
statusName.includes('finished') ||
|
||||
statusName.includes('delivered');
|
||||
|
||||
return isCompleted;
|
||||
});
|
||||
|
||||
// Calculer les points (1 point par ticket pour simplifier)
|
||||
const getStoryPoints = () => {
|
||||
return 1; // Simplifié pour l'instant, pas de story points dans JiraTask
|
||||
};
|
||||
|
||||
const currentSprintPoints = completedIssues
|
||||
.reduce((sum) => sum + getStoryPoints(), 0);
|
||||
|
||||
|
||||
// Créer un historique basé sur les données réelles des 4 dernières périodes
|
||||
const sprintHistory = this.generateSprintHistoryFromIssues(issues, completedIssues);
|
||||
const averageVelocity = sprintHistory.length > 0
|
||||
? Math.round(sprintHistory.reduce((sum, sprint) => sum + sprint.completedPoints, 0) / sprintHistory.length)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
currentSprintPoints,
|
||||
averageVelocity,
|
||||
sprintHistory
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un historique de sprints basé sur les dates de création/résolution des tickets
|
||||
*/
|
||||
private generateSprintHistoryFromIssues(allIssues: JiraTask[], completedIssues: JiraTask[]): SprintVelocity[] {
|
||||
const now = new Date();
|
||||
const sprintHistory: SprintVelocity[] = [];
|
||||
|
||||
// Créer 4 périodes de 2 semaines (8 semaines au total)
|
||||
for (let i = 3; i >= 0; i--) {
|
||||
const endDate = new Date(now.getTime() - (i * 14 * 24 * 60 * 60 * 1000));
|
||||
const startDate = new Date(endDate.getTime() - (14 * 24 * 60 * 60 * 1000));
|
||||
|
||||
// Compter les tickets complétés dans cette période
|
||||
const completedInPeriod = completedIssues.filter(issue => {
|
||||
const updatedDate = new Date(issue.updated);
|
||||
return updatedDate >= startDate && updatedDate <= endDate;
|
||||
});
|
||||
|
||||
// Compter les tickets créés dans cette période (approximation du planifié)
|
||||
const createdInPeriod = allIssues.filter(issue => {
|
||||
const createdDate = new Date(issue.created);
|
||||
return createdDate >= startDate && createdDate <= endDate;
|
||||
});
|
||||
|
||||
const completedPoints = completedInPeriod.length;
|
||||
const plannedPoints = Math.max(completedPoints, createdInPeriod.length);
|
||||
const completionRate = plannedPoints > 0 ? Math.round((completedPoints / plannedPoints) * 100) : 0;
|
||||
|
||||
sprintHistory.push({
|
||||
sprintName: i === 0 ? 'Sprint actuel' : `Sprint -${i}`,
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
completedPoints,
|
||||
plannedPoints,
|
||||
completionRate
|
||||
});
|
||||
}
|
||||
|
||||
return sprintHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les métriques de cycle time
|
||||
*/
|
||||
private async calculateCycleTimeMetrics(issues: JiraTask[]): Promise<{
|
||||
averageCycleTime: number;
|
||||
cycleTimeByType: CycleTimeByType[];
|
||||
}> {
|
||||
const completedIssues = issues.filter(issue => {
|
||||
const statusCategory = issue.status?.category?.toLowerCase();
|
||||
const statusName = issue.status?.name?.toLowerCase() || '';
|
||||
|
||||
// Support Jira français ET anglais
|
||||
return statusCategory === 'done' ||
|
||||
statusCategory === 'terminé' ||
|
||||
statusName.includes('done') ||
|
||||
statusName.includes('closed') ||
|
||||
statusName.includes('resolved') ||
|
||||
statusName.includes('complete') ||
|
||||
statusName.includes('fait') ||
|
||||
statusName.includes('clôturé') ||
|
||||
statusName.includes('cloturé') ||
|
||||
statusName.includes('en production') ||
|
||||
statusName.includes('finished') ||
|
||||
statusName.includes('delivered');
|
||||
});
|
||||
|
||||
// Calculer le cycle time (de création à résolution)
|
||||
const cycleTimes = completedIssues
|
||||
.filter(issue => issue.created && issue.updated) // S'assurer qu'on a les dates
|
||||
.map(issue => {
|
||||
const created = new Date(issue.created);
|
||||
const resolved = new Date(issue.updated);
|
||||
const days = Math.max(0.1, (resolved.getTime() - created.getTime()) / (1000 * 60 * 60 * 24)); // Minimum 0.1 jour
|
||||
return Math.round(days * 10) / 10; // Arrondir à 1 décimale
|
||||
})
|
||||
.filter(time => time > 0 && time < 365); // Filtrer les valeurs aberrantes (plus d'un an)
|
||||
|
||||
const averageCycleTime = cycleTimes.length > 0
|
||||
? Math.round(cycleTimes.reduce((sum, time) => sum + time, 0) / cycleTimes.length * 10) / 10
|
||||
: 0;
|
||||
|
||||
|
||||
// Grouper par type d'issue (recalculer avec les données filtrées)
|
||||
const validCompletedIssues = completedIssues.filter(issue => issue.created && issue.updated);
|
||||
const typeStats = new Map<string, number[]>();
|
||||
|
||||
validCompletedIssues.forEach((issue, index) => {
|
||||
if (index < cycleTimes.length) { // Sécurité pour éviter l'index out of bounds
|
||||
const issueType = issue.issuetype?.name || 'Unknown';
|
||||
if (!typeStats.has(issueType)) {
|
||||
typeStats.set(issueType, []);
|
||||
}
|
||||
const cycleTime = cycleTimes[index];
|
||||
if (cycleTime > 0 && cycleTime < 365) { // Même filtre que plus haut
|
||||
typeStats.get(issueType)!.push(cycleTime);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const cycleTimeByType: CycleTimeByType[] = Array.from(typeStats.entries()).map(([type, times]) => {
|
||||
const average = times.reduce((sum, time) => sum + time, 0) / times.length;
|
||||
const sorted = [...times].sort((a, b) => a - b);
|
||||
const median = sorted.length % 2 === 0
|
||||
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
|
||||
: sorted[Math.floor(sorted.length / 2)];
|
||||
|
||||
return {
|
||||
issueType: type,
|
||||
averageDays: Math.round(average * 10) / 10,
|
||||
medianDays: Math.round(median * 10) / 10,
|
||||
samples: times.length
|
||||
};
|
||||
}).sort((a, b) => b.samples - a.samples);
|
||||
|
||||
return {
|
||||
averageCycleTime,
|
||||
cycleTimeByType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le work in progress (WIP)
|
||||
*/
|
||||
private async calculateWorkInProgress(issues: JiraTask[]): Promise<{
|
||||
byStatus: StatusDistribution[];
|
||||
byAssignee: AssigneeWorkload[];
|
||||
}> {
|
||||
// Grouper par statut
|
||||
const statusCounts = new Map<string, number>();
|
||||
issues.forEach(issue => {
|
||||
const status = issue.status?.name || 'Unknown';
|
||||
statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
|
||||
});
|
||||
|
||||
const byStatus: StatusDistribution[] = Array.from(statusCounts.entries()).map(([status, count]) => ({
|
||||
status,
|
||||
count,
|
||||
percentage: Math.round((count / issues.length) * 100)
|
||||
})).sort((a, b) => b.count - a.count);
|
||||
|
||||
// Grouper par assignee (WIP seulement)
|
||||
const wipIssues = issues.filter(issue => {
|
||||
const statusCategory = issue.status?.category?.toLowerCase();
|
||||
const statusName = issue.status?.name?.toLowerCase() || '';
|
||||
|
||||
// Exclure les tickets terminés (support français ET anglais)
|
||||
return statusCategory !== 'done' &&
|
||||
statusCategory !== 'terminé' &&
|
||||
!statusName.includes('done') &&
|
||||
!statusName.includes('closed') &&
|
||||
!statusName.includes('resolved') &&
|
||||
!statusName.includes('complete') &&
|
||||
!statusName.includes('fait') &&
|
||||
!statusName.includes('clôturé') &&
|
||||
!statusName.includes('cloturé') &&
|
||||
!statusName.includes('en production') &&
|
||||
!statusName.includes('finished') &&
|
||||
!statusName.includes('delivered');
|
||||
});
|
||||
|
||||
const assigneeWorkload = new Map<string, {
|
||||
displayName: string;
|
||||
todo: number;
|
||||
inProgress: number;
|
||||
review: number;
|
||||
}>();
|
||||
|
||||
wipIssues.forEach(issue => {
|
||||
const assignee = issue.assignee;
|
||||
const status = issue.status?.name?.toLowerCase() || '';
|
||||
|
||||
const assigneeKey = assignee?.emailAddress || 'unassigned';
|
||||
const displayName = assignee?.displayName || 'Non assigné';
|
||||
|
||||
if (!assigneeWorkload.has(assigneeKey)) {
|
||||
assigneeWorkload.set(assigneeKey, {
|
||||
displayName,
|
||||
todo: 0,
|
||||
inProgress: 0,
|
||||
review: 0
|
||||
});
|
||||
}
|
||||
|
||||
const workload = assigneeWorkload.get(assigneeKey)!;
|
||||
const statusCategory = issue.status?.category?.toLowerCase();
|
||||
|
||||
// Classification robuste français/anglais basée sur les catégories et noms Jira
|
||||
if (statusCategory === 'indeterminate' ||
|
||||
statusCategory === 'en cours' ||
|
||||
status.includes('progress') ||
|
||||
status.includes('en cours') ||
|
||||
status.includes('developing') ||
|
||||
status.includes('implementation')) {
|
||||
workload.inProgress++;
|
||||
} else if (status.includes('review') ||
|
||||
status.includes('testing') ||
|
||||
status.includes('validation') ||
|
||||
status.includes('validating') ||
|
||||
status.includes('ready for')) {
|
||||
workload.review++;
|
||||
} else if (statusCategory === 'new' ||
|
||||
statusCategory === 'a faire' ||
|
||||
status.includes('todo') ||
|
||||
status.includes('to do') ||
|
||||
status.includes('a faire') ||
|
||||
status.includes('backlog') ||
|
||||
status.includes('product backlog') ||
|
||||
status.includes('ready to sprint') ||
|
||||
status.includes('estimating') ||
|
||||
status.includes('refinement') ||
|
||||
status.includes('open') ||
|
||||
status.includes('created')) {
|
||||
workload.todo++;
|
||||
} else {
|
||||
// Fallback: si on ne peut pas classifier, mettre en "À faire"
|
||||
workload.todo++;
|
||||
}
|
||||
});
|
||||
|
||||
const byAssignee: AssigneeWorkload[] = Array.from(assigneeWorkload.entries()).map(([assignee, workload]) => ({
|
||||
assignee,
|
||||
displayName: workload.displayName,
|
||||
todoCount: workload.todo,
|
||||
inProgressCount: workload.inProgress,
|
||||
reviewCount: workload.review,
|
||||
totalActive: workload.todo + workload.inProgress + workload.review
|
||||
})).sort((a, b) => b.totalActive - a.totalActive);
|
||||
|
||||
return {
|
||||
byStatus,
|
||||
byAssignee
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface JiraConfig {
|
||||
baseUrl: string;
|
||||
email: string;
|
||||
apiToken: string;
|
||||
projectKey?: string; // Clé du projet à surveiller pour les analytics d'équipe (ex: "MYTEAM")
|
||||
ignoredProjects?: string[]; // Liste des clés de projets à ignorer (ex: ["DEMO", "TEST"])
|
||||
}
|
||||
|
||||
@@ -39,12 +40,42 @@ export class JiraService {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide l'existence d'un projet Jira
|
||||
*/
|
||||
async validateProject(projectKey: string): Promise<{ exists: boolean; name?: string; error?: string }> {
|
||||
try {
|
||||
const response = await this.makeJiraRequestPrivate(`/rest/api/3/project/${projectKey}`);
|
||||
|
||||
if (response.status === 404) {
|
||||
return { exists: false, error: `Projet "${projectKey}" introuvable` };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return { exists: false, error: `Erreur API: ${response.status} - ${errorText}` };
|
||||
}
|
||||
|
||||
const project = await response.json();
|
||||
return {
|
||||
exists: true,
|
||||
name: project.name
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la validation du projet:', error);
|
||||
return {
|
||||
exists: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur de connexion'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste la connexion à Jira
|
||||
*/
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.makeJiraRequest('/rest/api/3/myself');
|
||||
const response = await this.makeJiraRequestPrivate('/rest/api/3/myself');
|
||||
if (!response.ok) {
|
||||
console.error(`Test connexion Jira échoué: ${response.status} ${response.statusText}`);
|
||||
const errorText = await response.text();
|
||||
@@ -80,11 +111,10 @@ export class JiraService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les tickets assignés à l'utilisateur connecté avec pagination
|
||||
* Récupère les tickets avec une requête JQL personnalisée avec pagination
|
||||
*/
|
||||
async getAssignedIssues(): Promise<JiraTask[]> {
|
||||
async searchIssues(jql: string): 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[] = [];
|
||||
@@ -114,7 +144,7 @@ export class JiraService {
|
||||
|
||||
console.log(`🌐 POST /rest/api/3/search/jql avec ${nextPageToken ? 'nextPageToken' : 'première page'}`);
|
||||
|
||||
const response = await this.makeJiraRequest('/rest/api/3/search/jql', 'POST', requestBody);
|
||||
const response = await this.makeJiraRequestPrivate('/rest/api/3/search/jql', 'POST', requestBody);
|
||||
|
||||
console.log(`📡 Status réponse: ${response.status}`);
|
||||
|
||||
@@ -174,6 +204,14 @@ export class JiraService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les tickets assignés à l'utilisateur connecté
|
||||
*/
|
||||
async getAssignedIssues(): Promise<JiraTask[]> {
|
||||
const jql = 'assignee = currentUser() AND resolution = Unresolved AND issuetype != Epic ORDER BY updated DESC';
|
||||
return this.searchIssues(jql);
|
||||
}
|
||||
|
||||
/**
|
||||
* S'assure que le tag "🔗 From Jira" existe dans la base
|
||||
*/
|
||||
@@ -649,10 +687,17 @@ export class JiraService {
|
||||
return priorityMapping[jiraPriority] || 'medium';
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectue une requête à l'API Jira avec authentification (méthode publique pour analytics)
|
||||
*/
|
||||
async makeJiraRequest(endpoint: string, method: string = 'GET', body?: unknown): Promise<Response> {
|
||||
return this.makeJiraRequestPrivate(endpoint, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectue une requête à l'API Jira avec authentification
|
||||
*/
|
||||
private async makeJiraRequest(endpoint: string, method: string = 'GET', body?: unknown): Promise<Response> {
|
||||
private async makeJiraRequestPrivate(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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user