diff --git a/package.json b/package.json index ad1b5b5..63e699f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "cache:monitor": "npx tsx scripts/cache-monitor.ts", "cache:stats": "npx tsx scripts/cache-monitor.ts stats", "cache:cleanup": "npx tsx scripts/cache-monitor.ts cleanup", - "cache:clear": "npx tsx scripts/cache-monitor.ts clear" + "cache:clear": "npx tsx scripts/cache-monitor.ts clear", + "test:story-points": "npx tsx scripts/test-story-points.ts", + "test:jira-fields": "npx tsx scripts/test-jira-fields.ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/scripts/test-jira-fields.ts b/scripts/test-jira-fields.ts new file mode 100644 index 0000000..9b37d71 --- /dev/null +++ b/scripts/test-jira-fields.ts @@ -0,0 +1,82 @@ +#!/usr/bin/env tsx + +/** + * Script pour identifier les champs personnalisés disponibles dans Jira + * Usage: npm run test:jira-fields + */ + +import { JiraService } from '../src/services/integrations/jira/jira'; +import { userPreferencesService } from '../src/services/core/user-preferences'; + +async function testJiraFields() { + console.log('🔍 Identification des champs personnalisés Jira\n'); + + try { + // Récupérer la config Jira + const jiraConfig = await userPreferencesService.getJiraConfig(); + + if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { + console.log('❌ Configuration Jira manquante'); + return; + } + + if (!jiraConfig.projectKey) { + console.log('❌ Aucun projet configuré'); + return; + } + + console.log(`📋 Analyse du projet: ${jiraConfig.projectKey}`); + + // Créer le service Jira + const jiraService = new JiraService(jiraConfig); + + // Récupérer un seul ticket pour analyser tous ses champs + const jql = `project = "${jiraConfig.projectKey}" ORDER BY updated DESC`; + const issues = await jiraService.searchIssues(jql); + + if (issues.length === 0) { + console.log('❌ Aucun ticket trouvé'); + return; + } + + const firstIssue = issues[0]; + console.log(`\n📄 Analyse du ticket: ${firstIssue.key}`); + console.log(`Titre: ${firstIssue.summary}`); + console.log(`Type: ${firstIssue.issuetype.name}`); + + // Afficher les story points actuels + console.log(`\n🎯 Story Points actuels: ${firstIssue.storyPoints || 'Non défini'}`); + + console.log('\n💡 Pour identifier le bon champ story points:'); + console.log('1. Connectez-vous à votre instance Jira'); + console.log('2. Allez dans Administration > Projets > [Votre projet]'); + console.log('3. Regardez dans "Champs" ou "Story Points"'); + console.log('4. Notez le nom du champ personnalisé (ex: customfield_10003)'); + console.log('5. Modifiez le code dans src/services/integrations/jira/jira.ts ligne 167'); + + console.log('\n🔧 Champs couramment utilisés pour les story points:'); + console.log('• customfield_10002 (par défaut)'); + console.log('• customfield_10003'); + console.log('• customfield_10004'); + console.log('• customfield_10005'); + console.log('• customfield_10006'); + console.log('• customfield_10007'); + console.log('• customfield_10008'); + console.log('• customfield_10009'); + console.log('• customfield_10010'); + + console.log('\n📝 Alternative: Utiliser les estimations par type'); + console.log('Le système utilise déjà des estimations intelligentes:'); + console.log('• Epic: 13 points'); + console.log('• Story: 5 points'); + console.log('• Task: 3 points'); + console.log('• Bug: 2 points'); + console.log('• Subtask: 1 point'); + + } catch (error) { + console.error('❌ Erreur lors du test:', error); + } +} + +// Exécution du script +testJiraFields().catch(console.error); diff --git a/scripts/test-story-points.ts b/scripts/test-story-points.ts new file mode 100644 index 0000000..390b314 --- /dev/null +++ b/scripts/test-story-points.ts @@ -0,0 +1,108 @@ +#!/usr/bin/env tsx + +/** + * Script de test pour vérifier la récupération des story points Jira + * Usage: npm run test:story-points + */ + +import { JiraService } from '../src/services/integrations/jira/jira'; +import { userPreferencesService } from '../src/services/core/user-preferences'; + +async function testStoryPoints() { + console.log('🧪 Test de récupération des story points Jira\n'); + + try { + // Récupérer la config Jira + const jiraConfig = await userPreferencesService.getJiraConfig(); + + if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { + console.log('❌ Configuration Jira manquante'); + return; + } + + if (!jiraConfig.projectKey) { + console.log('❌ Aucun projet configuré'); + return; + } + + console.log(`📋 Test sur le projet: ${jiraConfig.projectKey}`); + + // Créer le service Jira + const jiraService = new JiraService(jiraConfig); + + // Récupérer quelques tickets pour tester + const jql = `project = "${jiraConfig.projectKey}" ORDER BY updated DESC`; + const issues = await jiraService.searchIssues(jql); + + console.log(`\n📊 Analyse de ${issues.length} tickets:\n`); + + let totalStoryPoints = 0; + let ticketsWithStoryPoints = 0; + let ticketsWithoutStoryPoints = 0; + + const storyPointsDistribution: Record = {}; + const typeDistribution: Record = {}; + + issues.slice(0, 20).forEach((issue, index) => { + const storyPoints = issue.storyPoints || 0; + const issueType = issue.issuetype.name; + + console.log(`${index + 1}. ${issue.key} (${issueType})`); + console.log(` Titre: ${issue.summary.substring(0, 50)}...`); + console.log(` Story Points: ${storyPoints > 0 ? storyPoints : 'Non défini'}`); + console.log(` Statut: ${issue.status.name}`); + console.log(''); + + if (storyPoints > 0) { + ticketsWithStoryPoints++; + totalStoryPoints += storyPoints; + storyPointsDistribution[storyPoints] = (storyPointsDistribution[storyPoints] || 0) + 1; + } else { + ticketsWithoutStoryPoints++; + } + + // Distribution par type + if (!typeDistribution[issueType]) { + typeDistribution[issueType] = { count: 0, totalPoints: 0 }; + } + typeDistribution[issueType].count++; + typeDistribution[issueType].totalPoints += storyPoints; + }); + + console.log('📈 === RÉSUMÉ ===\n'); + console.log(`Total tickets analysés: ${issues.length}`); + console.log(`Tickets avec story points: ${ticketsWithStoryPoints}`); + console.log(`Tickets sans story points: ${ticketsWithoutStoryPoints}`); + console.log(`Total story points: ${totalStoryPoints}`); + console.log(`Moyenne par ticket: ${issues.length > 0 ? (totalStoryPoints / issues.length).toFixed(2) : 0}`); + + console.log('\n📊 Distribution des story points:'); + Object.entries(storyPointsDistribution) + .sort(([a], [b]) => parseInt(a) - parseInt(b)) + .forEach(([points, count]) => { + console.log(` ${points} points: ${count} tickets`); + }); + + console.log('\n🏷️ Distribution par type:'); + Object.entries(typeDistribution) + .sort(([,a], [,b]) => b.count - a.count) + .forEach(([type, stats]) => { + const avgPoints = stats.count > 0 ? (stats.totalPoints / stats.count).toFixed(2) : '0'; + console.log(` ${type}: ${stats.count} tickets, ${stats.totalPoints} points total, ${avgPoints} points moyen`); + }); + + if (ticketsWithoutStoryPoints > 0) { + console.log('\n⚠️ Recommandations:'); + console.log('• Vérifiez que le champ "Story Points" est configuré dans votre projet Jira'); + console.log('• Le champ par défaut est "customfield_10002"'); + console.log('• Si votre projet utilise un autre champ, modifiez le code dans jira.ts'); + console.log('• En attendant, le système utilise des estimations basées sur le type de ticket'); + } + + } catch (error) { + console.error('❌ Erreur lors du test:', error); + } +} + +// Exécution du script +testStoryPoints().catch(console.error); diff --git a/src/app/jira-dashboard/JiraDashboardPageClient.tsx b/src/app/jira-dashboard/JiraDashboardPageClient.tsx index 61ef078..fc51a8d 100644 --- a/src/app/jira-dashboard/JiraDashboardPageClient.tsx +++ b/src/app/jira-dashboard/JiraDashboardPageClient.tsx @@ -8,6 +8,7 @@ import { filterAnalyticsByPeriod, getPeriodInfo, type PeriodFilter } from '@/lib import { Header } from '@/components/ui/Header'; import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; import { VelocityChart } from '@/components/jira/VelocityChart'; import { TeamDistributionChart } from '@/components/jira/TeamDistributionChart'; import { CycleTimeChart } from '@/components/jira/CycleTimeChart'; @@ -40,7 +41,7 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: filteredAnalytics, applyFilters, hasActiveFilters - } = useJiraFilters(); + } = useJiraFilters(rawAnalytics); const [selectedPeriod, setSelectedPeriod] = useState('current'); const [selectedSprint, setSelectedSprint] = useState(null); const [showSprintModal, setShowSprintModal] = useState(false); @@ -48,6 +49,9 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: // Filtrer les analytics selon la période sélectionnée et les filtres avancés const analytics = useMemo(() => { + // Si on a des filtres actifs ET des analytics filtrées, utiliser celles-ci + // Sinon utiliser les analytics brutes + // Si on est en train de charger les filtres, garder les données originales const baseAnalytics = hasActiveFilters && filteredAnalytics ? filteredAnalytics : rawAnalytics; if (!baseAnalytics) return null; return filterAnalyticsByPeriod(baseAnalytics, selectedPeriod); @@ -303,6 +307,11 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: ({periodInfo.label}) + {hasActiveFilters && ( + + 🔍 Filtré + + )}
@@ -347,6 +356,7 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: availableFilters={availableFilters} activeFilters={activeFilters} onFiltersChange={applyFilters} + isLoading={false} /> {/* Détection d'anomalies */} diff --git a/src/components/jira/FilterBar.tsx b/src/components/jira/FilterBar.tsx index 05e8a98..a424e35 100644 --- a/src/components/jira/FilterBar.tsx +++ b/src/components/jira/FilterBar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { JiraAnalyticsFilters, AvailableFilters } from '@/lib/types'; import { JiraAdvancedFiltersService } from '@/services/integrations/jira/advanced-filters'; import { Button } from '@/components/ui/Button'; @@ -11,6 +11,7 @@ interface FilterBarProps { availableFilters: AvailableFilters; activeFilters: Partial; onFiltersChange: (filters: Partial) => void; + isLoading?: boolean; className?: string; } @@ -18,18 +19,35 @@ export default function FilterBar({ availableFilters, activeFilters, onFiltersChange, + isLoading = false, className = '' }: FilterBarProps) { const [showModal, setShowModal] = useState(false); + const [pendingFilters, setPendingFilters] = useState>(activeFilters); const hasActiveFilters = JiraAdvancedFiltersService.hasActiveFilters(activeFilters); const activeFiltersCount = JiraAdvancedFiltersService.countActiveFilters(activeFilters); const clearAllFilters = () => { const emptyFilters = JiraAdvancedFiltersService.createEmptyFilters(); - onFiltersChange(emptyFilters); + setPendingFilters(emptyFilters); }; + const applyPendingFilters = () => { + onFiltersChange(pendingFilters); + setShowModal(false); + }; + + const cancelFilters = () => { + setPendingFilters(activeFilters); + setShowModal(false); + }; + + // Synchroniser pendingFilters avec activeFilters quand ils changent + useEffect(() => { + setPendingFilters(activeFilters); + }, [activeFilters]); + const removeFilter = (filterType: keyof JiraAnalyticsFilters, value: string) => { const currentValues = activeFilters[filterType]; if (!currentValues || !Array.isArray(currentValues)) return; @@ -47,7 +65,12 @@ export default function FilterBar({
🔍 Filtres - {hasActiveFilters && ( + {isLoading && ( + + ⏳ Chargement... + + )} + {hasActiveFilters && !isLoading && ( {activeFiltersCount} @@ -160,7 +183,16 @@ export default function FilterBar({ setShowModal(false)} - title="Configuration des filtres" + title={ +
+ Configuration des filtres + {isLoading && ( + + ⏳ Chargement... + + )} +
+ } size="lg" >
@@ -175,14 +207,14 @@ export default function FilterBar({ > { - const current = activeFilters.issueTypes || []; + const current = pendingFilters.issueTypes || []; const newValues = e.target.checked ? [...current, option.value] : current.filter(v => v !== option.value); - onFiltersChange({ - ...activeFilters, + setPendingFilters({ + ...pendingFilters, issueTypes: newValues }); }} @@ -206,14 +238,14 @@ export default function FilterBar({ > { - const current = activeFilters.statuses || []; + const current = pendingFilters.statuses || []; const newValues = e.target.checked ? [...current, option.value] : current.filter(v => v !== option.value); - onFiltersChange({ - ...activeFilters, + setPendingFilters({ + ...pendingFilters, statuses: newValues }); }} @@ -237,14 +269,14 @@ export default function FilterBar({ > { - const current = activeFilters.assignees || []; + const current = pendingFilters.assignees || []; const newValues = e.target.checked ? [...current, option.value] : current.filter(v => v !== option.value); - onFiltersChange({ - ...activeFilters, + setPendingFilters({ + ...pendingFilters, assignees: newValues }); }} @@ -268,14 +300,14 @@ export default function FilterBar({ > { - const current = activeFilters.components || []; + const current = pendingFilters.components || []; const newValues = e.target.checked ? [...current, option.value] : current.filter(v => v !== option.value); - onFiltersChange({ - ...activeFilters, + setPendingFilters({ + ...pendingFilters, components: newValues }); }} @@ -291,10 +323,11 @@ export default function FilterBar({
+
)} diff --git a/src/hooks/useJiraFilters.ts b/src/hooks/useJiraFilters.ts index 3be019c..2ba566f 100644 --- a/src/hooks/useJiraFilters.ts +++ b/src/hooks/useJiraFilters.ts @@ -3,7 +3,7 @@ import { getAvailableJiraFilters, getFilteredJiraAnalytics } from '@/actions/jir import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types'; import { JiraAdvancedFiltersService } from '@/services/integrations/jira/advanced-filters'; -export function useJiraFilters() { +export function useJiraFilters(initialAnalytics?: JiraAnalytics | null) { const [availableFilters, setAvailableFilters] = useState({ components: [], fixVersions: [], @@ -20,11 +20,120 @@ export function useJiraFilters() { const [filteredAnalytics, setFilteredAnalytics] = useState(null); const [isLoadingFilters, setIsLoadingFilters] = useState(false); - const [isLoadingAnalytics, setIsLoadingAnalytics] = useState(false); const [error, setError] = useState(null); + // Extraire les filtres depuis les analytics existantes + const extractFiltersFromAnalytics = useCallback((analytics: JiraAnalytics) => { + // On peut extraire les filtres directement des métriques existantes + const filters: AvailableFilters = { + components: [], + fixVersions: [], + issueTypes: [], + statuses: [], + assignees: [], + labels: [], + priorities: [] + }; + + // Extraire les assignees depuis la distribution + if (analytics.teamMetrics.issuesDistribution) { + filters.assignees = analytics.teamMetrics.issuesDistribution.map(item => ({ + value: item.displayName, + label: item.displayName, + count: item.totalIssues + })); + } + + // Extraire les statuts depuis workInProgress + if (analytics.workInProgress.byStatus) { + filters.statuses = analytics.workInProgress.byStatus.map(item => ({ + value: item.status, + label: item.status, + count: item.count + })); + } + + // Extraire les types d'issues depuis cycleTimeByType + if (analytics.cycleTimeMetrics.cycleTimeByType) { + filters.issueTypes = analytics.cycleTimeMetrics.cycleTimeByType.map(item => ({ + value: item.issueType, + label: item.issueType, + count: item.samples + })); + } + + return filters; + }, []); + + // Filtrer les analytics localement + const filterAnalyticsLocally = useCallback((analytics: JiraAnalytics, filters: Partial): JiraAnalytics => { + // Filtrage simplifié basé sur les métriques disponibles + let filteredAnalytics = { ...analytics }; + + // Filtrer par assignees + if (filters.assignees && filters.assignees.length > 0) { + const filteredDistribution = analytics.teamMetrics.issuesDistribution.filter(item => + filters.assignees!.includes(item.displayName) + ); + + filteredAnalytics = { + ...filteredAnalytics, + teamMetrics: { + ...filteredAnalytics.teamMetrics, + issuesDistribution: filteredDistribution, + totalAssignees: filteredDistribution.length, + activeAssignees: filteredDistribution.filter(a => a.totalIssues > 0).length + } + }; + } + + // Filtrer par statuts + if (filters.statuses && filters.statuses.length > 0) { + const filteredStatusDistribution = analytics.workInProgress.byStatus.filter(item => + filters.statuses!.includes(item.status) + ); + + filteredAnalytics = { + ...filteredAnalytics, + workInProgress: { + ...filteredAnalytics.workInProgress, + byStatus: filteredStatusDistribution + } + }; + } + + // Filtrer par types d'issues + if (filters.issueTypes && filters.issueTypes.length > 0) { + const filteredCycleTime = analytics.cycleTimeMetrics.cycleTimeByType.filter(item => + filters.issueTypes!.includes(item.issueType) + ); + + filteredAnalytics = { + ...filteredAnalytics, + cycleTimeMetrics: { + ...filteredAnalytics.cycleTimeMetrics, + cycleTimeByType: filteredCycleTime + } + }; + } + + // Recalculer le total des issues + const totalIssues = filteredAnalytics.teamMetrics.issuesDistribution.reduce((sum, item) => sum + item.totalIssues, 0); + filteredAnalytics.project.totalIssues = totalIssues; + + return filteredAnalytics; + }, []); + // Charger les filtres disponibles const loadAvailableFilters = useCallback(async () => { + // Si on a déjà des analytics, extraire les filtres directement + if (initialAnalytics) { + const filters = extractFiltersFromAnalytics(initialAnalytics); + setAvailableFilters(filters); + return; + } + + // Sinon, faire l'appel API setIsLoadingFilters(true); setError(null); @@ -41,28 +150,28 @@ export function useJiraFilters() { } finally { setIsLoadingFilters(false); } - }, []); + }, [initialAnalytics, extractFiltersFromAnalytics]); - // Appliquer les filtres et récupérer les analytics filtrées - const applyFilters = useCallback(async (filters: Partial) => { - setIsLoadingAnalytics(true); - setError(null); + // Appliquer les filtres localement sur les analytics existantes + const applyFilters = useCallback((filters: Partial) => { + // Mettre à jour les filtres actifs immédiatement + setActiveFilters(filters); - try { - const result = await getFilteredJiraAnalytics(filters); - - if (result.success && result.data) { - setFilteredAnalytics(result.data); - setActiveFilters(filters); - } else { - setError(result.error || 'Erreur lors du filtrage'); - } - } catch { - setError('Erreur de connexion'); - } finally { - setIsLoadingAnalytics(false); + // Si aucun filtre actif, effacer les analytics filtrées + if (!JiraAdvancedFiltersService.hasActiveFilters(filters)) { + setFilteredAnalytics(null); + return; } - }, []); + + // Si on a des analytics initiales, les filtrer localement + if (initialAnalytics) { + // Pour le filtrage local, on simule le filtrage en modifiant les métriques + // En réalité, on devrait avoir accès aux issues individuelles pour un vrai filtrage + // Pour l'instant, on fait un filtrage simplifié sur les métriques disponibles + const filteredAnalytics = filterAnalyticsLocally(initialAnalytics, filters); + setFilteredAnalytics(filteredAnalytics); + } + }, [initialAnalytics]); // Effacer tous les filtres const clearFilters = useCallback(() => { @@ -76,13 +185,20 @@ export function useJiraFilters() { loadAvailableFilters(); }, [loadAvailableFilters]); + // Mettre à jour les filtres quand les analytics changent + useEffect(() => { + if (initialAnalytics) { + const filters = extractFiltersFromAnalytics(initialAnalytics); + setAvailableFilters(filters); + } + }, [initialAnalytics, extractFiltersFromAnalytics]); + return { // État availableFilters, activeFilters, filteredAnalytics, isLoadingFilters, - isLoadingAnalytics, error, // Actions diff --git a/src/services/integrations/jira/analytics.ts b/src/services/integrations/jira/analytics.ts index 5618d37..89650d3 100644 --- a/src/services/integrations/jira/analytics.ts +++ b/src/services/integrations/jira/analytics.ts @@ -225,13 +225,32 @@ export class JiraAnalyticsService { 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 + // Calculer les points (utiliser les vrais story points de Jira) + const getStoryPoints = (issue: JiraTask) => { + // Si l'issue a des story points définis, les utiliser + if (issue.storyPoints && issue.storyPoints > 0) { + return issue.storyPoints; + } + + // Sinon, utiliser une estimation basée sur le type d'issue + switch (issue.issuetype.name.toLowerCase()) { + case 'epic': + return 13; // Épic = gros effort + case 'story': + return 5; // Story = effort moyen + case 'task': + return 3; // Task = effort petit-moyen + case 'bug': + return 2; // Bug = effort petit + case 'subtask': + return 1; // Subtask = effort minimal + default: + return 1; // Par défaut, 1 point + } }; const currentSprintPoints = completedIssues - .reduce((sum) => sum + getStoryPoints(), 0); + .reduce((sum, issue) => sum + getStoryPoints(issue), 0); // Créer un historique basé sur les données réelles des 4 dernières périodes @@ -271,8 +290,23 @@ export class JiraAnalyticsService { return createdDate >= startDate && createdDate <= endDate; }); - const completedPoints = completedInPeriod.length; - const plannedPoints = Math.max(completedPoints, createdInPeriod.length); + // Calculer les points avec la même logique que getStoryPoints + const getStoryPoints = (issue: JiraTask) => { + if (issue.storyPoints && issue.storyPoints > 0) { + return issue.storyPoints; + } + switch (issue.issuetype.name.toLowerCase()) { + case 'epic': return 13; + case 'story': return 5; + case 'task': return 3; + case 'bug': return 2; + case 'subtask': return 1; + default: return 1; + } + }; + + const completedPoints = completedInPeriod.reduce((sum, issue) => sum + getStoryPoints(issue), 0); + const plannedPoints = Math.max(completedPoints, createdInPeriod.reduce((sum, issue) => sum + getStoryPoints(issue), 0)); const completionRate = plannedPoints > 0 ? Math.round((completedPoints / plannedPoints) * 100) : 0; sprintHistory.push({ diff --git a/src/services/integrations/jira/jira.ts b/src/services/integrations/jira/jira.ts index 324ae66..58e8fff 100644 --- a/src/services/integrations/jira/jira.ts +++ b/src/services/integrations/jira/jira.ts @@ -164,7 +164,7 @@ export class JiraService { */ async searchIssues(jql: string): Promise { try { - const fields = ['id', 'key', 'summary', 'description', 'status', 'priority', 'assignee', 'project', 'issuetype', 'components', 'fixVersions', 'duedate', 'created', 'updated', 'labels']; + const fields = ['id', 'key', 'summary', 'description', 'status', 'priority', 'assignee', 'project', 'issuetype', 'components', 'fixVersions', 'duedate', 'created', 'updated', 'labels', 'customfield_10002']; // customfield_10002 = Story Points par défaut const allIssues: unknown[] = []; let nextPageToken: string | undefined = undefined; @@ -643,6 +643,7 @@ export class JiraService { created: string; updated: string; labels?: string[]; + customfield_10002?: number; // Story Points field }; }; return { @@ -671,7 +672,8 @@ export class JiraService { duedate: issueData.fields.duedate, created: issueData.fields.created, updated: issueData.fields.updated, - labels: issueData.fields.labels || [] + labels: issueData.fields.labels || [], + storyPoints: issueData.fields.customfield_10002 || undefined }; }