From d00f4c0c738fef2cd84e33f4b0126e71ebd8906a Mon Sep 17 00:00:00 2001 From: rlmcneary2 Date: Tue, 18 Jun 2024 18:51:42 -0400 Subject: [PATCH] #69 Create percent-complete function. --- public/percent-complete/percent-complete.js | 65 +++++++++++++ public/shared/issue-data/issue-data.js | 102 ++++++++++++++++++++ public/timeline-report.js | 32 +++++- 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 public/percent-complete/percent-complete.js create mode 100644 public/shared/issue-data/issue-data.js diff --git a/public/percent-complete/percent-complete.js b/public/percent-complete/percent-complete.js new file mode 100644 index 00000000..02db1a1f --- /dev/null +++ b/public/percent-complete/percent-complete.js @@ -0,0 +1,65 @@ +import { JiraIssue } from "../shared/issue-data/issue-data.js"; + +/** + * @param { JiraIssue[] } issues + * @param { PercentCompleteOptions } options + */ +export function percentComplete(issues, options) { + console.log(`percentComplete: issues=`, issues); + + const map = buildMap(issues, options); + + console.log("percentComplete: map=", map); +} + +/** + * @param {JiraIssue[]} issues + * @param {PercentCompleteOptions} options + * @returns {Record>} + */ +function buildMap(issues, options) { + return issues.reduce((acc, issue) => { + const key = options.getIssueKey(issue); + const parentKey = options.getParentKey(issue); + + acc[key] = { + children: acc[key]?.children ?? [], + hierarchyLevel: issue.fields["Issue Type"].hierarchyLevel, + parentKey, + type: options.getType(issue), + }; + + if (parentKey) { + if (acc[parentKey]) { + acc[parentKey].children.push(key); + } else { + acc[parentKey] = { children: [key] }; + } + } + + return acc; + }, {}); +} + +/** + * @typedef CompletionIssue + * @property {CompletionIssue[]} children + * @property {number} hierarchyLevel + * @property {string} type + */ + +/** + * @typedef PercentCompleteOptions + * @property {(issue: JiraIssue) => number} getConfidence + * @property {(teamKey: string) => number} getDaysPerSprint + * @property {(issue: JiraIssue) => string} getDueDate + * @property {(issue: JiraIssue) => number} getEstimate + * @property {(issue: JiraIssue) => string} getIssueKey + * @property {(issue: JiraIssue) => string | undefined} getParentKey + * @property {(issue: JiraIssue) => string} getStartDate + * @property {(issue: JiraIssue) => string} getTeamKey + * @property {(issue: JiraIssue) => string} getType + * @property {(teamKey: string) => number} getVelocity + * @property {string[]} includeTypes + * @property {number} uncertaintyWeight + */ diff --git a/public/shared/issue-data/issue-data.js b/public/shared/issue-data/issue-data.js new file mode 100644 index 00000000..ea1ae92a --- /dev/null +++ b/public/shared/issue-data/issue-data.js @@ -0,0 +1,102 @@ +/** + * @param {JiraIssue} issue + * @returns {number} + */ +export function getConfidence({ fields }) { + return fields.Confidence; +} + +/** + * @param {string} teamKey + * @returns {number} + */ +export function getDaysPerSprint(teamKey) { + return 10; +} + +/** + * @param {JiraIssue} issue + * @returns {string | null} + */ +export function getDueDate({ fields }) { + return fields["Due date"]; +} + +/** + * @param {JiraIssue} issue + * @returns {number} + */ +export function getEstimate({ fields }) { + return fields["Story points median"]; +} + +/** + * @param {JiraIssue} issue + * @returns {string} + */ +export function getIssueKey({ key }) { + return key; +} + +/** + * @param {JiraIssue} issue + * @returns {string | void} + */ +export function getParentKey({ fields }) { + return fields["Parent Link"].data?.key; +} + +/** + * @param {JiraIssue} issue + * @returns {string | null} + */ +export function getStartDate({ fields }) { + return fields["Start date"]; +} + +/** + * @param {JiraIssue} issue + * @returns {string} + */ +export function getTeamKeyDefault({ fields }) { + var matches = fields.Summary.match(/M\d: ([^:]+): /); + if (matches) { + return fields["Project key"] + ":" + matches[1]; + } else { + return fields["Project key"]; + } +} + +/** + * @param {JiraIssue} issue + * @returns {string} + */ +export function getType({ fields }) { + return fields["Issue Type"].name; +} + +/** + * @param {string} teamKey + * @returns {number} + */ +export function getVelocity(teamKey) { + return 21; +} + +/** + * @typedef {{ + * fields: { + * Confidence: number, + * 'Due date': string | null, + * 'Issue Type': { hierarchyLevel: number, name: string }, + * 'Parent Link': { data: { key: string } }, + * 'Project Key': string, + * 'Start date': string | null, + * Status: { name: string } + * 'Story points median'?: number, + * Summary: string + * }, + * id: string, + * key: string + * }} JiraIssue + */ diff --git a/public/timeline-report.js b/public/timeline-report.js index a1a2b664..82d19421 100644 --- a/public/timeline-report.js +++ b/public/timeline-report.js @@ -1,5 +1,19 @@ import { StacheElement, type, ObservableObject, ObservableArray } from "./can.js"; import { calculationKeysToNames } from "./prepare-issues/date-data.js"; +import { percentComplete } from "./percent-complete/percent-complete.js" +import { + getConfidence, + getDaysPerSprint, + getDueDate, + getEstimate, + getIssueKey, + getParentKey, + getStartDate, + getTeamKeyDefault, + getType, + getVelocity + } from "./shared/issue-data/issue-data.js" + @@ -253,6 +267,8 @@ const configurationView = ` `; +const UNCERTAINTY_WEIGHT_DEFAULT = 80; + export class TimelineReport extends StacheElement { static view = ` @@ -779,7 +795,21 @@ export class TimelineReport extends StacheElement { issuesPromise, serverInfoPromise ]).then(([issues, serverInfo]) => { const formatted = rawIssuesToBaseIssueFormat(issues, serverInfo); - console.log(formatted); + percentComplete(issues, { + getType, + getTeamKey: getTeamKeyDefault, + getDaysPerSprint, + getIssueKey, + getParentKey, + getVelocity, + getEstimate, + getConfidence, + getStartDate, + getDueDate, + //getParallelWorkLimit: (TEAM_KEY) => 1 + includeTypes: ["Epic"], + uncertaintyWeight: UNCERTAINTY_WEIGHT_DEFAULT, + }); return formatted; }) }