Muchos cambios
This commit is contained in:
@@ -100,7 +100,7 @@ async function fetchJiraIssues(email, token, period) {
|
||||
|
||||
function buildFieldsList() {
|
||||
return [
|
||||
'summary', 'status', 'assignee', 'resolutiondate', 'created', 'updated',
|
||||
'summary', 'status', 'assignee', 'resolutiondate', 'created', 'updated', 'duedate',
|
||||
config.fields.spEstimate, config.fields.spFinal, config.fields.developer,
|
||||
config.fields.qaAssignee, config.fields.qaEntryRtqa, config.fields.qaTotalTimeRtqa,
|
||||
config.fields.qaLastDurRtqa, config.fields.qaEntryQa, config.fields.qaTotalTimeQa,
|
||||
@@ -115,13 +115,26 @@ function minsToHrs(mins) {
|
||||
return Math.round((mins / 60) * 10) / 10;
|
||||
}
|
||||
|
||||
function processIssues(issues) {
|
||||
function processIssues(issues, period = '90d') {
|
||||
const assigneeData = {};
|
||||
const monthlyDone = {};
|
||||
const issuesList = [];
|
||||
const warnings = [];
|
||||
const now = new Date();
|
||||
|
||||
let cutoffDate = null;
|
||||
if (period !== 'all') {
|
||||
const periodDays = period === '1d' ? 1 : parseInt(period.replace('d', ''));
|
||||
if (!isNaN(periodDays)) {
|
||||
if (period === '1d') {
|
||||
cutoffDate = new Date();
|
||||
cutoffDate.setHours(0,0,0,0);
|
||||
} else {
|
||||
cutoffDate = new Date(Date.now() - (periodDays * 24 * 60 * 60 * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const qaData = {
|
||||
tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0,
|
||||
monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [],
|
||||
@@ -133,7 +146,13 @@ function processIssues(issues) {
|
||||
};
|
||||
|
||||
const crData = {
|
||||
tickets: 0, totalTimeCr: 0, issues: [], monthlyTotalCr: {}, monthlyCountCr: {}, stalledIssues: []
|
||||
tickets: 0, totalTimeCr: 0, issues: [], monthlyTotalCr: {}, monthlyCountCr: {}, stalledIssues: [], times: []
|
||||
};
|
||||
|
||||
const currentStates = {
|
||||
codeReview: { count: 0, sp: 0 },
|
||||
readyToQa: { count: 0, sp: 0 },
|
||||
inQa: { count: 0, sp: 0 }
|
||||
};
|
||||
|
||||
for (const issue of issues) {
|
||||
@@ -154,8 +173,30 @@ function processIssues(issues) {
|
||||
|
||||
const sp = (f[config.fields.spFinal] != null ? f[config.fields.spFinal] : f[config.fields.spEstimate]) ?? 0;
|
||||
const statusKey = f.status?.statusCategory?.key ?? 'new';
|
||||
const statusName = f.status?.name?.toLowerCase() ?? '';
|
||||
const isDone = statusKey === 'done';
|
||||
|
||||
if (isDone && f.resolutiondate && cutoffDate) {
|
||||
const resDate = new Date(f.resolutiondate);
|
||||
if (resDate < cutoffDate) {
|
||||
continue; // Ticket resuelto antes del período (actualizado recientemente por otro motivo)
|
||||
}
|
||||
}
|
||||
|
||||
const isInProgress = statusKey === 'indeterminate';
|
||||
|
||||
if (!isDone) {
|
||||
if (statusName === 'code review' || statusName.includes('code review') || statusName === 'cr') {
|
||||
currentStates.codeReview.count++;
|
||||
currentStates.codeReview.sp += sp;
|
||||
} else if (statusName === 'ready to qa' || statusName.includes('ready to qa') || statusName === 'listo para qa') {
|
||||
currentStates.readyToQa.count++;
|
||||
currentStates.readyToQa.sp += sp;
|
||||
} else if (statusName === 'qa' || statusName.includes('in qa') || statusName === 'en qa') {
|
||||
currentStates.inQa.count++;
|
||||
currentStates.inQa.sp += sp;
|
||||
}
|
||||
}
|
||||
const resMonth = f.resolutiondate ? f.resolutiondate.slice(0, 7) : null;
|
||||
const creMonth = f.created ? f.created.slice(0, 7) : null;
|
||||
|
||||
@@ -233,6 +274,7 @@ function processIssues(issues) {
|
||||
if (timeCrMin > 0) {
|
||||
crData.tickets++;
|
||||
crData.totalTimeCr += timeCrMin;
|
||||
crData.times.push(timeCrMin);
|
||||
if (qaMonth) {
|
||||
crData.monthlyTotalCr[qaMonth] = (crData.monthlyTotalCr[qaMonth] || 0) + timeCrMin;
|
||||
crData.monthlyCountCr[qaMonth] = (crData.monthlyCountCr[qaMonth] || 0) + 1;
|
||||
@@ -252,6 +294,13 @@ function processIssues(issues) {
|
||||
const days = (now - new Date(f.updated)) / (1000 * 60 * 60 * 24);
|
||||
if (days > 14) warnings.push({ type: 'stalled', key: issue.key, summary: f.summary, devs, sp, days: Math.round(days) });
|
||||
}
|
||||
if (!isDone && f.duedate) {
|
||||
const dueDate = new Date(f.duedate);
|
||||
if (dueDate < now) {
|
||||
const daysOverdue = Math.floor((now - dueDate) / (1000 * 60 * 60 * 24));
|
||||
warnings.push({ type: 'expired', key: issue.key, summary: f.summary, devs, sp, days: daysOverdue });
|
||||
}
|
||||
}
|
||||
if (isDone && sp === 0) warnings.push({ type: 'done_no_sp', key: issue.key, summary: f.summary, devs, sp: 0 });
|
||||
if (!isDone && devs.includes('Sin asignar') && sp > 0) warnings.push({ type: 'unassigned_sp', key: issue.key, summary: f.summary, devs, sp });
|
||||
|
||||
@@ -276,9 +325,25 @@ function processIssues(issues) {
|
||||
const monthlyAvgCr = {};
|
||||
for (const m of Object.keys(crData.monthlyTotalCr)) monthlyAvgCr[m] = minsToHrs(crData.monthlyTotalCr[m] / crData.monthlyCountCr[m]);
|
||||
|
||||
const crTimesSorted = crData.times.sort((a, b) => a - b);
|
||||
function getPercentile(sortedArr, p) {
|
||||
if (sortedArr.length === 0) return 0;
|
||||
if (p <= 0) return sortedArr[0];
|
||||
if (p >= 1) return sortedArr[sortedArr.length - 1];
|
||||
const index = (sortedArr.length - 1) * p;
|
||||
const lower = Math.floor(index);
|
||||
const upper = lower + 1;
|
||||
const weight = index % 1;
|
||||
if (upper >= sortedArr.length) return sortedArr[lower];
|
||||
return sortedArr[lower] * (1 - weight) + sortedArr[upper] * weight;
|
||||
}
|
||||
const p50Cr = minsToHrs(getPercentile(crTimesSorted, 0.5));
|
||||
const p90Cr = minsToHrs(getPercentile(crTimesSorted, 0.9));
|
||||
const p99Cr = minsToHrs(getPercentile(crTimesSorted, 0.99));
|
||||
|
||||
return {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
assigneeData, monthlyDone, issuesList, warnings,
|
||||
assigneeData, monthlyDone, issuesList, warnings, currentStates,
|
||||
deliveryData: {
|
||||
avgLeadTime: deliveryData.doneTicketsLead > 0 ? Math.round(deliveryData.totalLeadTimeDays / deliveryData.doneTicketsLead) : 0,
|
||||
avgCycleTime: deliveryData.doneTicketsCycle > 0 ? Math.round(deliveryData.totalCycleTimeDays / deliveryData.doneTicketsCycle) : 0,
|
||||
@@ -287,7 +352,7 @@ function processIssues(issues) {
|
||||
spDeviationPct: deliveryData.doneSpOriginal > 0 ? ((deliveryData.doneSpFinal - deliveryData.doneSpOriginal) / deliveryData.doneSpOriginal * 100).toFixed(1) : 0
|
||||
},
|
||||
crData: {
|
||||
...crData, monthlyAvgCr,
|
||||
...crData, monthlyAvgCr, p50Cr, p90Cr, p99Cr,
|
||||
avgTimeCr: crData.tickets > 0 ? minsToHrs(crData.totalTimeCr / crData.tickets) : 0
|
||||
},
|
||||
qaData: {
|
||||
@@ -316,7 +381,7 @@ exports.handler = async (event) => {
|
||||
if (!config.jira.email || !config.jira.token) throw new Error('Faltan credenciales JIRA en variables de entorno');
|
||||
|
||||
const jiraData = await fetchJiraIssues(config.jira.email, config.jira.token, period);
|
||||
const data = processIssues(jiraData.issues);
|
||||
const data = processIssues(jiraData.issues, period);
|
||||
|
||||
data.isTruncated = jiraData.isTruncated;
|
||||
data.jiraTotal = jiraData.isTruncated ? "1500+" : jiraData.issues.length;
|
||||
|
||||
Reference in New Issue
Block a user