} />
);
diff --git a/src/components/dateRangeSelector.jsx b/src/components/dateRangeSelector.jsx
new file mode 100644
index 0000000..60432b1
--- /dev/null
+++ b/src/components/dateRangeSelector.jsx
@@ -0,0 +1,22 @@
+const DateRangeSelector = ({ showHistorical, dateRange, setDateRange }) => {
+ if (!showHistorical) return null;
+
+ return (
+
+ setDateRange(e.target.value)}
+ value={dateRange}
+ style={{ marginLeft: '1rem' }}
+ >
+ Last 7 days
+ Last 15 days
+ Last 30 days
+ Last 3 months
+ Lifetime
+
+
+ );
+};
+
+export default DateRangeSelector;
\ No newline at end of file
diff --git a/src/components/historicalToggle.jsx b/src/components/historicalToggle.jsx
new file mode 100644
index 0000000..7eabd43
--- /dev/null
+++ b/src/components/historicalToggle.jsx
@@ -0,0 +1,22 @@
+const HistoricalToggle = ({ showHistorical, setShowHistorical }) => {
+ return (
+
+
+ setShowHistorical(false)}
+ className={!showHistorical ? 'selected' : ''}
+ >
+ Current
+
+ setShowHistorical(true)}
+ className={showHistorical ? 'selected' : ''}
+ >
+ Historical
+
+
+
+ );
+};
+
+export default HistoricalToggle;
\ No newline at end of file
diff --git a/src/components/radarPieToggle.jsx b/src/components/radarPieToggle.jsx
index e328759..8ce8e49 100644
--- a/src/components/radarPieToggle.jsx
+++ b/src/components/radarPieToggle.jsx
@@ -1,11 +1,12 @@
import React, { useState } from 'react';
+import usePersistentState from './usePersistentState'
import RadarChart from './radarChart';
import PieChart from './pieChart';
import '../styles/radarPieToggle.css';
-const RadarPieToggle = ({ radarData, pieData, title }) => {
- const [selectedChart, setSelectedChart] = useState('radar');
+const RadarPieToggle = ({ RadarPieID, radarData, pieData, title }) => {
+ const [selectedChart, setSelectedChart] = usePersistentState(RadarPieID,'radar');
const selectRadar = () => setSelectedChart('radar');
const selectPie = () => setSelectedChart('pie');
diff --git a/src/domain/commits.jsx b/src/domain/commits.jsx
index 49bb90e..439b922 100644
--- a/src/domain/commits.jsx
+++ b/src/domain/commits.jsx
@@ -1,28 +1,14 @@
-export const filterHistoricData = (data, days) => {
- if (days === "lifetime") return data;
-
- const today = new Date();
- const cutoff = new Date(today);
- cutoff.setDate(today.getDate() - parseInt(days));
- const cutoffDateString = cutoff.toISOString().split("T")[0];
-
- const filtered = {};
- for (const date in data) {
- if (date >= cutoffDateString) {
- filtered[date] = data[date];
- }
- }
-
- return filtered;
-};
+export const getGaugeDataAnonymous = (data) => {
+ return data.commits?.total > 0 ? ((data.commits?.total || 0) - (data.commits?.anonymous || 0)) / data.commits?.total : 0
+}
export const transformCommitsDataForLineChart = (data) => {
- const xDataCommits = [];
+ const xData = [];
const userSeries = {};
for (const date in data) {
- xDataCommits.push(date);
- const commits = data[date].commits;
+ xData.push(date);
+ const commits = data[date].commits || {};
for (const user in commits) {
if (user === 'total' || user === 'anonymous') continue;
@@ -31,68 +17,136 @@ export const transformCommitsDataForLineChart = (data) => {
}
}
- const seriesDataCommits = Object.keys(userSeries).map(user => ({
+ const seriesData = Object.keys(userSeries).map(user => ({
name: user,
data: userSeries[user]
}));
- return { xDataCommits, seriesDataCommits };
+ return { xData, seriesData };
};
export const transformModifiedLinesDataForLineChart = (data) => {
- const xDataModified = [];
+ const xData = [];
const userSeries = {};
for (const date in data) {
- xDataModified.push(date);
- const modifiedLines = data[date].modified_lines;
+ xData.push(date);
+ const modifiedLines = data[date].modified_lines || {};
for (const user in modifiedLines) {
if (user === 'total') continue;
if (!userSeries[user]) userSeries[user] = [];
- userSeries[user].push(modifiedLines[user].modified);
+ userSeries[user].push(modifiedLines[user]?.modified || 0);
}
}
- const seriesModified = Object.keys(userSeries).map(user => ({
+ const seriesData = Object.keys(userSeries).map(user => ({
name: user,
data: userSeries[user]
}));
- return { xDataModified, seriesModified };
+ return { xData, seriesData };
+};
+
+export const GetRadarDataCommits = (data) => {
+ const radarData = {};
+ for (const [user, val] of Object.entries(data.commits || {})) {
+ if (user !== 'total' && user !== 'anonymous') {
+ radarData[user] = val;
+ }
+ }
+ return radarData;
};
-export const prepareRadarData = (modifiedLinesData) => {
- const radarChartModifiedLines = {};
- for (const [user, { modified }] of Object.entries(modifiedLinesData)) {
- radarChartModifiedLines[user] = modified;
+export const GetRadarDataModifiedLines = (data) => {
+ const radarData = {};
+ for (const [user, val] of Object.entries(data.modified_lines || {})) {
+ if (user !== 'total' && val.modified !== undefined) {
+ radarData[user] = val.modified;
+ }
}
- return radarChartModifiedLines;
+ return radarData;
};
-export const getPieChartData = (dataObj, key = null) => {
- return Object.entries(dataObj)
+export const getPieChartDataCommits = (data) => {
+ return Object.entries(data.commits || {})
.filter(([user]) => user !== 'total' && user !== 'anonymous')
- .map(([user, val]) => [user, key ? val[key] : val]);
+ .map(([user, val]) => [user, val]);
};
-export const getGaugeChartPercentages = (dataObj, totalKey, anonymousKey = null) => {
- const total = dataObj[totalKey];
- const anonymous = anonymousKey ? dataObj[anonymousKey] : 0;
- return Object.entries(dataObj)
- .filter(([user]) => user !== totalKey && user !== anonymousKey)
+export const getPieChartDataModifiedLines = (data) => {
+ return Object.entries(data.modified_lines || {})
+ .filter(([user]) => user !== 'total')
+ .map(([user, val]) => [user, val.modified]);
+};
+
+export const getGaugeChartDataCommits = (data) => {
+ const total = data.commits?.total || 0;
+ const anonymous = data.commits?.anonymous || 0;
+ const denominator = total - anonymous;
+
+ return Object.entries(data.commits || {})
+ .filter(([user]) => user !== 'total' && user !== 'anonymous')
.map(([user, val]) => ({
user,
- percentage: (total - anonymous) > 0 ? val / (total - anonymous) : 0
+ percentage: denominator > 0 ? val / denominator : 0,
}));
};
-export const getGaugeChartModifiedLines = (dataObj, totalKey) => {
- const total = dataObj[totalKey].modified;
- return Object.entries(dataObj)
- .filter(([user]) => user !== totalKey)
+export const getGaugeChartDataModifiedLines = (data) => {
+ const totalModified = data.modified_lines?.total?.modified || 0;
+
+ return Object.entries(data.modified_lines || {})
+ .filter(([user]) => user !== 'total')
.map(([user, val]) => ({
user,
- percentage: total > 0 ? val.modified / total : 0
+ percentage: totalModified > 0 ? val.modified / totalModified : 0,
}));
};
+
+export const transformCommitsDataForUser = (data, username) => {
+ const xDataCommits = [];
+ const yDataCommits = [];
+
+ for (const date in data) {
+ xDataCommits.push(date);
+ yDataCommits.push(data[date].commits[username] || 0);
+ }
+
+ return { xDataCommits, yDataCommits };
+ };
+
+export const transformModifiedLinesDataForUser = (data, username) => {
+ const xDataModifiedLines = [];
+ const yDataModifiedLines = [];
+
+ for (const date in data) {
+ const userData = data[date].modified_lines[username].modified;
+ xDataModifiedLines.push(date);
+ yDataModifiedLines.push(userData);
+ }
+
+ return { xDataModifiedLines, yDataModifiedLines };
+ };
+
+ export const transformDataForLineChartCommits = (data) => {
+ const xData = [];
+ const yData = [];
+ for (const date in data) {
+ xData.push(date);
+ yData.push(data[date].commits.total);
+ }
+
+ return { xData, yData };
+ };
+
+ export const transformDataForLineChartModifiedLines = (data) => {
+ const xDataModifedLines = [];
+ const yDataModifedLines = [];
+ for (const date in data) {
+ xDataModifedLines.push(date);
+ yDataModifedLines.push(data[date].modified_lines.total.modified);
+ }
+
+ return { xDataModifedLines, yDataModifedLines };
+ };
diff --git a/src/domain/issues.jsx b/src/domain/issues.jsx
new file mode 100644
index 0000000..dfa10eb
--- /dev/null
+++ b/src/domain/issues.jsx
@@ -0,0 +1,123 @@
+
+export const transformAssignedIssuesDataForLineChart = (data) => {
+ const xDataAssigned = [];
+ const userSeries = {};
+
+ for (const date in data) {
+ xDataAssigned.push(date);
+ const issues = data[date].issues.assigned;
+
+ for (const user in issues) {
+ if (user === 'non_assigned') continue;
+ if (!userSeries[user]) {
+ userSeries[user] = [];
+ }
+ userSeries[user].push(issues[user]);
+ }
+ }
+
+ const seriesDataAssigned = Object.keys(userSeries).map(user => ({
+ name: user,
+ data: userSeries[user]
+ }));
+
+ return { xDataAssigned, seriesDataAssigned };
+};
+
+export const getRadarAndPieDataIssuesAssigned = (data) => {
+ const radarData = {};
+ const assigned = data.issues.assigned || {};
+ for (const [user, val] of Object.entries(assigned)) {
+ if (user !== 'non_assigned' && user !== 'total') {
+ radarData[user] = val;
+ }
+ }
+ const pieData = Object.entries(radarData)
+ return { radarData, pieData };
+};
+
+export const getGaugeDataIssuesAssigned = (data) => {
+ const total = data.issues.total || 0;
+ const nonAssigned = data.issues.assigned['non_assigned'] || 0;
+ const assigned = total - nonAssigned;
+ return total > 0 ? assigned / total : 0;
+};
+
+export const getGaugeDataAssignedIssuesPerUser = (data) => {
+ const total = data.issues.total || 0;
+ const nonAssigned = data.issues.assigned['non_assigned'] || 0;
+ const totalAssigned = total - nonAssigned;
+
+ return Object.entries(data.issues.assigned)
+ .filter(([user]) => user !== 'non_assigned')
+ .map(([user, assigned]) => ({
+ user,
+ percentage: totalAssigned > 0 ? assigned / totalAssigned : 0,
+ }));
+};
+
+export const getGaugeDataClosedIssuesPerUser = (data) => {
+ return Object.entries(data.issues.closed).map(([user, closed]) => {
+ const assigned = data.issues.assigned[user] || 0;
+ return {
+ user,
+ percentage: assigned > 0 ? closed / assigned : 0,
+ };
+ });
+};
+
+export const getGaugeDataIssuesHavePR = (data) => {
+ const totalClosed = data.issues.total_closed || 0;
+ const havePullRequest = data.issues.have_pull_request || 0;
+ return totalClosed > 0 ? havePullRequest / totalClosed : 0;
+};
+
+export const transformAssignedIssuesDataForUser = (data, username) => {
+ const xDataAssignedIssues = [];
+ const yDataAssignedIssues = [];
+
+ for (const date in data) {
+ xDataAssignedIssues.push(date);
+ yDataAssignedIssues.push(data[date].issues?.assigned?.[username] || 0);
+ }
+
+ return { xDataAssignedIssues, yDataAssignedIssues };
+ };
+
+export const transformClosedIssuesDataForUser = (data, username) => {
+ const xDataClosedIssues = [];
+ const yDataClosedIssues = [];
+
+ for (const date in data) {
+ xDataClosedIssues.push(date);
+ yDataClosedIssues.push(data[date].issues?.closed?.[username] || 0);
+ }
+
+ return { xDataClosedIssues, yDataClosedIssues };
+ };
+
+export const transformIssuesDataForAreaChart = (data) => {
+ const xDataIssues = [];
+ const closedIssues = [];
+ const openIssues = [];
+
+ for (const date in data) {
+ const total = data[date].issues.total || 0;
+ const closed = data[date].issues.total_closed || 0;
+ const open = total - closed;
+ xDataIssues.push(date);
+ closedIssues.push(closed);
+ openIssues.push(open);
+ }
+ return { xDataIssues, closedIssues, openIssues };
+ };
+
+export const getPieDataIssuesStatus = (data) => {
+ const issues = data.issues
+ const pieDataIssuesStatus = [
+ ["Open",issues.total - issues.total_closed],
+ ["Closed",issues.total_closed]
+ ]
+ const pieDataIssuesStatusColor = ["red","green"];
+ return {pieDataIssuesStatus,pieDataIssuesStatusColor};
+}
\ No newline at end of file
diff --git a/src/domain/projects.jsx b/src/domain/projects.jsx
new file mode 100644
index 0000000..db0d266
--- /dev/null
+++ b/src/domain/projects.jsx
@@ -0,0 +1,504 @@
+export const getActiveIteration = (data) => {
+ const has_iterations = data.project.has_iterations;
+ const today = new Date();
+ if (!has_iterations) return "total";
+ const iterations = data.project.iterations
+ for (const key in iterations) {
+ const { startDate, endDate } = iterations[key];
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ if (today >= start && today <= end) {
+ return key;
+ }
+ }
+ return "total";
+ };
+
+export const filterHistoricDataByIteration = (data,dataHist, iterationName) => {
+ if (!data) return null;
+ if (iterationName === "total") return dataHist;
+
+ const iteration = data.project.iterations[iterationName] || null;
+ if (!iteration) return dataHist;
+
+ const startDate = new Date(iteration.startDate);
+ const endDate = new Date(iteration.endDate);
+
+ const filtered = {};
+ for (const dateStr in dataHist) {
+ const date = new Date(dateStr);
+ if (date >= startDate && date <= endDate) {
+ filtered[dateStr] = dataHist[dateStr];
+ }
+ }
+ return filtered;
+ };
+
+export const transformAssignedTasksDataForLineChart = (data,dataHistoric, selectedIteration, iterations) => {
+ let allDates = [];
+ if (!dataHistoric) return { xDataAssigned: [], seriesDataAssigned: [] }
+
+ if (selectedIteration === "total") {
+ allDates = Object.keys(dataHistoric).sort((a,b) => new Date(a) - new Date(b));
+ } else {
+ const iteration = iterations[selectedIteration];
+ if (!iteration) return { xDataAssigned: [], seriesDataAssigned: [] };
+
+ const startDate = new Date(iteration.startDate);
+ const endDate = new Date(iteration.endDate);
+
+ const dates = [];
+ let currentDate = new Date(startDate);
+ while (currentDate <= endDate) {
+ const dateStr = currentDate.toISOString().split('T')[0];
+ dates.push(dateStr);
+ currentDate.setDate(currentDate.getDate() + 1);
+ }
+ allDates = dates;
+ }
+
+ const userSeries = {};
+ const allUsers = new Set();
+
+ const assignedPerMember = data.project.metrics_by_iteration["total"]?.assigned_per_member || {};
+
+ Object.keys(assignedPerMember)
+ .filter(u => u !== "non_assigned")
+ .forEach(u => allUsers.add(u));
+
+ allUsers.forEach(u => {
+ userSeries[u] = [];
+ });
+
+ for (const date of allDates) {
+ const dayData = dataHistoric[date];
+ const iterationData = dayData?.project?.metrics_by_iteration?.[selectedIteration];
+ const assignedPerMember = iterationData?.assigned_per_member || {};
+ allUsers.forEach(u => {
+ userSeries[u].push(assignedPerMember[u] || null);
+ });
+ }
+
+ const seriesDataAssigned = Array.from(allUsers).map(u => ({
+ name: u,
+ data: userSeries[u],
+ }));
+
+ return { xDataAssigned: allDates, seriesDataAssigned };
+};
+
+export const transformFeatureDataForAreaChart = (dataHistoric, selectedIteration, iterations) => {
+ if (!dataHistoric) return { xDataFeature: [], allSeries: [] };
+
+ let allDates = [];
+
+ if (selectedIteration === "total") {
+ allDates = Object.keys(dataHistoric).sort((a, b) => new Date(a) - new Date(b));
+ } else {
+ const iteration = iterations[selectedIteration];
+ if (!iteration) return { xDataFeature: [], allSeries: [] };
+
+ const startDate = new Date(iteration.startDate);
+ const endDate = new Date(iteration.endDate);
+
+ let currentDate = new Date(startDate);
+ while (currentDate <= endDate) {
+ const dateStr = currentDate.toISOString().split('T')[0];
+ allDates.push(dateStr);
+ currentDate.setDate(currentDate.getDate() + 1);
+ }
+ }
+
+ const knownKeys = {
+ total_features_done: 'Done',
+ total_features_in_progress: 'In Progress',
+ total_features_todo: 'To Do',
+ };
+
+ const baseFeatureData = {
+ 'Done': [],
+ 'In Progress': [],
+ 'To Do': [],
+ };
+
+ const otherKeysData = {};
+ const allOtherKeys = new Set();
+
+ // Identificar claves adicionales desde los datos totales (ya que puede no haber por iteración)
+ for (const date of allDates) {
+ const metrics = dataHistoric[date]?.project?.metrics_by_iteration?.total;
+ if (!metrics) continue;
+
+ for (const [key, value] of Object.entries(metrics)) {
+ if (
+ !Object.keys(knownKeys).includes(key) &&
+ typeof value === 'number' &&
+ key.startsWith('total_features_') &&
+ Number.isInteger(value)
+ ) {
+ allOtherKeys.add(key);
+ }
+ }
+ }
+
+ for (const key of allOtherKeys) {
+ otherKeysData[key] = [];
+ }
+
+ const xDataFeature = [];
+
+ for (const date of allDates) {
+ const metrics =
+ selectedIteration === "total"
+ ? dataHistoric[date]?.project?.metrics_by_iteration?.total
+ : dataHistoric[date]?.project?.metrics_by_iteration?.[selectedIteration]
+
+ xDataFeature.push(date);
+ for (const [key, label] of Object.entries(knownKeys)) {
+ baseFeatureData[label].push(metrics?.[key] || 0);
+ }
+
+ for (const key of allOtherKeys) {
+ const value = metrics?.[key];
+ otherKeysData[key].push(Number.isInteger(value) ? value : 0);
+ }
+ }
+
+ const baseSeries = [
+ { label: 'Done', data: baseFeatureData['Done'], color: 'rgb(0, 255, 0)' },
+ { label: 'In Progress', data: baseFeatureData['In Progress'], color: 'orange' },
+ { label: 'To Do', data: baseFeatureData['To Do'], color: 'rgb(255, 0, 0)' }
+ ];
+
+ const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({
+ label: key
+ .replace('total_features_', '')
+ .replace(/_/g, ' ')
+ .replace(/\b\w/g, c => c.toUpperCase()),
+ data
+ }));
+
+ const allSeries = [...baseSeries, ...otherSeries];
+ return { xDataFeature, allSeries };
+};
+
+const formatDate = (dateStr) => {
+ const d = new Date(dateStr);
+ const day = String(d.getDate()).padStart(2, '0');
+ const month = String(d.getMonth() + 1).padStart(2, '0');
+ const year = d.getFullYear();
+ return `${day}/${month}/${year}`;
+}
+
+export const getRadarPieDataAssignedTasks = (data,iteration) => {
+ const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member;
+ const radarDataAssigned = assignedPerMember;
+ const pieDataAssigned = Object.entries(radarDataAssigned)
+ return {radarDataAssigned,pieDataAssigned}
+}
+export const getDateRangeForIteration = (data,iterationName) => {
+ const iteration = data.project.iterations[iterationName];
+
+ if (!iteration) return '';
+ return `${formatDate(iteration.startDate)} - ${formatDate(iteration.endDate)}`;
+ }
+
+export const getIssueTypeDataForChart = (projectData,selectedIteration) => {
+
+ const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration];
+
+ if (!iterationData) return [];
+
+ const {
+ total_tasks = 0,
+ total_bugs = 0,
+ total_features = 0,
+ } = iterationData;
+ const typeColorsPieChart = ["orange", "#0f58ff","red"];
+ const typePieChartData = [
+ ['Tasks', total_tasks],
+ ['Features', total_features],
+ ['Bugs', total_bugs],
+ ]
+ return { typePieChartData, typeColorsPieChart}
+ };
+
+export const getFeatureDataForChart = (projectData, selectedIteration) => {
+ const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration];
+ if (!iterationData) return { baseData: [], otherData: [] };
+
+ const baseData = [];
+ const otherData = [];
+
+ const knownKeys = {
+ total_features_todo: 'To Do',
+ total_features_in_progress: 'In Progress',
+ total_features_done: 'Done',
+ };
+
+ for (const [key, label] of Object.entries(knownKeys)) {
+ const value = iterationData[key] || 0;
+ baseData.push({ label, value });
+ }
+
+ for (const [key, value] of Object.entries(iterationData)) {
+ if (
+ key.startsWith('total_features_') &&
+ !knownKeys.hasOwnProperty(key)
+ ) {
+ const label = key
+ .replace('total_features_', '')
+ .replace(/_/g, ' ')
+ .replace(/\b\w/g, c => c.toUpperCase());
+ otherData.push({ label, value });
+ }
+ }
+ const featurePieChartData = [
+ ...baseData.map(item => [item.label, item.value]),
+ ...otherData.map(item => [item.label, item.value])
+ ]; const featureColorsPieChart = ["green", "orange","red"];
+ return {featurePieChartData,featureColorsPieChart};
+}
+
+export const getGaugeDataTasksAssigned = (data,iteration) => {
+ const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member;
+ const totalTasks = data.project.metrics_by_iteration[iteration].total_tasks;
+ const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0);
+ const percentageTasksAssigned = totalTasks > 0 ? totalAssigned / totalTasks : 0;
+ return percentageTasksAssigned;
+}
+
+export const getGaugeDataTasksStandardStatus = (data,iteration) => {
+ const totalTasks = data.project.metrics_by_iteration[iteration].total_tasks;
+ const totalInProgress = data.project.metrics_by_iteration[iteration].in_progress || 0
+ const totalDone = data.project.metrics_by_iteration[iteration].done || 0
+ const totalToDo = data.project.metrics_by_iteration[iteration].todo || 0
+
+ const totalStandardStatuses = totalInProgress + totalDone + totalToDo
+ const percentageStandardStatus = totalTasks > 0 ? totalStandardStatuses / totalTasks : 0;
+ return percentageStandardStatus;
+}
+
+export const getGaugeDataItemsIssues = (data,iteration) => {
+ const totalIssues = data.project.metrics_by_iteration[iteration].total_issues;
+ const totalItems = data.project.metrics_by_iteration[iteration].total
+ const percentageItemsIssues = totalItems > 0 ? totalIssues / totalItems : 0;
+ return percentageItemsIssues;
+}
+
+export const getGaugeDataItemIssuesWithType= (data,iteration) => {
+ const totalIssues = data.project.metrics_by_iteration[iteration].total_issues;
+ const totalIssuesWithType = data.project.metrics_by_iteration[iteration].total_issues_with_type
+ const percentageItemIssuesWithType = totalIssues > 0 ? totalIssuesWithType / totalIssues : 0
+
+ return percentageItemIssuesWithType;
+}
+
+export const getGaugeDataItemIssuesWithIteration= (data) => {
+ const taskDataNoIteration = data.project.metrics_by_iteration['no_iteration']
+ const taskDataTotal = data.project.metrics_by_iteration['total']
+ const itemsTotal = taskDataTotal.total
+ const itemsNoIteration = taskDataNoIteration.total
+ const percentageIteration = itemsTotal > 0 ? (itemsTotal - itemsNoIteration) / itemsTotal : 0
+
+ return percentageIteration;
+}
+
+export const getGaugeDataAssignedTasksPerUser = (data,iteration) => {
+ const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member;
+ const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0);
+ return Object.entries(assignedPerMember).map(([user, userTasks]) => {
+ return {
+ user,
+ percentage: totalAssigned > 0 ? userTasks / totalAssigned : 0,
+ };
+ });
+};
+
+export const getGaugeDataInProgressTasksPerUser = (data,iteration) => {
+ const inProgresPerMember = data.project.metrics_by_iteration[iteration].in_progress_per_member || 0;
+ return Object.entries(inProgresPerMember).map(([user, inProgress]) => {
+ return {
+ user,
+ percentage: inProgress > 0 ? 1 : 0,
+ };
+ });
+};
+
+export const getGaugeDataDoneTasksPerUser = (data,iteration) => {
+ const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member;
+ const donePerMember = data.project.metrics_by_iteration[iteration].done_per_member || 0;
+ return Object.entries(donePerMember).map(([user, done]) => {
+ const assigned = assignedPerMember[user]
+ return {
+ user,
+ percentage: assigned > 0 ? done / assigned : 0,
+ };
+ });
+};
+
+export const getGaugeDataStandardStatusTasksPerUser = (data,iteration) => {
+ const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member;
+ const donePerMember = data.project.metrics_by_iteration[iteration].done_per_member || 0;
+ const inProgressPerMember = data.project.metrics_by_iteration[iteration].in_progress_per_member || 0;
+ const toDoPerMember = data.project.metrics_by_iteration[iteration].todo_per_member || 0;
+ return Object.entries(assignedPerMember).map(([user, assigned]) => {
+ const done = donePerMember[user]
+ const inProgress = inProgressPerMember[user]
+ const toDo = toDoPerMember[user]
+ const standardStatus = done + inProgress + toDo
+ return {
+ user,
+ percentage: assigned > 0 ? standardStatus / assigned : 0,
+ };
+ });
+};
+
+export const transformTasksAssignedDataForUser = (data, username) => {
+ const xDataTasksAssigned = [];
+ const yDataTasksAssigned = [];
+
+ for (const date in data) {
+ xDataTasksAssigned.push(date);
+ const assigned = data[date]?.project?.metrics_by_iteration?.total?.assigned_per_member?.[username] || 0;
+ yDataTasksAssigned.push(assigned);
+ }
+
+ return { xDataTasksAssigned, yDataTasksAssigned };
+ };
+
+export const transformTasksToDoDataForUser = (data, username) => {
+ const xDataTasksToDo = [];
+ const yDataTasksToDo = [];
+
+ for (const date in data) {
+ xDataTasksToDo.push(date);
+ yDataTasksToDo.push(data[date].project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0);
+ }
+
+ return { xDataTasksToDo, yDataTasksToDo };
+ };
+
+export const transformTasksInProgressDataForUser = (data, username) => {
+ const xDataTasksInProgress = [];
+ const yDataTasksInProgress = [];
+
+ for (const date in data) {
+ xDataTasksInProgress.push(date);
+ yDataTasksInProgress.push(data[date].project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0);
+ }
+
+ return { xDataTasksInProgress, yDataTasksInProgress };
+ };
+
+
+export const transformTasksDoneDataForUser = (data, username) => {
+ const xDataTasksDone = [];
+ const yDataTasksDone = [];
+
+ for (const date in data) {
+ xDataTasksDone.push(date);
+ const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0;
+ yDataTasksDone.push(done);
+ }
+
+ return { xDataTasksDone, yDataTasksDone };
+ };
+
+export const transformTasksStandardDataForUser = (data, username) => {
+ const xDataTasksStandard = [];
+ const yDataTasksStandard = [];
+
+ for (const date in data) {
+ xDataTasksStandard.push(date);
+ const todo = data[date]?.project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0;
+ const inProgress = data[date]?.project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0;
+ const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0;
+ yDataTasksStandard.push(todo + inProgress + done);
+ }
+
+ return { xDataTasksStandard, yDataTasksStandard };
+ };
+
+export const transformTaskDataForAreaChart = (data) => {
+ const xDataTask = [];
+ const doneTask = [];
+ const inProgressTask = [];
+ const toDoTask = []
+ const otherKeysData = {}
+ const allOtherKeys = new Set();
+
+ for (const date in data) {
+ const metrics = data[date].project.metrics_by_iteration.total;
+ for (const [key, value] of Object.entries(metrics)) {
+ if (
+ !['done', 'in_progress', 'todo', 'total'].includes(key) &&
+ !key.startsWith('total_') &&
+ !key.startsWith('total') &&
+ typeof value === 'number' &&
+ Number.isInteger(value)
+ ) {
+ allOtherKeys.add(key);
+ }
+ }
+ }
+
+ for (const key of allOtherKeys) {
+ otherKeysData[key] = [];
+ }
+ for (const date in data) {
+ const metrics = data[date].project.metrics_by_iteration.total;
+ const done = metrics.done || 0;
+ const todo = metrics.todo || 0;
+ const inProgress = metrics.in_progress || 0;
+
+ xDataTask.push(date);
+ doneTask.push(done);
+ inProgressTask.push(inProgress);
+ toDoTask.push(todo)
+ for (const key of allOtherKeys) {
+ const value = metrics[key];
+ otherKeysData[key].push(Number.isInteger(value) ? value : 0);
+ }
+ }
+ const baseSeries = [
+ { label: 'Done', data: doneTask, color: "rgb(0, 255, 0)" },
+ { label: 'In Progress', data: inProgressTask, color: 'orange' },
+ { label: 'To Do', data: toDoTask, color: "rgb(255, 0, 0)" }
+ ];
+
+ const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({
+ label: key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
+ data,
+ }));
+ const allSeries = [...baseSeries, ...otherSeries];
+
+ return { xDataTask, allSeries };
+ };
+
+export const getPieDataTasksStatus = (data) => {
+ const taskData = data.project.metrics_by_iteration.total;
+ const totalInProgress = taskData.in_progress;
+ const totalDone = taskData.done;
+ const totalToDo = taskData.todo;
+ const pieDataTasksStatus = [
+ ["Todo",totalToDo],
+ ["In Progress",totalInProgress],
+ ["Done", totalDone]
+ ]
+ for (const [key, value] of Object.entries(taskData)) {
+ if (
+ !["todo", "in_progress", "done", "total"].includes(key) &&
+ !key.startsWith("total_") &&
+ !key.startsWith("total") &&
+ typeof value === "number" &&
+ Number.isInteger(value)
+ ) {
+ const label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
+ pieDataTasksStatus.push([label, value]);
+ }
+ }
+ const pieDataTasksStatusColor = ["red", "orange","green"];
+ return {pieDataTasksStatus,pieDataTasksStatusColor};
+}
\ No newline at end of file
diff --git a/src/domain/pullRequests.jsx b/src/domain/pullRequests.jsx
new file mode 100644
index 0000000..17fb0e5
--- /dev/null
+++ b/src/domain/pullRequests.jsx
@@ -0,0 +1,170 @@
+export const transformCreatedPRsDataForLineChart = (data) => {
+ const xDataCreated = [];
+ const userSeries = {};
+
+ for (const date in data) {
+ xDataCreated.push(date);
+ const created = data[date].pull_requests.created;
+
+ for (const user in created) {
+ if (!userSeries[user]) {
+ userSeries[user] = [];
+ }
+ userSeries[user].push(created[user]);
+ }
+ }
+ const seriesDataCreated = Object.keys(userSeries).map(user => ({
+ name: user,
+ data: userSeries[user]
+ }));
+
+ return { xDataCreated, seriesDataCreated };
+};
+
+export const transformMergedPRsDataForLineChart = (data) => {
+ const xDataMerged = [];
+ const userSeries = {};
+
+ for (const date in data) {
+ xDataMerged.push(date);
+ const merged = data[date].pull_requests.merged_per_member;
+
+ for (const user in merged) {
+ if (!userSeries[user]) {
+ userSeries[user] = [];
+ }
+ userSeries[user].push(merged[user]);
+ }
+ }
+ const seriesDataMerged = Object.keys(userSeries).map(user => ({
+ name: user,
+ data: userSeries[user]
+ }));
+
+ return { xDataMerged, seriesDataMerged };
+};
+
+export const getRadarAndPieDataForCreatedPRs = (data) => {
+ const radarDataCreated = data.pull_requests.created
+ const pieDataCreated = Object.entries(radarDataCreated)
+ return {radarDataCreated,pieDataCreated}
+}
+
+export const getRadarAndPieDataForMergedPRs = (data) => {
+ const radarDataMerged = data.pull_requests.merged_per_member
+
+ const pieDataMerged = Object.entries(radarDataMerged)
+ return {radarDataMerged,pieDataMerged}
+}
+
+export const getGaugeChartDataMergedPRs = (data) => {
+ const total = data.pull_requests.total
+ const totalMerged = data.pull_requests.merged
+ const totalClosed = data.pull_requests.closed
+
+ const percentageMerged = (total - totalClosed) > 0 ? totalMerged / (total - totalClosed) : 0
+
+ return percentageMerged
+}
+
+export const getGaugeChartDataReviewedPRs = (data) => {
+ const totalMerged = data.pull_requests.merged
+ const totalMergedNotByAuthor = data.pull_requests.not_merged_by_author
+
+ const percentatgeReviewed = totalMerged > 0 ? totalMergedNotByAuthor / totalMerged : 0
+
+ return percentatgeReviewed
+}
+
+export const getGaugeChartDataMergesPRs = (data) => {
+ const totalMerged = data.pull_requests.merged
+ const merges = data.commit_merges
+
+ const percentageMergesPR = merges > 0 ? totalMerged / merges : 0
+
+ return percentageMergesPR
+}
+
+export const getGaugeDataCreatedPRsPerUser = (data) => {
+ const total = data.pull_requests.total
+ return Object.entries(data.pull_requests.created).map(([user, created]) => {
+ return {
+ user,
+ percentage: total > 0 ? created / total : 0,
+ };
+ });
+};
+
+
+export const getGaugeDataMergedPRsPerUser = (data) => {
+ const totalMerged = data.pull_requests.merged
+ return Object.entries(data.pull_requests.merged_per_member).map(([user, merged]) => {
+ return {
+ user,
+ percentage: totalMerged > 0 ? merged / totalMerged : 0,
+ };
+ });
+};
+
+export const transformCreatedPRsDataForUser = (data, username) => {
+ const xDataCreatedPRs = [];
+ const yDataCreatedPRs = [];
+
+ for (const date in data) {
+ xDataCreatedPRs.push(date);
+ yDataCreatedPRs.push(data[date].pull_requests?.created?.[username] || 0);
+ }
+
+ return { xDataCreatedPRs, yDataCreatedPRs };
+ };
+
+export const transformMergedPRsDataForUser = (data, username) => {
+ const xDataMergedPRs = [];
+ const yDataMergedPRs = [];
+
+ for (const date in data) {
+ xDataMergedPRs.push(date);
+ yDataMergedPRs.push(data[date].pull_requests?.merged_per_member?.[username] || 0);
+ }
+
+ return { xDataMergedPRs, yDataMergedPRs };
+ };
+
+export const transformPRDataForAreaChart = (data) => {
+ const xDataPRs = [];
+ const mergedPRs = [];
+ const closedPRs = [];
+ const openPRs = [];
+
+ for (const date in data) {
+ const total = data[date].pull_requests.total || 0;
+ const merged = data[date].pull_requests.merged || 0;
+ const closed = data[date].pull_requests.closed || 0;
+ const open = total - merged - closed;
+
+ xDataPRs.push(date);
+ mergedPRs.push(merged);
+ closedPRs.push(closed);
+ openPRs.push(open);
+ }
+
+ const areaPRData = [
+ { label: 'Merged', data: mergedPRs, color:'rgb(0, 255, 0)' },
+ { label: 'Closed', data: closedPRs, color: 'orange' },
+ { label: 'Open', data: openPRs, color: 'rgb(255, 0, 0)' },
+ ];
+
+ return { xDataPRs, areaPRData };
+};
+
+export const getPieDataPullRequestStatus = (data) => {
+ const pullRequests = data.pull_requests;
+ const open = pullRequests.total - pullRequests.merged - pullRequests.closed
+ const pieDataPullRequestStatus = [
+ ["Open", open],
+ ["Closed", pullRequests.closed],
+ ["Merged", pullRequests.merged],
+ ];
+ const pieDataPullRequestStatusColor = ["red", "orange","green"];
+ return {pieDataPullRequestStatus,pieDataPullRequestStatusColor};
+}
\ No newline at end of file
diff --git a/src/domain/utils.jsx b/src/domain/utils.jsx
new file mode 100644
index 0000000..a1bb271
--- /dev/null
+++ b/src/domain/utils.jsx
@@ -0,0 +1,21 @@
+export const filterHistoricData = (data, days) => {
+ if (days === "lifetime") return data;
+
+ const today = new Date();
+ const cutoff = new Date(today);
+ cutoff.setDate(today.getDate() - parseInt(days));
+ const cutoffDateString = cutoff.toISOString().split("T")[0];
+
+ const filtered = {};
+ for (const date in data) {
+ if (date >= cutoffDateString) {
+ filtered[date] = data[date];
+ }
+ }
+
+ return filtered;
+};
+
+export const truncateName = (name, maxLength = 18) => {
+ return name.length > maxLength ? name.slice(0, maxLength) + '...' : name;
+};
diff --git a/src/historicalSections/commitsPageHistoricalSection.jsx b/src/historicalSections/commitsPageHistoricalSection.jsx
new file mode 100644
index 0000000..12d707b
--- /dev/null
+++ b/src/historicalSections/commitsPageHistoricalSection.jsx
@@ -0,0 +1,37 @@
+// components/historicalSection.jsx
+import React from 'react';
+import LineChartMultiple from '../components/lineChartMultiple.jsx';
+
+function CommitsPageHistoricalSection({ xDataCommits, seriesDataCommits, xDataModified, seriesDataModified, hasHistoric }) {
+ if (!hasHistoric) {
+ return (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop
+ s'hagi fet la primera execució del
+ workflow Daily Metrics.
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default CommitsPageHistoricalSection;
diff --git a/src/historicalSections/indexHistoricalSection.jsx b/src/historicalSections/indexHistoricalSection.jsx
new file mode 100644
index 0000000..0806dfa
--- /dev/null
+++ b/src/historicalSections/indexHistoricalSection.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import LineChart from '../components/lineChart';
+import AreaChart from '../components/areaChart';
+import AreaChartMultiple from '../components/areaChartMultiple';
+
+function IndexHistoricalSection({ features, historicData, xData, yData, xDataModifedLines, yDataModifedLines, xDataIssues, openIssues, closedIssues, xDataPRs, areaPRData, xDataTask, allSeries }) {
+ if (!historicData) {
+ return (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà.
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {features.includes("issues") && (
+
+ )}
+ {features.includes("pull-requests") && (
+
+ )}
+ {features.includes("projects") && (
+
+ )}
+
+ );
+}
+
+export default IndexHistoricalSection;
diff --git a/src/historicalSections/individualPageHistoricalSection.jsx b/src/historicalSections/individualPageHistoricalSection.jsx
new file mode 100644
index 0000000..797d178
--- /dev/null
+++ b/src/historicalSections/individualPageHistoricalSection.jsx
@@ -0,0 +1,167 @@
+// HistoricalCharts.jsx
+import React from 'react';
+import LineChart from '../components/lineChart.jsx';
+
+function IndividualHistoricSection({
+ historicData,
+ features,
+ xDataCommits,
+ yDataCommits,
+ xDataModifiedLines,
+ yDataModifiedLines,
+ xDataAssignedIssues,
+ yDataAssignedIssues,
+ xDataClosedIssues,
+ yDataClosedIssues,
+ xDataCreatedPRs,
+ yDataCreatedPRs,
+ xDataMergedPRs,
+ yDataMergedPRs,
+ xDataTasksAssigned,
+ yDataTasksAssigned,
+ xDataTasksToDo,
+ yDataTasksToDo,
+ xDataTasksInProgress,
+ yDataTasksInProgress,
+ xDataTasksDone,
+ yDataTasksDone,
+ xDataTasksStandard,
+ yDataTasksStandard
+}) {
+ if (!historicData) {
+ return (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop
+ s'hagi fet la primera execució del
+ workflow Daily Metrics.
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+ {features.includes("issues") && (
+
+
+
+
)}
+ {features.includes("issues") && (
+
+
+
)}
+ {features.includes("pull-requests") && (
+
+
+
)}
+ {features.includes("pull-requests") && (
+
+
+
)}
+ {features.includes("projects") && (
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+
+ )}
+
+ );
+}
+
+export default IndividualHistoricSection;
diff --git a/src/historicalSections/issuesPageHistoricalSection.jsx b/src/historicalSections/issuesPageHistoricalSection.jsx
new file mode 100644
index 0000000..a69b05f
--- /dev/null
+++ b/src/historicalSections/issuesPageHistoricalSection.jsx
@@ -0,0 +1,38 @@
+// components/issuesHistoricalSection.jsx
+import React from 'react';
+import LineChartMultiple from '../components/lineChartMultiple.jsx';
+
+function IssuesPageHistoricalSection({ historicData, xDataAssigned, seriesDataAssigned }) {
+ if (!historicData) {
+ return (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop s'hagi fet la primera execució del workflow Daily Metrics.
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default IssuesPageHistoricalSection;
diff --git a/src/historicalSections/projectsPageHistoricalSection.jsx b/src/historicalSections/projectsPageHistoricalSection.jsx
new file mode 100644
index 0000000..5162830
--- /dev/null
+++ b/src/historicalSections/projectsPageHistoricalSection.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import LineChartMultiple from '../components/lineChartMultiple';
+import AreaChartMultiple from '../components/areaChartMultiple';
+
+function ProjectsPageHistoricalSection({
+ historicData,
+ xDataAssigned,
+ seriesDataAssigned,
+ xDataFeature,
+ allSeries,
+}) {
+ if (!historicData) {
+ return (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop
+ s'hagi fet la primera execució del
+ workflow Daily Metrics.
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default ProjectsPageHistoricalSection;
\ No newline at end of file
diff --git a/src/historicalSections/pullRequestsPageHistoricalSection.jsx b/src/historicalSections/pullRequestsPageHistoricalSection.jsx
new file mode 100644
index 0000000..4cdfe29
--- /dev/null
+++ b/src/historicalSections/pullRequestsPageHistoricalSection.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import LineChartMultiple from '../components/lineChartMultiple.jsx';
+
+function PullRequestsPageHistoricalSection({ historicData, xDataCreated, seriesDataCreated, xDataMerged, seriesDataMerged }) {
+ if (!historicData) {
+ return (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop s'hagi fet la primera execució del workflow Daily Metrics.
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default PullRequestsPageHistoricalSection;
diff --git a/src/metricsSections/commitsPageMetricsSection.jsx b/src/metricsSections/commitsPageMetricsSection.jsx
new file mode 100644
index 0000000..db1ff0a
--- /dev/null
+++ b/src/metricsSections/commitsPageMetricsSection.jsx
@@ -0,0 +1,43 @@
+// components/metricsByUserSection.jsx
+import React from 'react';
+import GaugeChart from '../components/gaugeChart.jsx';
+
+function CommitsPageMetricsSection({ commitsGaugeData, modifiedLinesGaugeData, totalPeople }) {
+ return (
+
+
Metrics by User
+
+
+ Commits per user
+
+ ⓘ
+
+ Percentage of commits per user relative to the total number of commits
+
+
+
+
+ {commitsGaugeData.map(({ user, percentage }) => (
+
+ ))}
+
+
+
+ Modified lines per user
+
+ ⓘ
+
+ Percentage of modified lines per user relative to the total number of modified lines
+
+
+
+
+ {modifiedLinesGaugeData.map(({ user, percentage }) => (
+
+ ))}
+
+
+ );
+}
+
+export default CommitsPageMetricsSection;
diff --git a/src/metricsSections/indexGraphicsSection.jsx b/src/metricsSections/indexGraphicsSection.jsx
new file mode 100644
index 0000000..f4657a1
--- /dev/null
+++ b/src/metricsSections/indexGraphicsSection.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import RadarChart from '../components/radarChart';
+import PieChart from '../components/pieChart';
+import GaugeChart from '../components/gaugeChart';
+import LineChart from '../components/lineChart';
+
+function IndexGraphicsSection({ features, radarData, pieDataPullRequestStatus, pieDataPullRequestStatusColor, pieDataIssuesStatus, pieDataIssuesStatusColor, pieDataTasksStatus, pieDataTasksStatusColor, historicData, gaugeAnonymousData, xData, yData, xDataModifedLines, yDataModifedLines }) {
+ const onlyCommitsFallback = (
+
+
+
+
+ {historicData ? (
+
+ ) : (
+
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà.
+
+ )}
+
+ );
+
+ if (!(features.includes("issues") || features.includes("pull-requests") || features.includes("projects"))) {
+ return onlyCommitsFallback;
+ }
+
+ return (
+
+
+
+
+ {features.includes("pull-requests") && (
+
+ )}
+ {features.includes("issues") && (
+
+ )}
+ {features.includes("projects") && (
+
+ )}
+
+ );
+}
+
+export default IndexGraphicsSection;
diff --git a/src/metricsSections/individualPageMetricsSection.jsx b/src/metricsSections/individualPageMetricsSection.jsx
new file mode 100644
index 0000000..c208a9f
--- /dev/null
+++ b/src/metricsSections/individualPageMetricsSection.jsx
@@ -0,0 +1,177 @@
+// UserGaugeSection.jsx
+import React from 'react';
+import GaugeChart from '../components/gaugeChart.jsx';
+
+function IndividualMetricsSection({
+ selectedUser,
+ totalPeople,
+ percentageCommits,
+ percentageModifiedlLines,
+ percentageAssigned,
+ percentageIssuesClosed,
+ percentageCreated,
+ percentageMerged,
+ percentageTasksAssigned,
+ percentageTasksInProgress,
+ percentageTasksDone,
+ percentageTasksStandard,
+ features
+}) {
+ return (
+
+
+ Commits
+
+ ⓘ
+ Percentage of commits made by the user relative to the total number of commits
+
+
+
+
+
+
+ Modified Lines
+
+ ⓘ
+ Percentage of commits made by the user relative to the total number of commits
+
+
+
+
+ {features.includes("issues") && (
+
+
+ Issues assigned
+
+ ⓘ
+ Percentage of issues assigned to the user relative to the number of assigned issues
+
+
+
+ )}
+ {features.includes("issues") && (
+
+
+ Issues closed
+
+ ⓘ
+ Percentage of issues closed by the user relative to the issues assigned to the user
+
+
+
+ )}
+ {features.includes("pull-requests") && (
+
+
+ Pull Requests created
+
+ ⓘ
+ Percentage of pull requests created by the user relative to the total number of pull requests
+
+
+
+ )}
+ {features.includes("pull-requests") && (
+
+
+ Pull Requests merged
+
+ ⓘ
+ Percentage of pull requests merged by the user relative to the total number of pull requests merged
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+ Tasks assigned
+
+ ⓘ
+ Percentage of tasks assigned to the user relative to the number of assigned tasks
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+ Tasks In Progress
+
+ ⓘ
+
+ Shows if the user is actively working on at least one task.
+ If the gauge is at 100%, the user has at least one task in progress.
+ If it's at 0%, the user currently has no tasks in progress.
+
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+ Tasks Done
+
+ ⓘ
+ Percentage of tasks done by the user relative to the tasks assigned to the user
+
+
+
+ )}
+ {features.includes("projects") && (
+
+
+ Tasks with standard status
+
+ ⓘ
+ Percentage of tasks with standard status (ToDo, In Progress, Done) of the user relative to the user assigned tasks
+
+
+
+ )}
+
+ );
+}
+
+export default IndividualMetricsSection;
\ No newline at end of file
diff --git a/src/metricsSections/issuesPageMetricsSection.jsx b/src/metricsSections/issuesPageMetricsSection.jsx
new file mode 100644
index 0000000..81d319a
--- /dev/null
+++ b/src/metricsSections/issuesPageMetricsSection.jsx
@@ -0,0 +1,53 @@
+// components/issuesMetricsByUserSection.jsx
+import React from 'react';
+import GaugeChart from '../components/gaugeChart.jsx';
+
+function IssuesPageMetricsSection({ gaugeDataAssigned, gaugeDataClosed, totalPeople }) {
+ return (
+
+
Metrics by User
+
+
+ Issues assigned per user
+
+ ⓘ
+
+ Percentage of issues assigned per user relative to the number of assigned issues
+
+
+
+
+ {gaugeDataAssigned.map(({ user, percentage }) => (
+
+ ))}
+
+
+
+ Issues closed per user
+
+ ⓘ
+
+ Percentage of issues closed per user relative to the issues assigned to that user
+
+
+
+
+ {gaugeDataClosed.map(({ user, percentage }) => (
+
+ ))}
+
+
+ );
+}
+
+export default IssuesPageMetricsSection;
diff --git a/src/metricsSections/projectsPageMetricsSection.jsx b/src/metricsSections/projectsPageMetricsSection.jsx
new file mode 100644
index 0000000..09727c5
--- /dev/null
+++ b/src/metricsSections/projectsPageMetricsSection.jsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import GaugeChart from '../components/gaugeChart';
+
+function ProjectsPageMetricsSection({
+ gaugeDataAssignedTasks,
+ gaugeDataInProgressTasks,
+ gaugeDataDoneTasks,
+ gaugeDataStandardTasks,
+ totalPeople,
+}) {
+ return (
+
+
Metrics by User
+
+
+ Tasks assigned per user
+
+ ⓘ
+ Percentage of tasks assigned per user relative to the number of assigned tasks
+
+
+
+ {gaugeDataAssignedTasks.map(({ user, percentage }) => (
+
+ ))}
+
+
+
+ Tasks In Progress per user
+
+ ⓘ
+
+ Shows if the user is actively working on at least one task.
+ If the gauge is at 100%, the user has at least one task in progress.
+ If it's at 0%, the user currently has no tasks in progress.
+
+
+
+
+ {gaugeDataInProgressTasks.map(({ user, percentage }) => (
+
+ ))}
+
+
+
+ Tasks Done per user
+
+ ⓘ
+
+ Percentage of tasks done per user relative to the tasks assigned to that user
+
+
+
+
+ {gaugeDataDoneTasks.map(({ user, percentage }) => (
+
+ ))}
+
+
+
+ Tasks with standard status per user
+
+ ⓘ
+
+ Percentage of tasks with standard status (ToDo, In Progress, Done) per user relative to their assigned tasks
+
+
+
+
+ {gaugeDataStandardTasks.map(({ user, percentage }) => (
+
+ ))}
+
+
+ );
+}
+
+export default ProjectsPageMetricsSection;
\ No newline at end of file
diff --git a/src/metricsSections/pullRequestsPageMetricsSection.jsx b/src/metricsSections/pullRequestsPageMetricsSection.jsx
new file mode 100644
index 0000000..33ce11d
--- /dev/null
+++ b/src/metricsSections/pullRequestsPageMetricsSection.jsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import GaugeChart from '../components/gaugeChart.jsx';
+
+function PullRequestsMetricsSection({ gaugeDataCreatedPRs, gaugeDataMergedPRs, totalPeople }) {
+ return (
+
+
Metrics by User
+
+
+ Pull requests created per user
+
+ ⓘ
+
+ Percentage of pull requests created per user relative to the total number of pull requests
+
+
+
+
+ {gaugeDataCreatedPRs.map(({ user, percentage }) => (
+
+ ))}
+
+
+
+ Pull requests merged per user
+
+ ⓘ
+
+ Percentage of pull requests merged per user relative to the number of merged pull requests
+
+
+
+
+ {gaugeDataMergedPRs.map(({ user, percentage }) => (
+
+ ))}
+
+
+ );
+}
+
+export default PullRequestsMetricsSection;
diff --git a/src/pages/commitsPage.jsx b/src/pages/commitsPage.jsx
index 2411c74..7ecf644 100644
--- a/src/pages/commitsPage.jsx
+++ b/src/pages/commitsPage.jsx
@@ -1,152 +1,78 @@
import React from 'react';
-import GaugeChart from '../components/gaugeChart.jsx';
-import RadarPieToggle from '../components/radarPieToggle.jsx';
-import LineChartMultiple from '../components/lineChartMultiple.jsx';
import usePersistentStateSession from '../components/usePersistentStateSession.jsx';
-import '../styles/commits.css';
+import HistoricalToggle from '../components/historicalToggle.jsx';
+import DateRangeSelector from '../components/dateRangeSelector.jsx';
+import CommitsPageSummarySection from '../summarySections/commitsPageSummarySection.jsx';
+import CommitsPageMetricsSection from '../metricsSections/commitsPageMetricsSection.jsx';
+import CommitsPageHistoricalSection from '../historicalSections/commitsPageHistoricalSection.jsx';
+import '../styles/elements.css';
import {
- filterHistoricData,
transformCommitsDataForLineChart,
transformModifiedLinesDataForLineChart,
- prepareRadarData,
- getPieChartData,
- getGaugeChartPercentages,
- getGaugeChartModifiedLines
+ getPieChartDataCommits,
+ getPieChartDataModifiedLines,
+ getGaugeChartDataCommits,
+ getGaugeChartDataModifiedLines,
+ GetRadarDataCommits,
+ GetRadarDataModifiedLines,
} from '../domain/commits.jsx';
-function CommitsPage({ data, historicData, features }) {
- const commitsData = data.commits;
- const modifiedLinesData = data.modified_lines;
+import { filterHistoricData } from '../domain/utils.jsx';
+function CommitsPage({ data, historicData, features }) {
const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalCommits', false);
const [dateRange, setDateRange] = usePersistentStateSession('dateRangeCommits', "7");
- const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
- const { xDataCommits, seriesDataCommits } = transformCommitsDataForLineChart(filteredhistoricaData);
- const { xDataModified, seriesModified } = transformModifiedLinesDataForLineChart(filteredhistoricaData);
- const radarChartModifiedLines = prepareRadarData(modifiedLinesData);
+ const filteredHistoricData = historicData ? filterHistoricData(historicData, dateRange) : null;
+
+ const { xData: xDataCommits, seriesData: seriesDataCommits } = transformCommitsDataForLineChart(filteredHistoricData || {});
+ const { xData: xDataModified, seriesData: seriesDataModified } = transformModifiedLinesDataForLineChart(filteredHistoricData || {});
- const totalCommits = commitsData.total;
- const totalModifiedLines = modifiedLinesData.total.modified;
- const totalPeople = Object.keys(data.avatars).length;
+ const totalCommits = data.commits?.total || 0;
+ const gaugeDataAnonymous = totalCommits > 0 ? ((totalCommits - (data.commits?.anonymous || 0)) / totalCommits) : 0;
- const dataPieChartCommits = getPieChartData(commitsData);
- const dataPieChartModifiedLines = getPieChartData(modifiedLinesData, 'modified');
- const commitsGaugeData = getGaugeChartPercentages(commitsData, 'total', 'anonymous');
- const modifiedLinesGaugeData = getGaugeChartModifiedLines(modifiedLinesData, 'total');
+ const dataPieChartCommits = getPieChartDataCommits(data);
+ const dataPieChartModifiedLines = getPieChartDataModifiedLines(data);
+ const commitsGaugeData = getGaugeChartDataCommits(data);
+ const modifiedLinesGaugeData = getGaugeChartDataModifiedLines(data);
+ const radarChartCommits = GetRadarDataCommits(data);
+ const radarChartModifiedLines = GetRadarDataModifiedLines(data);
+
+ const totalPeople = Object.keys(data.avatars || {}).length || 1;
return (
Commits
-
-
- setShowHistorical(false)} className={!showHistorical ? 'selected' : ''}>
- Current
-
- setShowHistorical(true)} className={showHistorical ? 'selected' : ''}>
- Historical
-
-
-
- {showHistorical && (
-
- setDateRange(e.target.value)}
- value={dateRange}
- style={{ marginLeft: '1rem' }}
- >
- Last 7 days
- Last 15 days
- Last 30 days
- Last 3 months
- Lifetime
-
-
- )}
+
+
{!showHistorical && (
<>
-
-
Summary
-
-
-
-
-
-
-
-
-
- Non-anonymous commits
-
- ⓘ
- Percentage of commits that have a member of the project as its author
-
-
- 0 ? (totalCommits - commitsData.anonymous) / totalCommits : 0}
- totalPeople={1}
- />
-
-
-
-
-
-
Metrics by User
-
- Commits per user
-
- ⓘ
- Percentage of commits per user relative to the total number of commits
-
-
-
- {commitsGaugeData.map(({ user, percentage }) => (
-
- ))}
-
-
-
- Modified lines per user
-
- ⓘ
- Percentage of modified lines per user relative to the total number of modified lines
-
-
-
- {modifiedLinesGaugeData.map(({ user, percentage }) => (
-
- ))}
-
-
+
+
>
)}
{showHistorical && (
- <>
- {historicData ? (
-
- ) : (
-
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop s'hagi fet la primera execució del workflow Daily Metrics.
-
- )}
- >
+
)}
);
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 909580e..6a6f238 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -1,472 +1,140 @@
-import React, { useState } from 'react';
-import "../styles/index.css";
-import PieChart from '../components/pieChart';
-import RadarChart from '../components/radarChart';
-import LineChart from '../components/lineChart';
-import AreaChart from '../components/areaChart';
-import GaugeChart from '../components/gaugeChart';
-import AreaChartMultiple from '../components/areaChartMultiple';
-import usePersistentStateSession from '../components/usePersistentStateSession';
-
-function Index({data,historicData,features}) {
+import React from 'react';
+import "../styles/index.css";
+import usePersistentStateSession from '../components/usePersistentStateSession';
+import HistoricalToggle from '../components/historicalToggle.jsx';
+import DateRangeSelector from '../components/dateRangeSelector.jsx';
+
+import IndexStatsSection from '../summarySections/indexStatsSection.jsx';
+import IndexGraphicsSection from '../metricsSections/indexGraphicsSection.jsx';
+import IndexHistoricalSection from '../historicalSections/indexHistoricalSection.jsx';
+
+import { filterHistoricData } from '../domain/utils';
+import {
+ getGaugeDataAnonymous,
+ transformDataForLineChartCommits,
+ transformDataForLineChartModifiedLines
+} from '../domain/commits';
+import {
+ getGaugeDataIssuesAssigned,
+ getGaugeDataIssuesHavePR,
+ transformIssuesDataForAreaChart,
+ getPieDataIssuesStatus
+} from '../domain/issues';
+import {
+ getGaugeChartDataMergedPRs,
+ getGaugeChartDataReviewedPRs,
+ getGaugeChartDataMergesPRs,
+ transformPRDataForAreaChart,
+ getPieDataPullRequestStatus
+} from '../domain/pullRequests';
+import {
+ getGaugeDataTasksAssigned,
+ getGaugeDataTasksStandardStatus,
+ getGaugeDataItemsIssues,
+ getGaugeDataItemIssuesWithType,
+ getGaugeDataItemIssuesWithIteration,
+ transformTaskDataForAreaChart,
+ getPieDataTasksStatus
+} from '../domain/projects';
+
+function Index({ data, historicData, features }) {
const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIndex', false);
const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIndex', "7");
- const pullRequests = data.pull_requests;
- const open = pullRequests.total - pullRequests.merged - pullRequests.closed
- const issues = data.issues
- const datapullRequests = [
- ["Open", open],
- ["Closed", pullRequests.closed],
- ["Merged", pullRequests.merged],
- ];
- const dataissues = [
- ["Open",issues.total - issues.total_closed],
- ["Closed",issues.total_closed]
- ]
- const filterHistoricData = (data, days) => {
- if (days === "lifetime") return data;
-
- const today = new Date();
- const cutoff = new Date(today);
- cutoff.setDate(today.getDate() - parseInt(days));
- const cutoffDateString = cutoff.toISOString().split("T")[0];
- const filtered = {};
- for (const date in data) {
- if (date >= cutoffDateString) {
- filtered[date] = data[date];
- }
- }
- return filtered;
- };
const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
- const taskData = data.project.metrics_by_iteration.total;
- const totalTasks = taskData.total;
- const totalInProgress = taskData.in_progress;
- const totalDone = taskData.done;
- const totalToDo = taskData.todo;
- const dataTasks = [
- ["Todo",totalToDo],
- ["In Progress",totalInProgress],
- ["Done", totalDone]
- ]
-
- for (const [key, value] of Object.entries(taskData)) {
- if (
- !["todo", "in_progress", "done", "total"].includes(key) &&
- !key.startsWith("total_") &&
- !key.startsWith("total") &&
- typeof value === "number" &&
- Number.isInteger(value)
- ) {
- const label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
- dataTasks.push([label, value]);
- }
- }
- const colorsPR = ["red", "orange","green"];
- const colorsIssues = ["red","green"];
- const colorsProject = ["red", "orange","green"];
+ // Radar Data
let radarData = {};
-
- radarData['Non-Anonymous Commits'] =
- data.commits.total > 0 ? (data.commits.total - data.commits.anonymous) / data.commits.total : 0;
+ radarData['Non-Anonymous Commits'] = getGaugeDataAnonymous(data);
if (features.includes('issues')) {
- radarData['Issues Assigned'] =
- data.issues.total > 0 ? (data.issues.total - data.issues.assigned.non_assigned) / data.issues.total : 0;
-
- radarData['Issues associated PR'] =
- data.issues.total_closed > 0 ? data.issues.have_pull_request / data.issues.total_closed : 0;
+ radarData['Issues Assigned'] = getGaugeDataIssuesAssigned(data);
+ radarData['Issues associated PR'] = getGaugeDataIssuesHavePR(data);
}
if (features.includes('pull-requests')) {
- radarData['Pull Requests Merged'] =
- (data.pull_requests.total - data.pull_requests.closed) > 0 ? data.pull_requests.merged / (data.pull_requests.total - data.pull_requests.closed) : 0;
-
- radarData['Pull Requests Reviewed'] =
- data.pull_requests.merged > 0 ? data.pull_requests.not_merged_by_author / data.pull_requests.merged : 0;
-
- radarData['Pull Requests Merges'] =
- data.commit_merges > 0 ? data.pull_requests.merged / data.commit_merges : 0;
+ radarData['Pull Requests Merged'] = getGaugeChartDataMergedPRs(data);
+ radarData['Pull Requests Reviewed'] = getGaugeChartDataReviewedPRs(data);
+ radarData['Pull Requests Merges'] = getGaugeChartDataMergesPRs(data);
}
- if (features.includes('projects')) {
- const taskData = data.project.metrics_by_iteration.total;
- const { non_assigned, ...assignedPerMember } = taskData.assigned_per_member;
- const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0);
- const totalTasks = taskData.total_tasks;
- radarData['Tasks Assigned'] =
- totalTasks > 0 ? totalAssigned / totalTasks : 0
- const todo = taskData.todo
- const inProgress = taskData.in_progress
- const done = taskData.done
- const standard = todo + inProgress + done;
- radarData['Tasks With Standard Status'] =
- totalTasks > 0 ? standard / totalTasks : 0
+ if (features.includes('projects')) {
+ radarData['Tasks Assigned'] = getGaugeDataTasksAssigned(data, "total");
+ radarData['Tasks With Standard Status'] = getGaugeDataTasksStandardStatus(data, "total");
+ radarData['Items that are Issues'] = getGaugeDataItemsIssues(data, "total");
+ radarData['Projects Issues with type'] = getGaugeDataItemIssuesWithType(data, "total");
- const totalDraftIssues = taskData.total
- const totalIssues = taskData.total_issues
- radarData['Items that are Issues'] =
- totalDraftIssues > 0 ? totalIssues / totalDraftIssues : 0
- const IssuesWithType = taskData.total_issues_with_type
- radarData['Projects Issues with type'] =
- totalIssues > 0 ? IssuesWithType / totalIssues : 0
- if(data.project.has_iterations) {
- const noIterationDraftIssues = data.project.metrics_by_iteration.no_iteration.total
- const iterationDraftIssues = totalDraftIssues - noIterationDraftIssues
- radarData['Items with iteration'] =
- totalDraftIssues > 0 ? iterationDraftIssues / totalDraftIssues : 0
+ if (data.project.has_iterations) {
+ radarData['Items with iteration'] = getGaugeDataItemIssuesWithIteration(data, "total");
}
}
- const transformDataForLineChart = (data) => {
- const xData = [];
- const yData = [];
- for (const date in data) {
- xData.push(date);
- yData.push(data[date].commits.total);
- }
-
- return { xData, yData };
- };
- const { xData, yData } = transformDataForLineChart(filteredhistoricaData);
+ // Pie Chart Data
+ const { pieDataPullRequestStatus, pieDataPullRequestStatusColor } = getPieDataPullRequestStatus(data);
+ const { pieDataIssuesStatus, pieDataIssuesStatusColor } = getPieDataIssuesStatus(data);
+ const { pieDataTasksStatus, pieDataTasksStatusColor } = getPieDataTasksStatus(data);
- const transformDataForLineChartModifiedLines = (data) => {
- const xDataModifedLines = [];
- const yDataModifedLines = [];
- for (const date in data) {
- xDataModifedLines.push(date);
- yDataModifedLines.push(data[date].modified_lines.total.modified);
- }
-
- return { xDataModifedLines, yDataModifedLines };
- };
+ // Line/Area Chart Data
+ const { xData, yData } = transformDataForLineChartCommits(filteredhistoricaData);
const { xDataModifedLines, yDataModifedLines } = transformDataForLineChartModifiedLines(filteredhistoricaData);
-
- const transformIssuesDataForAreaChart = (data) => {
- const xDataIssues = [];
- const closedIssues = [];
- const openIssues = [];
-
- for (const date in data) {
- const total = data[date].issues.total || 0;
- const closed = data[date].issues.total_closed || 0;
- const open = total - closed;
- xDataIssues.push(date);
- closedIssues.push(closed);
- openIssues.push(open);
- }
- return { xDataIssues, closedIssues, openIssues };
- };
const { xDataIssues, closedIssues, openIssues } = transformIssuesDataForAreaChart(filteredhistoricaData);
- const transformPRDataForAreaChart = (data) => {
- const xDataPRs = [];
- const mergedPRs = [];
- const openPRs = [];
-
- for (const date in data) {
- const total = data[date].pull_requests.total || 0;
- const merged = data[date].pull_requests.merged || 0;
- const closed = data[date].pull_requests.closed || 0;
- const open = total - merged - closed;
- xDataPRs.push(date);
- mergedPRs.push(merged);
- openPRs.push(open);
- }
- return { xDataPRs, mergedPRs, openPRs };
- };
- const { xDataPRs, mergedPRs, openPRs } = transformPRDataForAreaChart(filteredhistoricaData);
-
- const transformTaskDataForAreaChart = (data) => {
- const xDataTask = [];
- const doneTask = [];
- const inProgressTask = [];
- const toDoTask = []
- const otherKeysData = {}
- const allOtherKeys = new Set();
-
- for (const date in data) {
- const metrics = data[date].project.metrics_by_iteration.total;
- for (const [key, value] of Object.entries(metrics)) {
- if (
- !['done', 'in_progress', 'todo', 'total'].includes(key) &&
- !key.startsWith('total_') &&
- !key.startsWith('total') &&
- typeof value === 'number' &&
- Number.isInteger(value)
- ) {
- allOtherKeys.add(key);
- }
- }
- }
-
- for (const key of allOtherKeys) {
- otherKeysData[key] = [];
- }
- for (const date in data) {
- const metrics = data[date].project.metrics_by_iteration.total;
- const done = metrics.done || 0;
- const todo = metrics.todo || 0;
- const inProgress = metrics.in_progress || 0;
+ const { xDataPRs, areaPRData } = transformPRDataForAreaChart(filteredhistoricaData);
+ const { xDataTask, allSeries } = transformTaskDataForAreaChart(filteredhistoricaData);
- xDataTask.push(date);
- doneTask.push(done);
- inProgressTask.push(inProgress);
- toDoTask.push(todo)
- for (const key of allOtherKeys) {
- const value = metrics[key];
- otherKeysData[key].push(Number.isInteger(value) ? value : 0);
- }
- }
- return { xDataTask, doneTask, inProgressTask,toDoTask,otherKeysData };
- };
- const { xDataTask, doneTask, inProgressTask,toDoTask,otherKeysData } = transformTaskDataForAreaChart(filteredhistoricaData);
-
- const baseSeries = [
- { label: 'Done', data: doneTask, color: "rgb(0, 255, 0)" },
- { label: 'In Progress', data: inProgressTask, color: 'orange' },
- { label: 'To Do', data: toDoTask, color: "rgb(255, 0, 0)" }
- ];
+ const gaugeAnonymousData = getGaugeDataAnonymous(data);
- const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({
- label: key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
- data,
- }));
-
- const allSeries = [...baseSeries, ...otherSeries];
-
-
return (
General overview
-
-
-
-
Project Stats
-
-
-
Total Members: {Object.keys(data.commits).filter(user => user !== 'anonymous' && user !== 'total').length}
-
Total Commits: {data.commits.total}
- {features.includes("issues") && (
-
Total Issues: {data.issues.total}
) }
- {features.includes("pull-requests") && (
-
Total Pull Requests: {data.pull_requests.total}
)}
-
Total Additions: {data.modified_lines.total.additions}
-
Total Deletions: {data.modified_lines.total.deletions}
-
Total Modifications: {data.modified_lines.total.modified}
-
Total Lines of code: {data.modified_lines.total.additions - data.modified_lines.total.deletions}
- {features.includes("projects") && (
-
Total Tasks: {data.project.metrics_by_iteration.total.total_tasks}
)}
- {features.includes("projects") && (
-
Total Features: {data.project.metrics_by_iteration.total.total_features}
)}
-
-
-
- {(features.includes("issues") || features.includes("pull-requests") || features.includes("projects")) && (
-
-
- setShowHistorical(false)}
- className={!showHistorical ? 'selected' : ''}
- >
- Current
-
- setShowHistorical(true)}
- className={showHistorical ? 'selected' : ''}
- >
- Historical
-
-
-
)}
-
- {showHistorical && (
-
- setDateRange(e.target.value)}
- value={dateRange}
- style={{ marginLeft: '1rem' }}
- >
- Last 7 days
- Last 15 days
- Last 30 days
- Last 3 months
- Lifetime
-
-
- )}
-
- {!showHistorical && (
- <>
- {(features.includes("issues") || features.includes("pull-requests") || features.includes("projects")) ? (
-
-
-
-
- {features.includes("pull-requests") && (
-
- )}
- {features.includes("issues") && (
-
- )}
- {features.includes("projects") && (
-
- )}
-
- ) : (
-
-
-
- 0 ? (data.commits.total - data.commits.anonymous) / data.commits.total : 0 }
- totalPeople={1}
- />
-
- { historicData ?
-
- (
) :
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics.
-
}
-
-
- )}
- >
-)}
-
- {showHistorical && (
-
- {historicData ? (
-
-
-
-
-
-
-
- {features.includes("issues") && (
-
)}
- {features.includes("pull-requests") && (
-
)}
- {features.includes("projects") && (
-
)}
-
- ) : (
-
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics.
-
- )}
-
+
+
+ {(features.includes("issues") || features.includes("pull-requests") || features.includes("projects")) && (
+
+ )}
+
+
+
+ {!showHistorical ? (
+
+ ) : (
+
)}
- );
+ );
}
export default Index;
diff --git a/src/pages/individual.jsx b/src/pages/individual.jsx
deleted file mode 100644
index b0a811d..0000000
--- a/src/pages/individual.jsx
+++ /dev/null
@@ -1,593 +0,0 @@
-import React, { useState } from 'react';
-import "../styles/individual.css";
-import LineChart from '../components/lineChart';
-import GaugeChart from '../components/gaugeChart';
-import usePersistentStateSession from '../components/usePersistentStateSession';
-import usePersistentState from '../components/usePersistentState';
-
-function Individual({ data, historicData, features }) {
- const [selectedUser, setSelectedUser] = usePersistentState("selectedUser",Object.keys(data.avatars)[0]);
- const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIndividual', false);
- const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIndividual', "7");
- const filterHistoricData = (data, days) => {
- if (days === "lifetime") return data;
-
- const today = new Date();
- const cutoff = new Date(today);
- cutoff.setDate(today.getDate() - parseInt(days));
- const cutoffDateString = cutoff.toISOString().split("T")[0];
- const filtered = {};
- for (const date in data) {
- if (date >= cutoffDateString) {
- filtered[date] = data[date];
- }
- }
-
- return filtered;
- };
- const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
- const users = Object.keys(data.avatars);
- const avatar = data.avatars[selectedUser] || "";
- const commits = data.commits[selectedUser] || 0;
- const modifiedLines = data.modified_lines[selectedUser] || { additions: 0, deletions: 0, modified: 0 };
- const streak = data.commit_streak[selectedUser] || 0;
- const longestStreak = data.longest_commit_streak_per_user[selectedUser] || 0;
- const issuesAssigned = data.issues.assigned[selectedUser] || 0;
- const issuesClosed = data.issues.closed[selectedUser] || 0;
- const totalCommits = data.commits['total']
- const totalModifiedLines = data.modified_lines['total'].modified
- const totalPeople = Object.keys(data.avatars).length;
- const tasksAssigned = data.project.metrics_by_iteration.total.assigned_per_member[selectedUser] || 0
- const tasksClosed = data.project.metrics_by_iteration.total.done_per_member[selectedUser] || 0
- const tasksInProgress = data.project.metrics_by_iteration.total.in_progress_per_member[selectedUser] || 0
- const tasksTodo = data.project.metrics_by_iteration.total.todo_per_member[selectedUser] || 0
- const tasksStandard = tasksClosed + tasksInProgress + tasksTodo
- const truncateName = (name, maxLength = 18) => {
- return name.length > maxLength ? name.slice(0, maxLength) + '...' : name;
- };
- const userCommits = data.commits[selectedUser];
- const percentageCommits = totalCommits > 0 ? userCommits / totalCommits: 0;
- const userModifiedLines = data.modified_lines[selectedUser].modified
- const percentageModifiedlLines = totalModifiedLines > 0 ? userModifiedLines / totalModifiedLines : 0
- const totalAssignedIssues = data.issues.total - data.issues.assigned["non_assigned"]
- const pullRequestsCreated = data.pull_requests.created[selectedUser]
- const pullRequestsMerged = data.pull_requests.merged_per_member[selectedUser]
- const percentageAssigned = totalAssignedIssues > 0 ? issuesAssigned / totalAssignedIssues: 0
- const percentageIssuesClosed = issuesAssigned > 0 ? issuesClosed/issuesAssigned: 0
- const percentageCreated = data.pull_requests.total > 0 ? data.pull_requests.created[selectedUser] / data.pull_requests.total: 0
- const percentageMerged = data.pull_requests.merged > 0 ? data.pull_requests.merged_per_member[selectedUser] / data.pull_requests.merged: 0
- const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration.total.assigned_per_member;
- const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0);
- const percentageTasksAssigned = totalAssigned > 0 ? tasksAssigned / totalAssigned : 0;
- const percentageTasksDone = tasksAssigned > 0 ? tasksClosed / tasksAssigned : 0;
- const percentageTasksInProgress = tasksInProgress > 0 ? tasksInProgress / tasksInProgress : 0;
- const percentageTasksStandard = tasksAssigned > 0 ? tasksStandard / tasksAssigned : 0;
- const transformCommitsDataForUser = (data, username) => {
- const xDataCommits = [];
- const yDataCommits = [];
-
- for (const date in data) {
- xDataCommits.push(date);
- yDataCommits.push(data[date].commits[username] || 0);
- }
-
- return { xDataCommits, yDataCommits };
- };
-
- const { xDataCommits, yDataCommits } = transformCommitsDataForUser(filteredhistoricaData, selectedUser)
-
- const transformModifiedLinesDataForUser = (data, username) => {
- const xDataModifiedLines = [];
- const yDataModifiedLines = [];
-
- for (const date in data) {
- const userData = data[date].modified_lines[username].modified;
- xDataModifiedLines.push(date);
- yDataModifiedLines.push(userData);
- }
-
- return { xDataModifiedLines, yDataModifiedLines };
- };
- const { xDataModifiedLines, yDataModifiedLines } = transformModifiedLinesDataForUser(filteredhistoricaData, selectedUser)
-
- const transformAssignedIssuesDataForUser = (data, username) => {
- const xDataAssignedIssues = [];
- const yDataAssignedIssues = [];
-
- for (const date in data) {
- xDataAssignedIssues.push(date);
- yDataAssignedIssues.push(data[date].issues?.assigned?.[username] || 0);
- }
-
- return { xDataAssignedIssues, yDataAssignedIssues };
- };
- const { xDataAssignedIssues, yDataAssignedIssues } = transformAssignedIssuesDataForUser(filteredhistoricaData, selectedUser);
-
- const transformClosedIssuesDataForUser = (data, username) => {
- const xDataClosedIssues = [];
- const yDataClosedIssues = [];
-
- for (const date in data) {
- xDataClosedIssues.push(date);
- yDataClosedIssues.push(data[date].issues?.closed?.[username] || 0);
- }
-
- return { xDataClosedIssues, yDataClosedIssues };
- };
-
- const { xDataClosedIssues, yDataClosedIssues } = transformClosedIssuesDataForUser(filteredhistoricaData, selectedUser);
-
-
- const transformCreatedPRsDataForUser = (data, username) => {
- const xDataCreatedPRs = [];
- const yDataCreatedPRs = [];
-
- for (const date in data) {
- xDataCreatedPRs.push(date);
- yDataCreatedPRs.push(data[date].pull_requests?.created?.[username] || 0);
- }
-
- return { xDataCreatedPRs, yDataCreatedPRs };
- };
- const { xDataCreatedPRs, yDataCreatedPRs } = transformCreatedPRsDataForUser(filteredhistoricaData, selectedUser);
-
- const transformMergedPRsDataForUser = (data, username) => {
- const xDataMergedPRs = [];
- const yDataMergedPRs = [];
-
- for (const date in data) {
- xDataMergedPRs.push(date);
- yDataMergedPRs.push(data[date].pull_requests?.merged_per_member?.[username] || 0);
- }
-
- return { xDataMergedPRs, yDataMergedPRs };
- };
- const { xDataMergedPRs, yDataMergedPRs } = transformMergedPRsDataForUser(filteredhistoricaData, selectedUser);
- const transformTasksAssignedDataForUser = (data, username) => {
- const xDataTasksAssigned = [];
- const yDataTasksAssigned = [];
-
- for (const date in data) {
- xDataTasksAssigned.push(date);
- const assigned = data[date]?.project?.metrics_by_iteration?.total?.assigned_per_member?.[username] || 0;
- yDataTasksAssigned.push(assigned);
- }
-
- return { xDataTasksAssigned, yDataTasksAssigned };
- };
-
- const { xDataTasksAssigned, yDataTasksAssigned } = transformTasksAssignedDataForUser(filteredhistoricaData, selectedUser);
-
-
- const transformTasksToDoDataForUser = (data, username) => {
- const xDataTasksToDo = [];
- const yDataTasksToDo = [];
-
- for (const date in data) {
- xDataTasksToDo.push(date);
- yDataTasksToDo.push(data[date].project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0);
- }
-
- return { xDataTasksToDo, yDataTasksToDo };
- };
-
- const { xDataTasksToDo, yDataTasksToDo } = transformTasksToDoDataForUser(filteredhistoricaData, selectedUser);
-
- const transformTasksInProgressDataForUser = (data, username) => {
- const xDataTasksInProgress = [];
- const yDataTasksInProgress = [];
-
- for (const date in data) {
- xDataTasksInProgress.push(date);
- yDataTasksInProgress.push(data[date].project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0);
- }
-
- return { xDataTasksInProgress, yDataTasksInProgress };
- };
-
- const { xDataTasksInProgress, yDataTasksInProgress } = transformTasksInProgressDataForUser(filteredhistoricaData, selectedUser);
-
- const transformTasksDoneDataForUser = (data, username) => {
- const xDataTasksDone = [];
- const yDataTasksDone = [];
-
- for (const date in data) {
- xDataTasksDone.push(date);
- const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0;
- yDataTasksDone.push(done);
- }
-
- return { xDataTasksDone, yDataTasksDone };
- };
-
- const { xDataTasksDone, yDataTasksDone } = transformTasksDoneDataForUser(filteredhistoricaData, selectedUser);
-
- const transformTasksStandardDataForUser = (data, username) => {
- const xDataTasksStandard = [];
- const yDataTasksStandard = [];
-
- for (const date in data) {
- xDataTasksStandard.push(date);
- const todo = data[date]?.project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0;
- const inProgress = data[date]?.project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0;
- const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0;
- yDataTasksStandard.push(todo + inProgress + done);
- }
-
- return { xDataTasksStandard, yDataTasksStandard };
- };
-
- const { xDataTasksStandard, yDataTasksStandard } = transformTasksStandardDataForUser(filteredhistoricaData, selectedUser);
-
- return (
-
-
Individual overview
-
-
-
-
-
setSelectedUser(e.target.value)}>
- {users.map(user => (
- {truncateName(user)}
- ))}
-
-
-
Commits: {commits}
-
Additions: {modifiedLines.additions}
-
Deletions: {modifiedLines.deletions}
-
Modifications: {modifiedLines.modified}
-
Commit Streak: {streak}
-
Longest Streak: {longestStreak}
- {features.includes("issues") && (
-
Issues Assigned: {issuesAssigned}
)}
- {features.includes("issues") && (
-
Issues Closed: {issuesClosed}
)}
- {features.includes("pull-requests") && (
-
Pull requests Created: {pullRequestsCreated}
)}
- {features.includes("pull-requests") && (
-
Pull requests merged: {pullRequestsMerged}
)}
- {features.includes("projects") && (
-
Tasks assigned: {tasksAssigned}
)}
- {features.includes("projects") && (
-
Tasks todo: {tasksTodo}
)}
- {features.includes("projects") && (
-
Tasks in progress: {tasksInProgress}
)}
- {features.includes("projects") && (
-
Tasks done: {tasksClosed}
)}
- {features.includes("projects") && (
-
Tasks with Standard Status: {tasksStandard}
)}
-
-
-
-
-
-
- setShowHistorical(false)}
- className={!showHistorical ? 'selected' : ''}
- >
- Current
-
- setShowHistorical(true)}
- className={showHistorical ? 'selected' : ''}
- >
- Historical
-
-
-
-
- {showHistorical && (
-
- setDateRange(e.target.value)}
- value={dateRange}
- style={{ marginLeft: '1rem' }}
- >
- Last 7 days
- Last 15 days
- Last 30 days
- Last 3 months
- Lifetime
-
-
- )}
-
- {!showHistorical && (
- <>
-
-
-
- Commits
-
- ⓘ
- Percentage of commits made by the user relative to the total number of commits
-
-
-
-
-
-
- Modified Lines
-
- ⓘ
- Percentage of commits made by the user relative to the total number of commits
-
-
-
-
- {features.includes("issues") && (
-
-
- Issues assigned
-
- ⓘ
- Percentage of issues assigned to the user relative to the number of assigned issues
-
-
-
- )}
- {features.includes("issues") && (
-
-
- Issues closed
-
- ⓘ
- Percentage of issues closed by the user relative to the issues assigned to the user
-
-
-
- )}
- {features.includes("pull-requests") && (
-
-
- Pull Requests created
-
- ⓘ
- Percentage of pull requests created by the user relative to the total number of pull requests
-
-
-
- )}
- {features.includes("pull-requests") && (
-
-
- Pull Requests merged
-
- ⓘ
- Percentage of pull requests merged by the user relative to the total number of pull requests merged
-
-
-
- )}
- {features.includes("projects") && (
-
-
- Tasks assigned
-
- ⓘ
- Percentage of tasks assigned to the user relative to the number of assigned tasks
-
-
-
- )}
- {features.includes("projects") && (
-
-
- Tasks In Progress
-
- ⓘ
-
- Shows if the user is actively working on at least one task.
- If the gauge is at 100%, the user has at least one task in progress.
- If it's at 0%, the user currently has no tasks in progress.
-
-
-
-
- )}
- {features.includes("projects") && (
-
-
- Tasks Done
-
- ⓘ
- Percentage of tasks done by the user relative to the tasks assigned to the user
-
-
-
- )}
- {features.includes("projects") && (
-
-
- Tasks with Standard Status
-
- ⓘ
- Percentage of tasks with standard status (ToDo, In Progress, Done) of the user relative to the user assigned tasks
-
-
-
- )}
-
- >)}
- {showHistorical && (
- <>
- {historicData ? (
- <>
-
-
-
-
-
-
-
- {features.includes("issues") && (
-
-
-
-
)}
- {features.includes("issues") && (
-
-
-
)}
- {features.includes("pull-requests") && (
-
-
-
)}
- {features.includes("pull-requests") && (
-
-
-
)}
- {features.includes("projects") && (
-
-
-
- )}
- {features.includes("projects") && (
-
-
-
- )}
- {features.includes("projects") && (
-
-
-
- )}
- {features.includes("projects") && (
-
-
-
- )}
-
- {features.includes("projects") && (
-
-
-
- )}
-
- >) : (
-
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics.
-
)}
- >
- )}
-
- );
-}
-
-export default Individual;
diff --git a/src/pages/individualPage.jsx b/src/pages/individualPage.jsx
new file mode 100644
index 0000000..d9c75ea
--- /dev/null
+++ b/src/pages/individualPage.jsx
@@ -0,0 +1,202 @@
+import React, { useState } from 'react';
+import "../styles/individual.css";
+import LineChart from '../components/lineChart.jsx';
+import GaugeChart from '../components/gaugeChart.jsx';
+import usePersistentStateSession from '../components/usePersistentStateSession.jsx';
+import usePersistentState from '../components/usePersistentState.jsx';
+import HistoricalToggle from '../components/historicalToggle.jsx';
+import DateRangeSelector from '../components/dateRangeSelector.jsx';
+import IndividualStatsSection from '../summarySections/individualPageStatisticsSection.jsx';
+import IndividualMetricsSection from '../metricsSections/individualPageMetricsSection.jsx';
+import IndividualHistoricSection from '../historicalSections/individualPageHistoricalSection.jsx';
+
+import {
+ transformCommitsDataForUser,
+ getGaugeChartDataCommits,
+ getGaugeChartDataModifiedLines,
+ transformModifiedLinesDataForUser
+} from '../domain/commits.jsx'
+import {
+ getGaugeDataAssignedIssuesPerUser,
+ getGaugeDataClosedIssuesPerUser,
+ transformAssignedIssuesDataForUser,
+ transformClosedIssuesDataForUser
+} from '../domain/issues.jsx'
+import {
+ getGaugeDataCreatedPRsPerUser,
+ getGaugeDataMergedPRsPerUser,
+ transformCreatedPRsDataForUser,
+ transformMergedPRsDataForUser
+} from '../domain/pullRequests.jsx'
+import {
+ getGaugeDataAssignedTasksPerUser,
+ getGaugeDataInProgressTasksPerUser,
+ getGaugeDataDoneTasksPerUser,
+ getGaugeDataStandardStatusTasksPerUser,
+ transformTasksAssignedDataForUser,
+ transformTasksToDoDataForUser,
+ transformTasksInProgressDataForUser,
+ transformTasksDoneDataForUser,
+ transformTasksStandardDataForUser,
+} from '../domain/projects.jsx'
+import {
+ filterHistoricData, }
+from '../domain/utils.jsx';
+
+function IndividualPage({ data, historicData, features }) {
+ const [selectedUser, setSelectedUser] = usePersistentState("selectedUser",Object.keys(data.avatars)[0]);
+ const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIndividual', false);
+ const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIndividual', "7");
+
+ //Statistics
+ const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
+ const users = Object.keys(data.avatars);
+ const avatar = data.avatars[selectedUser] || "";
+ const commits = data.commits[selectedUser] || 0;
+ const modifiedLines = data.modified_lines[selectedUser] || { additions: 0, deletions: 0, modified: 0 };
+ const streak = data.commit_streak[selectedUser] || 0;
+ const longestStreak = data.longest_commit_streak_per_user[selectedUser] || 0;
+ const issuesAssigned = data.issues.assigned[selectedUser] || 0;
+ const issuesClosed = data.issues.closed[selectedUser] || 0;
+ const pullRequestsCreated = data.pull_requests.created[selectedUser]
+ const pullRequestsMerged = data.pull_requests.merged_per_member[selectedUser]
+ const totalPeople = Object.keys(data.avatars).length;
+ const tasksAssigned = data.project.metrics_by_iteration.total.assigned_per_member[selectedUser] || 0
+ const tasksClosed = data.project.metrics_by_iteration.total.done_per_member[selectedUser] || 0
+ const tasksInProgress = data.project.metrics_by_iteration.total.in_progress_per_member[selectedUser] || 0
+ const tasksTodo = data.project.metrics_by_iteration.total.todo_per_member[selectedUser] || 0
+ const tasksStandard = tasksClosed + tasksInProgress + tasksTodo
+
+ //Gauges
+ const usersCommits = getGaugeChartDataCommits(data)
+ const percentageCommits = usersCommits.find(userObj => userObj.user === selectedUser).percentage;
+ const usersModifiedLines = getGaugeChartDataModifiedLines(data)
+ const percentageModifiedlLines = usersModifiedLines.find(userObj => userObj.user === selectedUser).percentage;
+
+ const usersAssignedIssues = getGaugeDataAssignedIssuesPerUser(data)
+ const percentageAssigned = usersAssignedIssues.find(userObj => userObj.user === selectedUser).percentage;
+ const usersClosedIssues = getGaugeDataClosedIssuesPerUser(data)
+ const percentageIssuesClosed = usersClosedIssues.find(userObj => userObj.user === selectedUser).percentage;
+
+ const usersCreatedPRs = getGaugeDataCreatedPRsPerUser(data)
+ const percentageCreated = usersCreatedPRs.find(userObj => userObj.user === selectedUser).percentage;
+ const usersMergedPRs = getGaugeDataMergedPRsPerUser(data)
+ const percentageMerged = usersMergedPRs.find(userObj => userObj.user === selectedUser).percentage;
+
+ const usersTasksAssigned = getGaugeDataAssignedTasksPerUser(data,"total");
+ const percentageTasksAssigned = usersTasksAssigned.find(userObj => userObj.user === selectedUser).percentage;
+ const usersTasksInProgress = getGaugeDataInProgressTasksPerUser(data,"total");
+ const percentageTasksInProgress = usersTasksInProgress.find(userObj => userObj.user === selectedUser).percentage;
+ const usersTasksDone = getGaugeDataDoneTasksPerUser(data,"total");
+ const percentageTasksDone = usersTasksDone.find(userObj => userObj.user === selectedUser).percentage;
+ const usersTasksStandard = getGaugeDataStandardStatusTasksPerUser(data,"total");
+ const percentageTasksStandard = usersTasksStandard.find(userObj => userObj.user === selectedUser).percentage;
+
+ //Historic
+ const { xDataCommits, yDataCommits } = transformCommitsDataForUser(filteredhistoricaData, selectedUser)
+
+ const { xDataModifiedLines, yDataModifiedLines } = transformModifiedLinesDataForUser(filteredhistoricaData, selectedUser)
+
+ const { xDataAssignedIssues, yDataAssignedIssues } = transformAssignedIssuesDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataClosedIssues, yDataClosedIssues } = transformClosedIssuesDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataCreatedPRs, yDataCreatedPRs } = transformCreatedPRsDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataMergedPRs, yDataMergedPRs } = transformMergedPRsDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataTasksAssigned, yDataTasksAssigned } = transformTasksAssignedDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataTasksToDo, yDataTasksToDo } = transformTasksToDoDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataTasksInProgress, yDataTasksInProgress } = transformTasksInProgressDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataTasksDone, yDataTasksDone } = transformTasksDoneDataForUser(filteredhistoricaData, selectedUser);
+
+ const { xDataTasksStandard, yDataTasksStandard } = transformTasksStandardDataForUser(filteredhistoricaData, selectedUser);
+
+ return (
+
+
Individual overview
+
+
+
+
+
+
+ {!showHistorical && (
+
+ )}
+ {showHistorical && (
+
+ )}
+
+ );
+}
+
+export default IndividualPage;
diff --git a/src/pages/issues.jsx b/src/pages/issues.jsx
deleted file mode 100644
index 10cd447..0000000
--- a/src/pages/issues.jsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import React, { useState } from 'react';
-import GaugeChart from '../components/gaugeChart';
-import RadarPieToggle from '../components/radarPieToggle';
-import LineChartMultiple from '../components/lineChartMultiple';
-import usePersistentStateSession from '../components/usePersistentStateSession';
-
-import '../styles/commits.css';
-
-function Issues({ data,historicData,features }) {
- const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIssues', false);
- const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIssues', "7");
- const filterHistoricData = (data, days) => {
- if (days === "lifetime") return data;
-
- const today = new Date();
- const cutoff = new Date(today);
- cutoff.setDate(today.getDate() - parseInt(days));
- const cutoffDateString = cutoff.toISOString().split("T")[0];
- const filtered = {};
- for (const date in data) {
- if (date >= cutoffDateString) {
- filtered[date] = data[date];
- }
- }
-
- return filtered;
- };
- const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
-
- const issuesData = data.issues;
- const totalIssues = issuesData.total;
- const totalClosed = issuesData.total_closed
- const havePullRequest = issuesData.have_pull_request
- const { non_assigned, ...filteredData } = issuesData.assigned;
- const totalPeople = Object.keys(data.avatars).length;
- const totalAssigned = totalIssues - issuesData.assigned['non_assigned']
- const closedBy = issuesData.closed
-
-
- const transformAssignedPRsDataForLineChart = (data) => {
- const xDataAssigned = [];
- const userSeries = {};
-
- for (const date in data) {
- xDataAssigned.push(date);
- const issues = data[date].issues.assigned;
-
- for (const user in issues) {
- if(user === 'non_assigned') continue;
- if (!userSeries[user]) {
- userSeries[user] = [];
- }
- userSeries[user].push(issues[user]);
- }
- }
- const seriesDataAssigned = Object.keys(userSeries).map(user => ({
- name: user,
- data: userSeries[user]
- }));
-
- return { xDataAssigned, seriesDataAssigned};
- };
-
- const { xDataAssigned, seriesDataAssigned } = transformAssignedPRsDataForLineChart(filteredhistoricaData);
-
-
- return (
-
-
Issues
-
-
- setShowHistorical(false)}
- className={!showHistorical ? 'selected' : ''}
- >
- Current
-
- setShowHistorical(true)}
- className={showHistorical ? 'selected' : ''}
- >
- Historical
-
-
-
-
- {showHistorical && (
-
- setDateRange(e.target.value)}
- value={dateRange}
- style={{ marginLeft: '1rem' }}
- >
- Last 7 days
- Last 15 days
- Last 30 days
- Last 3 months
- Lifetime
-
-
- )}
-
- {!showHistorical && (
- <>
-
-
Summary
-
-
-
-
-
-
- Issues assigned
-
- ⓘ
- Percentage of Issues that are assigned to a user relative to the total number of Issues
-
-
- 0 ? totalAssigned / totalIssues : 0}
- totalPeople= {1}
- />
-
- {features.includes("pull-requests") && (
-
-
- Issues closed with Pull Request
-
- ⓘ
-
- Percentage of closed Issues that have a Pull Request associated
-
-
-
- 0 ? havePullRequest / totalClosed : 0}
- totalPeople= {1}
- />
- )}
-
-
-
-
Metrics by User
-
- Issues assigned per user
-
- ⓘ
- Percentage of issues assigned per user relative to the number of assigned issues
-
-
-
- {Object.keys(issuesData.assigned).map((user) => {
- if (user !== 'non_assigned') {
- const userIssues = issuesData.assigned[user];
- const percentage = totalAssigned > 0 ? userIssues / totalAssigned : 0;
- return (
-
- );
- }
- return null;
- })}
-
-
- Issues closed per user
-
- ⓘ
-
- Percentage of issues closed per user relative to the issues assigned to that user
-
-
-
-
- {Object.keys(closedBy).map((user) => {
- const closedCount = closedBy[user];
- const assigned = issuesData.assigned[user];
- const percentage = assigned > 0 ? closedCount / assigned : 0;
- return (
-
- );
- })}
-
-
- >)}
-
-{showHistorical && (
- <>
- {historicData ? (
- <>
-
-
- >) : (
-
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics.
-
)}
- >
- )}
-
-
- );
-}
-
-export default Issues;
diff --git a/src/pages/issuesPage.jsx b/src/pages/issuesPage.jsx
new file mode 100644
index 0000000..8325df5
--- /dev/null
+++ b/src/pages/issuesPage.jsx
@@ -0,0 +1,79 @@
+// pages/IssuesPage.jsx
+import React from 'react';
+import usePersistentStateSession from '../components/usePersistentStateSession';
+import HistoricalToggle from '../components/historicalToggle.jsx';
+import DateRangeSelector from '../components/dateRangeSelector.jsx';
+import {
+ transformAssignedIssuesDataForLineChart,
+ getRadarAndPieDataIssuesAssigned,
+ getGaugeDataIssuesAssigned,
+ getGaugeDataAssignedIssuesPerUser,
+ getGaugeDataClosedIssuesPerUser,
+ getGaugeDataIssuesHavePR
+} from '../domain/issues';
+import { filterHistoricData } from '../domain/utils.jsx';
+
+import IssuesSummarySection from '../summarySections/issuesPageSummarySection.jsx';
+import IssuesMetricsByUserSection from '../metricsSections/issuesPageMetricsSection.jsx';
+import IssuesHistoricalSection from '../historicalSections/issuesPageHistoricalSection.jsx';
+
+import '../styles/elements.css';
+
+function IssuesPage({ data, historicData, features }) {
+ const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIssues', false);
+ const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIssues', "7");
+
+ const filteredHistoricalData = historicData ? filterHistoricData(historicData, dateRange) : null;
+ const totalPeople = Object.keys(data.avatars).length;
+
+ const { xDataAssigned, seriesDataAssigned } = transformAssignedIssuesDataForLineChart(filteredHistoricalData);
+ const percentageAssigned = getGaugeDataIssuesAssigned(data);
+ const gaugeDataHavePR = getGaugeDataIssuesHavePR(data);
+ const gaugeDataAssigned = getGaugeDataAssignedIssuesPerUser(data);
+ const gaugeDataClosed = getGaugeDataClosedIssuesPerUser(data);
+ const { radarData, pieData } = getRadarAndPieDataIssuesAssigned(data);
+
+ return (
+
+
Issues
+
+
+
+
+
+ {!showHistorical && (
+ <>
+
+
+ >
+ )}
+
+ {showHistorical && (
+
+ )}
+
+ );
+}
+
+export default IssuesPage;
diff --git a/src/pages/projects.jsx b/src/pages/projects.jsx
deleted file mode 100644
index b1c65f8..0000000
--- a/src/pages/projects.jsx
+++ /dev/null
@@ -1,649 +0,0 @@
-import React, { useState } from 'react';
-import GaugeChart from '../components/gaugeChart';
-import RadarPieToggle from '../components/radarPieToggle';
-import PieChart from '../components/pieChart';
-import LineChartMultiple from '../components/lineChartMultiple';
-import AreaChartMultiple from '../components/areaChartMultiple';
-import usePersistentStateSession from '../components/usePersistentStateSession';
-
-import '../styles/commits.css';
-
-function Projects({ data,historicData,features }) {
- const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalProject', false);
- const [dateRange, setDateRange] = usePersistentStateSession('dateRangeProject', "7");
- const today = new Date();
-
-const getActiveIteration = () => {
- const has_iterations = data.project.has_iterations;
- if (!has_iterations) return "total";
- const iterations = data.project.iterations
- for (const key in iterations) {
- const { startDate, endDate } = iterations[key];
- const start = new Date(startDate);
- const end = new Date(endDate);
-
- if (today >= start && today <= end) {
- return key;
- }
- }
- return "total";
- };
-
- const [selectedIteration, setSelectedIteration] = usePersistentStateSession("activeIteration",getActiveIteration());
- const filterHistoricData = (data, days) => {
- if (days === "lifetime") return data;
-
- const today = new Date();
- const cutoff = new Date(today);
- cutoff.setDate(today.getDate() - parseInt(days));
- const cutoffDateString = cutoff.toISOString().split("T")[0];
- const filtered = {};
- for (const date in data) {
- if (date >= cutoffDateString) {
- filtered[date] = data[date];
- }
- }
-
- return filtered;
- };
- const filterHistoricDataByIteration = (dataHist, iterationName) => {
- if (!data) return null;
- if (iterationName === "total") return dataHist;
-
- const iteration = data.project.iterations[iterationName] || null;
- if (!iteration) return dataHist;
-
- const startDate = new Date(iteration.startDate);
- const endDate = new Date(iteration.endDate);
-
- const filtered = {};
- for (const dateStr in dataHist) {
- const date = new Date(dateStr);
- if (date >= startDate && date <= endDate) {
- filtered[dateStr] = dataHist[dateStr];
- }
- }
- return filtered;
- };
-
- const iterationsOrdered = Object.keys(data.project.iterations);
- const iterations = Object.keys(data.project.metrics_by_iteration);
-
- const sortedIterationNames = [
- ...iterationsOrdered,
- ...iterations.filter(name => name === "total"),
- ];
-
- const filteredhistoricaData = historicData
- ? (selectedIteration === "total"
- ? filterHistoricData(historicData, dateRange)
- : filterHistoricDataByIteration(historicData, selectedIteration))
- : null;
- const taskData = data.project.metrics_by_iteration[selectedIteration];
- const totalTasks = taskData.total_tasks || 0
- const totalInProgress = taskData.in_progress || 0
- const totalDone = taskData.done || 0
- const totalToDo = taskData.todo || 0
-
- const totalStandardStatuses = totalInProgress + totalDone + totalToDo
-
- const { non_assigned, ...assignedPerMember } = taskData.assigned_per_member;
- const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0);
- const inProgresPerMember = taskData.in_progress_per_member || 0;
- const donePerMember = taskData.done_per_member || 0;
- const todoPerMember = taskData.todo_per_member || 0;
- const totalPeople = Object.keys(data.avatars).length;
-
- const totalIssues = taskData.total_issues
- const totalIssuesWithType = taskData.total_issues_with_type
- const totalItems = taskData.total
-
- const taskDataNoIteration = data.project.metrics_by_iteration['no_iteration']
- const taskDataTotal = data.project.metrics_by_iteration['total']
- const itemsTotal = taskDataTotal.total
- const itemsNoIteration = taskDataNoIteration.total
- const transformAssignedDataForLineChart = (dataHistoric, selectedIteration, iterations) => {
- let allDates = [];
- if (!dataHistoric) return { xDataAssigned: [], seriesDataAssigned: [] }
-
- if (selectedIteration === "total") {
- allDates = Object.keys(dataHistoric).sort((a,b) => new Date(a) - new Date(b));
- } else {
- const iteration = iterations[selectedIteration];
- if (!iteration) return { xDataAssigned: [], seriesDataAssigned: [] };
-
- const startDate = new Date(iteration.startDate);
- const endDate = new Date(iteration.endDate);
-
- const dates = [];
- let currentDate = new Date(startDate);
- while (currentDate <= endDate) {
- const dateStr = currentDate.toISOString().split('T')[0];
- dates.push(dateStr);
- currentDate.setDate(currentDate.getDate() + 1);
- }
- allDates = dates;
- }
-
- const userSeries = {};
- const allUsers = new Set();
-
- const assignedPerMember = data.project.metrics_by_iteration["total"]?.assigned_per_member || {};
-
- Object.keys(assignedPerMember)
- .filter(u => u !== "non_assigned")
- .forEach(u => allUsers.add(u));
-
- allUsers.forEach(u => {
- userSeries[u] = [];
- });
-
- for (const date of allDates) {
- const dayData = dataHistoric[date];
- const iterationData = dayData?.project?.metrics_by_iteration?.[selectedIteration];
- const assignedPerMember = iterationData?.assigned_per_member || {};
- allUsers.forEach(u => {
- userSeries[u].push(assignedPerMember[u] || null);
- });
- }
-
- const seriesDataAssigned = Array.from(allUsers).map(u => ({
- name: u,
- data: userSeries[u],
- }));
-
- return { xDataAssigned: allDates, seriesDataAssigned };
-};
-
- const { xDataAssigned, seriesDataAssigned } = transformAssignedDataForLineChart(
- filteredhistoricaData,
- selectedIteration,
- data.project.iterations
- );
-
- const transformFeatureDataForAreaChart = (data) => {
- const xDataFeature = [];
- const knownKeys = {
- total_features_done: 'Done',
- total_features_in_progress: 'In Progress',
- total_features_todo: 'To Do',
- };
-
- const baseFeatureData = {
- Done: [],
- 'In Progress': [],
- 'To Do': [],
- };
-
- const otherKeysData = {};
- const allOtherKeys = new Set();
-
- for (const date in data) {
- const metrics = data[date]?.project?.metrics_by_iteration?.total;
- if (!metrics) continue;
-
- for (const [key, value] of Object.entries(metrics)) {
- if (
- !Object.keys(knownKeys).includes(key) &&
- typeof value === 'number' &&
- key.startsWith('total_features_') &&
- Number.isInteger(value)
- ) {
- allOtherKeys.add(key);
- }
- }
- }
-
- for (const key of allOtherKeys) {
- otherKeysData[key] = [];
- }
-
- for (const date in data) {
- const metrics = data[date]?.project?.metrics_by_iteration?.total || {};
-
- xDataFeature.push(date);
-
- for (const [key, label] of Object.entries(knownKeys)) {
- baseFeatureData[label].push(metrics[key] || 0);
- }
-
- for (const key of allOtherKeys) {
- const value = metrics[key];
- otherKeysData[key].push(Number.isInteger(value) ? value : 0);
- }
- }
-
- return {
- xDataFeature,
- doneFeature: baseFeatureData['Done'],
- inProgressFeature: baseFeatureData['In Progress'],
- toDoFeature: baseFeatureData['To Do'],
- otherKeysData,
- };
- };
-
-
- const {
- xDataFeature,
- doneFeature,
- inProgressFeature,
- toDoFeature,
- otherKeysData
- } = transformFeatureDataForAreaChart(filteredhistoricaData);
-
- const baseSeries = [
- { label: 'Done', data: doneFeature, color: 'rgb(0, 255, 0)' },
- { label: 'In Progress', data: inProgressFeature, color: 'orange' },
- { label: 'To Do', data: toDoFeature, color: 'rgb(255, 0, 0)' }
- ];
-
- const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({
- label: key.replace('total_features_', '')
- .replace(/_/g, ' ')
- .replace(/\b\w/g, c => c.toUpperCase()),
- data
- }));
-
- const allSeries = [...baseSeries, ...otherSeries];
-
-
- function formatDate(dateStr) {
- const d = new Date(dateStr);
- const day = String(d.getDate()).padStart(2, '0');
- const month = String(d.getMonth() + 1).padStart(2, '0');
- const year = d.getFullYear();
- return `${day}/${month}/${year}`;
- }
- function getDateRangeForIteration(iterationName) {
- const iteration = data.project.iterations[iterationName];
-
- if (!iteration) return '';
- return `${formatDate(iteration.startDate)} - ${formatDate(iteration.endDate)}`;
- }
-
- function getTypeDataForChart(projectData,selectedIteration) {
-
- const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration];
-
- if (!iterationData) return [];
-
- const {
- total_tasks = 0,
- total_bugs = 0,
- total_features = 0,
- } = iterationData;
-
- return [
- ['Tasks', total_tasks],
- ['Features', total_features],
- ['Bugs', total_bugs],
- ];
- };
-
-function getFeatureDataForChart(projectData, selectedIteration) {
- const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration];
- if (!iterationData) return { baseData: [], otherData: [] };
-
- const baseData = [];
- const otherData = [];
-
- const knownKeys = {
- total_features_todo: 'To Do',
- total_features_in_progress: 'In Progress',
- total_features_done: 'Done',
- };
-
- for (const [key, label] of Object.entries(knownKeys)) {
- const value = iterationData[key] || 0;
- baseData.push({ label, value });
- }
-
- for (const [key, value] of Object.entries(iterationData)) {
- if (
- key.startsWith('total_features_') &&
- !knownKeys.hasOwnProperty(key)
- ) {
- const label = key
- .replace('total_features_', '')
- .replace(/_/g, ' ')
- .replace(/\b\w/g, c => c.toUpperCase());
- otherData.push({ label, value });
- }
- }
-
- return { baseData, otherData };
-}
-
- const typePieChartData = getTypeDataForChart(data, selectedIteration);
- const typeColorsPieChart = ["orange", "#0f58ff","red"];
-
- const { baseData, otherData } = getFeatureDataForChart(data, selectedIteration);
-
- const featurePieChartData = [
- ...baseData.map(item => [item.label, item.value]),
- ...otherData.map(item => [item.label, item.value])
- ]; const featureColorsPieChart = ["green", "orange","red"];
- return (
-
-
-
Projects
-
-
- setShowHistorical(false)}
- className={!showHistorical ? 'selected' : ''}
- >
- Current
-
- setShowHistorical(true)}
- className={showHistorical ? 'selected' : ''}
- >
- Historical
-
-
-
-
- {showHistorical && (
- <>
-
- setSelectedIteration(e.target.value)}
- value={selectedIteration}
- style={{ marginLeft: '1rem' }}
- >
- {sortedIterationNames.map((iterationName) => (
-
- {iterationName}
-
- ))}
-
-
- {selectedIteration === "total" && (
-
- setDateRange(e.target.value)}
- value={dateRange}
- style={{ marginLeft: '1rem' }}
- >
- Last 7 days
- Last 15 days
- Last 30 days
- Last 3 months
- Lifetime
-
-
- )}
- >
- )}
-
-
-
- {!showHistorical && (
- <>
-
-
setSelectedIteration(e.target.value)}
- value={selectedIteration}
- style={{ marginLeft: '1rem' }}
- >
- {sortedIterationNames.map((iterationName) => (
-
- {iterationName}
-
- ))}
-
-
- {getDateRangeForIteration(selectedIteration)}
-
-
-
-
Summary
-
-
-
-
-
-
-
-
- Tasks assigned
-
- ⓘ
- Percentage of tasks that are assigned to a user relative to the total number of Tasks
-
-
- 0 ? totalAssigned / totalTasks : 0}
- totalPeople= {1}
- />
-
-
-
- Tasks with Standard Status
-
- ⓘ
- Percentage of tasks that have a standard status (ToDo, In Progress, Done) to the total of tasks
-
-
- 0 ? totalStandardStatuses / totalTasks : 0}
- totalPeople= {1}
- />
-
-
-
- Issues
-
- ⓘ
- Percentage of Items that are Issues
-
-
- 0 ? totalIssues / totalItems : 0}
- totalPeople= {1}
- />
-
-
-
- Issues with type
-
- ⓘ
- Percentage of Issues that have a type assigned
-
-
- 0 ? totalIssuesWithType / totalIssues : 0}
- totalPeople= {1}
- />
-
- {data.project.has_iterations && (
-
-
- Items with iteration
-
- ⓘ
- Percentage of Items that are assigned to an iteration
-
-
- 0 ? (itemsTotal - itemsNoIteration) / itemsTotal : 0}
- totalPeople= {1}
- />
- )}
-
-
-
-
Metrics by User
-
- Tasks assigned per user
-
- ⓘ
- Percentage of tasks assigned per user relative to the number of assigned tasks
-
-
-
- {Object.keys(assignedPerMember).map((user) => {
- if (user !== 'non_assigned') {
- const userTasks = assignedPerMember[user];
- const percentage = totalAssigned > 0 ? userTasks / totalAssigned : 0;
- return (
-
- );
- }
- return null;
- })}
-
-
- Tasks In Progress per user
-
- ⓘ
-
- Shows if the user is actively working on at least one task.
- If the gauge is at 100%, the user has at least one task in progress.
- If it's at 0%, the user currently has no tasks in progress.
-
-
-
-
- {Object.keys(inProgresPerMember).map((user) => {
- const inProgress = inProgresPerMember[user]
- const percentage = inProgress > 0 ? 1 : 0;
- return (
-
- );
- })}
-
-
- Tasks Done per user
-
- ⓘ
-
- Percentage of tasks done per user relative to the tasks assigned to that user
-
-
-
-
- {Object.keys(donePerMember).map((user) => {
- const doneCount = donePerMember[user];
- const assigned = assignedPerMember[user];
- const percentage = assigned > 0 ? doneCount / assigned : 0;
- return (
-
- );
- })}
-
-
-
- Tasks with Standard Status per user
-
- ⓘ
-
- Percentage of tasks with standard status (ToDo, In Progress, Done) per user relative to their assigned tasks
-
-
-
-
- {Object.keys(assignedPerMember).map((user) => {
- const assigned = assignedPerMember[user] || 0;
- const inProgress = inProgresPerMember[user] || 0;
- const doneCount = donePerMember[user] || 0;
- const todoCount = todoPerMember[user] || 0;
- const standardTotal = inProgress + doneCount + todoCount;
- const percentage = assigned > 0 ? standardTotal / assigned : 0;
-
- return (
-
- );
- })}
-
-
- >)}
-
- {showHistorical && (
- <>
- {historicData ? (
- <>
-
- >) : (
-
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics.
-
)}
- >
- )}
-
- );
-}
-
-export default Projects;
diff --git a/src/pages/projectsPage.jsx b/src/pages/projectsPage.jsx
new file mode 100644
index 0000000..b46ef98
--- /dev/null
+++ b/src/pages/projectsPage.jsx
@@ -0,0 +1,148 @@
+import React, { useState } from 'react';
+import usePersistentStateSession from '../components/usePersistentStateSession';
+import HistoricalToggle from '../components/historicalToggle.jsx';
+import DateRangeSelector from '../components/dateRangeSelector.jsx';
+
+import ProjectsPageSummarySection from '../summarySections/projectsPageSummarySection.jsx';
+import ProjectsPageMetricsSection from '../metricsSections/projectsPageMetricsSection.jsx';
+import ProjectsPageHistoricalSection from '../historicalSections/projectsPageHistoricalSection.jsx';
+
+import {
+ getActiveIteration,
+ filterHistoricDataByIteration,
+ getRadarPieDataAssignedTasks,
+ transformAssignedTasksDataForLineChart,
+ transformFeatureDataForAreaChart,
+ getDateRangeForIteration,
+ getIssueTypeDataForChart,
+ getFeatureDataForChart,
+ getGaugeDataTasksAssigned,
+ getGaugeDataTasksStandardStatus,
+ getGaugeDataItemsIssues,
+ getGaugeDataItemIssuesWithType,
+ getGaugeDataItemIssuesWithIteration,
+ getGaugeDataAssignedTasksPerUser,
+ getGaugeDataInProgressTasksPerUser,
+ getGaugeDataDoneTasksPerUser,
+ getGaugeDataStandardStatusTasksPerUser
+} from '../domain/projects';
+import { filterHistoricData } from '../domain/utils';
+
+function ProjectsPage({ data, historicData, features }) {
+ const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalProject', false);
+ const [dateRange, setDateRange] = usePersistentStateSession('dateRangeProject', "7");
+
+ const [selectedIteration, setSelectedIteration] = usePersistentStateSession("activeIteration", getActiveIteration(data));
+
+ const iterationsOrdered = Object.keys(data.project.iterations);
+ const iterations = Object.keys(data.project.metrics_by_iteration);
+
+ const sortedIterationNames = [
+ ...iterationsOrdered,
+ ...iterations.filter(name => name === "total"),
+ ];
+ const totalPeople = Object.keys(data.avatars).length;
+
+ const filteredhistoricaData = historicData
+ ? (selectedIteration === "total"
+ ? filterHistoricData(historicData, dateRange)
+ : filterHistoricDataByIteration(data, historicData, selectedIteration))
+ : null;
+
+ const { radarDataAssigned, pieDataAssigned } = getRadarPieDataAssignedTasks(data, selectedIteration);
+
+ const { xDataAssigned, seriesDataAssigned } = transformAssignedTasksDataForLineChart(
+ data,
+ filteredhistoricaData,
+ selectedIteration,
+ data.project.iterations
+ );
+
+ const { xDataFeature, allSeries } = transformFeatureDataForAreaChart(filteredhistoricaData, selectedIteration, data.project.iterations);
+
+ const { typePieChartData, typeColorsPieChart } = getIssueTypeDataForChart(data, selectedIteration);
+
+ const { featurePieChartData, featureColorsPieChart } = getFeatureDataForChart(data, selectedIteration);
+
+ const percentageTasksAssigned = getGaugeDataTasksAssigned(data, selectedIteration);
+ const percentageStandardStatus = getGaugeDataTasksStandardStatus(data, selectedIteration);
+ const percentageItemsIssues = getGaugeDataItemsIssues(data, selectedIteration);
+ const percentageItemIssuesWithType = getGaugeDataItemIssuesWithType(data, selectedIteration);
+ const percentageIteration = getGaugeDataItemIssuesWithIteration(data);
+ const gaugeDataAssignedTasks = getGaugeDataAssignedTasksPerUser(data, selectedIteration);
+ const gaugeDataInProgressTasks = getGaugeDataInProgressTasksPerUser(data, selectedIteration);
+ const gaugeDataDoneTasks = getGaugeDataDoneTasksPerUser(data, selectedIteration);
+ const gaugeDataStandardTasks = getGaugeDataStandardStatusTasksPerUser(data, selectedIteration);
+
+ return (
+
+
Projects
+
+
+
setSelectedIteration(e.target.value)}
+ value={selectedIteration}
+ style={{ marginLeft: '1rem' }}
+ >
+ {sortedIterationNames.map((iterationName) => (
+
+ {iterationName}
+
+ ))}
+
+ {!showHistorical && (
+
+ {getDateRangeForIteration(data, selectedIteration)}
+
+ )}
+
+ {selectedIteration === "total" && (
+
+ )}
+
+ {!showHistorical ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+ );
+}
+
+export default ProjectsPage;
diff --git a/src/pages/pullRequests.jsx b/src/pages/pullRequests.jsx
deleted file mode 100644
index 8204766..0000000
--- a/src/pages/pullRequests.jsx
+++ /dev/null
@@ -1,287 +0,0 @@
-import React, { useState } from 'react';
-import GaugeChart from '../components/gaugeChart';
-import '../styles/commits.css';
-import RadarPieToggle from '../components/radarPieToggle';
-import LineChartMultiple from '../components/lineChartMultiple';
-import usePersistentStateSession from '../components/usePersistentStateSession';
-
-function PullRequests({ data,historicData,features }) {
- const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalPR', false);
- const [dateRange, setDateRange] = usePersistentStateSession('dateRangePR', "7");
- const filterHistoricData = (data, days) => {
- if (days === "lifetime") return data;
-
- const today = new Date();
- const cutoff = new Date(today);
- cutoff.setDate(today.getDate() - parseInt(days));
- const cutoffDateString = cutoff.toISOString().split("T")[0];
- const filtered = {};
- for (const date in data) {
- if (date >= cutoffDateString) {
- filtered[date] = data[date];
- }
- }
-
- return filtered;
- };
- const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
-
- const pullRequests = data.pull_requests
- const createdby = pullRequests.created
- const mergedby = pullRequests.merged_per_member
- const totalMerged = pullRequests.merged
- const totalMergedNotByAuthor = pullRequests.not_merged_by_author
- const totalClosed = pullRequests.closed
- const total = pullRequests.total
- const totalPeople = Object.keys(data.avatars).length;
- const merges = data.commit_merges
-
- const transformCreatedPRsDataForLineChart = (data) => {
- const xDataCreated = [];
- const userSeries = {};
-
- for (const date in data) {
- xDataCreated.push(date);
- const created = data[date].pull_requests.created;
-
- for (const user in created) {
- if (!userSeries[user]) {
- userSeries[user] = [];
- }
- userSeries[user].push(created[user]);
- }
- }
- const seriesDataCreated = Object.keys(userSeries).map(user => ({
- name: user,
- data: userSeries[user]
- }));
-
- return { xDataCreated, seriesDataCreated };
- };
-
- const { xDataCreated, seriesDataCreated } = transformCreatedPRsDataForLineChart(filteredhistoricaData);
-
- const transformMergedPRsDataForLineChart = (data) => {
- const xDataMerged = [];
- const userSeries = {};
-
- for (const date in data) {
- xDataMerged.push(date);
- const merged = data[date].pull_requests.merged_per_member;
-
- for (const user in merged) {
- if (!userSeries[user]) {
- userSeries[user] = [];
- }
- userSeries[user].push(merged[user]);
- }
- }
- const seriesDataMerged = Object.keys(userSeries).map(user => ({
- name: user,
- data: userSeries[user]
- }));
-
- return { xDataMerged, seriesDataMerged };
- };
-
- const { xDataMerged, seriesDataMerged } = transformMergedPRsDataForLineChart(filteredhistoricaData);
-
- return (
-
-
Pull requests
-
-
- setShowHistorical(false)}
- className={!showHistorical ? 'selected' : ''}
- >
- Current
-
- setShowHistorical(true)}
- className={showHistorical ? 'selected' : ''}
- >
- Historical
-
-
-
-
- {showHistorical && (
-
- setDateRange(e.target.value)}
- value={dateRange}
- style={{ marginLeft: '1rem' }}
- >
- Last 7 days
- Last 15 days
- Last 30 days
- Last 3 months
- Lifetime
-
-
- )}
-
- {!showHistorical && (
- <>
-
-
Summary
-
-
-
-
- Pull requests merged
-
- ⓘ
- Percentage of pull requests that are merged relative to the total number of pull requests that are not closed
-
-
- 0 ? totalMerged / (total - totalClosed) : 0}
- totalPeople= {1}
- />
-
-
-
- Pull requests reviewed
-
- ⓘ
-
- Percentage of pull requests merged by another user that is not the author relative to the total number of merged pull requests
-
-
-
- 0 ? totalMergedNotByAuthor / totalMerged : 0}
- totalPeople={1}
- />
-
-
-
- Merges from pull requests
-
- ⓘ
-
- Percentage of merges that are from pull requests
-
-
-
- 0 ? totalMerged / merges : 0}
- totalPeople={1}
- />
-
-
-
-
-
Metrics by User
-
- Pull requests created per user
-
- ⓘ
- Percentage of pull requests created per user relative to the total number of pull requets
-
-
-
- {Object.keys(createdby).map((user) => {
- const userPRs = createdby[user];
- const percentage = total > 0 ? userPRs / total : 0;
- return (
-
- );
- })}
-
-
- Pull requests merged per user
-
- ⓘ
- Percentage of pull requests merged per user relative to the number of merged pull requets
-
-
-
- {Object.keys(mergedby).map((user) => {
- const userMergedPRs = mergedby[user];
- const percentage = totalMerged > 0 ? userMergedPRs / totalMerged : 0;
- return (
-
- );
- })}
-
-
- >
- )}
- {showHistorical && (
- <>
- {historicData ? (
- <>
-
-
- >) : (
-
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics.
-
)}
- >
- )}
-
- );
-}
-
-export default PullRequests;
diff --git a/src/pages/pullRequestsPage.jsx b/src/pages/pullRequestsPage.jsx
new file mode 100644
index 0000000..b7e90ae
--- /dev/null
+++ b/src/pages/pullRequestsPage.jsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import usePersistentStateSession from '../components/usePersistentStateSession';
+import {
+ transformCreatedPRsDataForLineChart,
+ transformMergedPRsDataForLineChart,
+ getRadarAndPieDataForCreatedPRs,
+ getRadarAndPieDataForMergedPRs,
+ getGaugeChartDataMergedPRs,
+ getGaugeChartDataReviewedPRs,
+ getGaugeChartDataMergesPRs,
+ getGaugeDataCreatedPRsPerUser,
+ getGaugeDataMergedPRsPerUser
+} from '../domain/pullRequests';
+import HistoricalToggle from '../components/historicalToggle';
+import DateRangeSelector from '../components/dateRangeSelector.jsx';
+import { filterHistoricData } from '../domain/utils';
+
+import PullRequestsPageSummarySection from '../summarySections/pullRequestsPageSummarySection.jsx';
+import PullRequestsPageMetricsSection from '../metricsSections/pullRequestsPageMetricsSection.jsx';
+import PullRequestsPageHistoricalSection from '../historicalSections/pullRequestsPageHistoricalSection.jsx';
+
+import '../styles/elements.css';
+
+function PullRequestsPage({ data, historicData, features }) {
+ const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalPR', false);
+ const [dateRange, setDateRange] = usePersistentStateSession('dateRangePR', "7");
+
+ const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null;
+
+ const totalPeople = Object.keys(data.avatars).length;
+
+ const { xDataCreated, seriesDataCreated } = transformCreatedPRsDataForLineChart(filteredhistoricaData);
+ const { xDataMerged, seriesDataMerged } = transformMergedPRsDataForLineChart(filteredhistoricaData);
+
+ const { radarDataCreated, pieDataCreated } = getRadarAndPieDataForCreatedPRs(data);
+ const { radarDataMerged, pieDataMerged } = getRadarAndPieDataForMergedPRs(data);
+
+ const percentageMerged = getGaugeChartDataMergedPRs(data);
+ const percentatgeReviewed = getGaugeChartDataReviewedPRs(data);
+ const percentageMergesPR = getGaugeChartDataMergesPRs(data);
+
+ const gaugeDataCreatedPRs = getGaugeDataCreatedPRsPerUser(data);
+ const gaugeDataMergedPRs = getGaugeDataMergedPRsPerUser(data);
+
+ return (
+
+
Pull requests
+
+
+
+
+
+ {showHistorical && (
+
+ )}
+
+ {!showHistorical && (
+ <>
+
+
+ >
+ )}
+
+ {showHistorical && (
+
+ )}
+
+ );
+}
+
+export default PullRequestsPage;
diff --git a/src/styles/commits.css b/src/styles/elements.css
similarity index 100%
rename from src/styles/commits.css
rename to src/styles/elements.css
diff --git a/src/summarySections/commitsPageSummarySection.jsx b/src/summarySections/commitsPageSummarySection.jsx
new file mode 100644
index 0000000..444edbb
--- /dev/null
+++ b/src/summarySections/commitsPageSummarySection.jsx
@@ -0,0 +1,49 @@
+// components/summarySection.jsx
+import React from 'react';
+import RadarPieToggle from '../components/radarPieToggle.jsx';
+import GaugeChart from '../components/gaugeChart.jsx';
+
+function CommitsPageSummarySection({ radarChartCommits, radarChartModifiedLines, dataPieChartCommits, dataPieChartModifiedLines, gaugeDataAnonymous }) {
+ return (
+
+
Summary
+
+
+
+
+
+
+
+
+
+ Non-anonymous commits
+
+ ⓘ
+
+ Percentage of commits that have a member of the project as its author
+
+
+
+
+
+
+
+ );
+}
+
+export default CommitsPageSummarySection;
diff --git a/src/summarySections/indexStatsSection.jsx b/src/summarySections/indexStatsSection.jsx
new file mode 100644
index 0000000..568c132
--- /dev/null
+++ b/src/summarySections/indexStatsSection.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+
+function IndexStatsSection({ data, features }) {
+ return (
+
+
+
+
Project Statistics
+
+
+
Total Members: {Object.keys(data.avatars).length}
+
Total Commits: {data.commits.total}
+
Total Additions: {data.modified_lines.total.additions}
+
Total Deletions: {data.modified_lines.total.deletions}
+
Total Modifications: {data.modified_lines.total.modified}
+
Total Lines of code: {data.modified_lines.total.additions - data.modified_lines.total.deletions}
+ {features.includes("issues") && (
+
Total Issues: {data.issues.total}
+ )}
+ {features.includes("pull-requests") && (
+
Total Pull Requests: {data.pull_requests.total}
+ )}
+ {features.includes("projects") && (
+ <>
+
Total Tasks: {data.project.metrics_by_iteration.total.total_tasks}
+
Total Features: {data.project.metrics_by_iteration.total.total_features}
+ >
+ )}
+
+
+
+ );
+}
+
+export default IndexStatsSection;
diff --git a/src/summarySections/individualPageStatisticsSection.jsx b/src/summarySections/individualPageStatisticsSection.jsx
new file mode 100644
index 0000000..d8525a6
--- /dev/null
+++ b/src/summarySections/individualPageStatisticsSection.jsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { truncateName } from '../domain/utils';
+
+function IndividualStatsSection({
+ selectedUser,
+ setSelectedUser,
+ users,
+ avatar,
+ commits,
+ modifiedLines,
+ streak,
+ longestStreak,
+ issuesAssigned,
+ issuesClosed,
+ pullRequestsCreated,
+ pullRequestsMerged,
+ tasksAssigned,
+ tasksTodo,
+ tasksInProgress,
+ tasksClosed,
+ tasksStandard,
+ features
+}) {
+ return (
+
+
+
+
setSelectedUser(e.target.value)}>
+ {users.map(user => (
+ {truncateName(user)}
+ ))}
+
+
+
+
Commits: {commits}
+
Additions: {modifiedLines.additions}
+
Deletions: {modifiedLines.deletions}
+
Modifications: {modifiedLines.modified}
+
Commit Streak: {streak}
+
Longest Streak: {longestStreak}
+ {features.includes("issues") && (
+
Issues Assigned: {issuesAssigned}
)}
+ {features.includes("issues") && (
+
Issues Closed: {issuesClosed}
)}
+ {features.includes("pull-requests") && (
+
Pull Requests Created: {pullRequestsCreated}
)}
+ {features.includes("pull-requests") && (
+
Pull Requests merged: {pullRequestsMerged}
)}
+ {features.includes("projects") && (
+
Tasks assigned: {tasksAssigned}
)}
+ {features.includes("projects") && (
+
Tasks ToDo: {tasksTodo}
)}
+ {features.includes("projects") && (
+
Tasks In Progress: {tasksInProgress}
)}
+ {features.includes("projects") && (
+
Tasks Done: {tasksClosed}
)}
+ {features.includes("projects") && (
+
Tasks with a standard status: {tasksStandard}
)}
+
+
+
+ );
+}
+
+export default IndividualStatsSection;
diff --git a/src/summarySections/issuesPageSummarySection.jsx b/src/summarySections/issuesPageSummarySection.jsx
new file mode 100644
index 0000000..f5eab44
--- /dev/null
+++ b/src/summarySections/issuesPageSummarySection.jsx
@@ -0,0 +1,62 @@
+// components/issuesSummarySection.jsx
+import React from 'react';
+import GaugeChart from '../components/gaugeChart.jsx';
+import RadarPieToggle from '../components/radarPieToggle.jsx';
+
+function IssuesPageSummarySection({ radarData, pieData, percentageAssigned, gaugeDataHavePR, features }) {
+ return (
+
+
Summary
+
+
+
+
+
+
+
+ Issues assigned
+
+ ⓘ
+
+ Percentage of Issues that are assigned to a user relative to the total number of Issues
+
+
+
+
+
+
+ {features.includes("pull-requests") && (
+
+
+ Issues closed with Pull Request
+
+ ⓘ
+
+ Percentage of closed Issues that have a Pull Request associated
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default IssuesPageSummarySection;
diff --git a/src/summarySections/projectsPageSummarySection.jsx b/src/summarySections/projectsPageSummarySection.jsx
new file mode 100644
index 0000000..1593f4c
--- /dev/null
+++ b/src/summarySections/projectsPageSummarySection.jsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import RadarPieToggle from '../components/radarPieToggle';
+import PieChart from '../components/pieChart';
+import GaugeChart from '../components/gaugeChart';
+
+function ProjectsPageSummarySection({
+ radarDataAssigned,
+ pieDataAssigned,
+ typePieChartData,
+ typeColorsPieChart,
+ featurePieChartData,
+ featureColorsPieChart,
+ percentageTasksAssigned,
+ percentageStandardStatus,
+ percentageItemsIssues,
+ percentageItemIssuesWithType,
+ percentageIteration,
+ data,
+}) {
+ return (
+
+
Summary
+
+
+
+
+
+
+
+
+ Tasks assigned
+
+ ⓘ
+
+ Percentage of tasks that are assigned to a user relative to the total number of Tasks
+
+
+
+
+
+
+
+ Tasks with standard status
+
+ ⓘ
+
+ Percentage of tasks that have a standard status (ToDo, In Progress, Done) to the total of tasks
+
+
+
+
+
+
+
+ Issues
+
+ ⓘ
+ Percentage of Items that are Issues
+
+
+
+
+
+
+ Issues with type
+
+ ⓘ
+ Percentage of Issues that have a type assigned
+
+
+
+
+ {data.project.has_iterations && (
+
+
+ Items with iteration
+
+ ⓘ
+ Percentage of Items that are assigned to an iteration
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default ProjectsPageSummarySection;
\ No newline at end of file
diff --git a/src/summarySections/pullRequestsPageSummarySection.jsx b/src/summarySections/pullRequestsPageSummarySection.jsx
new file mode 100644
index 0000000..94e1e13
--- /dev/null
+++ b/src/summarySections/pullRequestsPageSummarySection.jsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import GaugeChart from '../components/gaugeChart.jsx';
+import RadarPieToggle from '../components/radarPieToggle.jsx';
+
+function PullRequestsPageSummarySection({
+ radarDataCreated,
+ pieDataCreated,
+ radarDataMerged,
+ pieDataMerged,
+ percentageMerged,
+ percentatgeReviewed,
+ percentageMergesPR,
+ features
+}) {
+ return (
+
+
Summary
+
+
+
+
+ Pull requests merged
+
+ ⓘ
+
+ Percentage of pull requests that are merged relative to the total number of pull requests that are not closed
+
+
+
+
+
+
+
+ Pull requests reviewed
+
+ ⓘ
+
+ Percentage of pull requests merged by another user that is not the author relative to the total number of merged pull requests
+
+
+
+
+
+
+
+ Merges from pull requests
+
+ ⓘ
+
+ Percentage of merges that are from pull requests
+
+
+
+
+
+
+
+ );
+}
+
+export default PullRequestsPageSummarySection;
diff --git a/src/tests/commits.test.jsx b/src/tests/commits.test.jsx
index 1755791..1c64a31 100644
--- a/src/tests/commits.test.jsx
+++ b/src/tests/commits.test.jsx
@@ -1,107 +1,179 @@
import {
- filterHistoricData,
+ getGaugeDataAnonymous,
transformCommitsDataForLineChart,
transformModifiedLinesDataForLineChart,
- prepareRadarData,
- getPieChartData,
- getGaugeChartPercentages,
- getGaugeChartModifiedLines,
+ GetRadarDataCommits,
+ GetRadarDataModifiedLines,
+ getPieChartDataCommits,
+ getPieChartDataModifiedLines,
+ getGaugeChartDataCommits,
+ getGaugeChartDataModifiedLines,
+ transformCommitsDataForUser,
+ transformModifiedLinesDataForUser,
+ transformDataForLineChartCommits,
+ transformDataForLineChartModifiedLines
} from '../domain/commits';
-describe('commitsCalculations utils', () => {
+describe('commits ', () => {
const mockHistoricData = {
'2025-06-01': {
commits: {
pau: 3,
lluis: 5,
total: 8,
- anonymous: 1
+ anonymous: 1,
},
modified_lines: {
pau: { modified: 100 },
lluis: { modified: 200 },
- total: { modified: 300 }
- }
+ total: { modified: 300 },
+ },
},
'2025-06-02': {
commits: {
pau: 4,
lluis: 6,
total: 10,
- anonymous: 2
+ anonymous: 2,
},
modified_lines: {
pau: { modified: 150 },
lluis: { modified: 250 },
- total: { modified: 400 }
- }
- }
+ total: { modified: 400 },
+ },
+ },
};
-
- test('filterHistoricData returns full data for "lifetime"', () => {
- expect(filterHistoricData(mockHistoricData, 'lifetime')).toEqual(mockHistoricData);
- });
-
test('transformCommitsDataForLineChart works as expected', () => {
- const { xDataCommits, seriesDataCommits } = transformCommitsDataForLineChart(mockHistoricData);
- expect(xDataCommits).toEqual(['2025-06-01', '2025-06-02']);
- expect(seriesDataCommits).toEqual([
+ const { xData, seriesData } = transformCommitsDataForLineChart(mockHistoricData);
+ expect(xData).toEqual(['2025-06-01', '2025-06-02']);
+ expect(seriesData).toEqual([
{ name: 'pau', data: [3, 4] },
- { name: 'lluis', data: [5, 6] }
+ { name: 'lluis', data: [5, 6] },
]);
});
test('transformModifiedLinesDataForLineChart works as expected', () => {
- const { xDataModified, seriesModified } = transformModifiedLinesDataForLineChart(mockHistoricData);
- expect(xDataModified).toEqual(['2025-06-01', '2025-06-02']);
- expect(seriesModified).toEqual([
+ const { xData, seriesData } = transformModifiedLinesDataForLineChart(mockHistoricData);
+ expect(xData).toEqual(['2025-06-01', '2025-06-02']);
+ expect(seriesData).toEqual([
{ name: 'pau', data: [100, 150] },
- { name: 'lluis', data: [200, 250] }
+ { name: 'lluis', data: [200, 250] },
]);
});
- test('prepareRadarData returns correct structure', () => {
+ test('GetRadarDataCommits returns correct radar data excluding total and anonymous', () => {
+ const input = {
+ commits: {
+ pau: 7,
+ lluis: 8,
+ total: 15,
+ anonymous: 3,
+ },
+ };
+ expect(GetRadarDataCommits(input)).toEqual({ pau: 7, lluis: 8 });
+ });
+
+ test('GetRadarDataModifiedLines returns correct radar data excluding total', () => {
+ const input = {
+ modified_lines: {
+ pau: { modified: 100 },
+ lluis: { modified: 200 },
+ total: { modified: 300 },
+ },
+ };
+ expect(GetRadarDataModifiedLines(input)).toEqual({ pau: 100, lluis: 200 });
+ });
+
+ test('getPieChartDataCommits returns filtered array excluding total and anonymous', () => {
const input = {
- pau: { modified: 100 },
- lluis: { modified: 200 }
+ commits: {
+ pau: 5,
+ lluis: 10,
+ total: 15,
+ anonymous: 2,
+ },
};
- expect(prepareRadarData(input)).toEqual({ pau: 100, lluis: 200 });
+ expect(getPieChartDataCommits(input)).toEqual([
+ ['pau', 5],
+ ['lluis', 10],
+ ]);
});
- test('getPieChartData with key works', () => {
+ test('getPieChartDataModifiedLines returns filtered array excluding total', () => {
const input = {
- pau: { modified: 100 },
- lluis: { modified: 200 },
- total: { modified: 300 }
+ modified_lines: {
+ pau: { modified: 100 },
+ lluis: { modified: 200 },
+ total: { modified: 300 },
+ },
};
- expect(getPieChartData(input, 'modified')).toEqual([
+ expect(getPieChartDataModifiedLines(input)).toEqual([
['pau', 100],
- ['lluis', 200]
+ ['lluis', 200],
]);
});
- test('getGaugeChartPercentages works', () => {
+ test('getGaugeChartDataCommits returns correct percentages excluding total and anonymous', () => {
const input = {
- pau: 3,
- lluis: 5,
- total: 8,
- anonymous: 2
+ commits: {
+ pau: 4,
+ lluis: 6,
+ total: 10,
+ anonymous: 1,
+ },
};
- expect(getGaugeChartPercentages(input, 'total', 'anonymous')).toEqual([
- { user: 'pau', percentage: 3 / 6 },
- { user: 'lluis', percentage: 5 / 6 }
+ const result = getGaugeChartDataCommits(input);
+ expect(result).toEqual([
+ { user: 'pau', percentage: 4 / 9 },
+ { user: 'lluis', percentage: 6 / 9 },
]);
});
- test('getGaugeChartModifiedLines works', () => {
+ test('getGaugeChartDataModifiedLines returns correct percentages excluding total', () => {
const input = {
- pau: { modified: 100 },
- lluis: { modified: 200 },
- total: { modified: 300 }
+ modified_lines: {
+ pau: { modified: 100 },
+ lluis: { modified: 300 },
+ total: { modified: 400 },
+ },
};
- expect(getGaugeChartModifiedLines(input, 'total')).toEqual([
- { user: 'pau', percentage: 100 / 300 },
- { user: 'lluis', percentage: 200 / 300 }
+ const result = getGaugeChartDataModifiedLines(input);
+ expect(result).toEqual([
+ { user: 'pau', percentage: 100 / 400 },
+ { user: 'lluis', percentage: 300 / 400 },
]);
});
+
+ test('transformCommitsDataForUser returns correct data for given user', () => {
+ const { xDataCommits, yDataCommits } = transformCommitsDataForUser(mockHistoricData, 'pau');
+ expect(xDataCommits).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataCommits).toEqual([3, 4]);
+ });
+
+ test('transformModifiedLinesDataForUser returns correct data for given user', () => {
+ const { xDataModifiedLines, yDataModifiedLines } = transformModifiedLinesDataForUser(mockHistoricData, 'lluis');
+ expect(xDataModifiedLines).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataModifiedLines).toEqual([200, 250]);
+ });
+
+ test('transformDataForLineChartCommits returns correct data', () => {
+ const { xData, yData } = transformDataForLineChartCommits(mockHistoricData);
+ expect(xData).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yData).toEqual([8, 10]);
+ });
+
+ test('transformDataForLineChartModifiedLines returns correct data', () => {
+ const { xDataModifedLines, yDataModifedLines } = transformDataForLineChartModifiedLines(mockHistoricData);
+ expect(xDataModifedLines).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataModifedLines).toEqual([300, 400]);
+ });
+ test('getGaugeDataAnonymous returns correct ratio of non-anonymous to total', () => {
+ const input = { commits: { total: 10, anonymous: 2 } };
+ expect(getGaugeDataAnonymous(input)).toBe(0.8);
+ });
+
+ test('getGaugeDataAnonymous returns 0 when total is 0', () => {
+ const input = { commits: { total: 0, anonymous: 0 } };
+ expect(getGaugeDataAnonymous(input)).toBe(0);
+ });
});
diff --git a/src/tests/issues.test.jsx b/src/tests/issues.test.jsx
new file mode 100644
index 0000000..3e95462
--- /dev/null
+++ b/src/tests/issues.test.jsx
@@ -0,0 +1,118 @@
+import {
+ transformAssignedIssuesDataForLineChart,
+ getRadarAndPieDataIssuesAssigned,
+ getGaugeDataIssuesAssigned,
+ getGaugeDataAssignedIssuesPerUser,
+ getGaugeDataClosedIssuesPerUser,
+ getGaugeDataIssuesHavePR,
+ transformAssignedIssuesDataForUser,
+ transformClosedIssuesDataForUser,
+ transformIssuesDataForAreaChart,
+ getPieDataIssuesStatus
+} from '../domain/issues';
+
+describe('issues', () => {
+ const mockHistoricData = {
+ '2025-06-01': {
+ issues: {
+ assigned: { pau: 3, lluis: 5, non_assigned: 1 },
+ closed: { pau: 2, lluis: 3, non_assigned: 1 },
+ total_closed: 5,
+ total: 9
+ },
+ },
+ '2025-06-02': {
+ issues: {
+ assigned: { pau: 4, lluis: 6, non_assigned: 2 },
+ closed: { pau: 2, lluis: 3},
+ total_closed: 5,
+ total: 12
+
+ },
+ },
+ };
+
+ const mockData = {
+ issues: {
+ total: 20,
+ total_closed: 10,
+ have_pull_request: 7,
+ assigned: {
+ pau: 5,
+ lluis: 5,
+ non_assigned: 10,
+ },
+ closed: {
+ pau: 3,
+ lluis: 2,
+ },
+ },
+ };
+
+ test('transformAssignedIssuesDataForLineChart returns expected structure', () => {
+ expect(transformAssignedIssuesDataForLineChart(mockHistoricData)).toEqual({
+ xDataAssigned: ['2025-06-01', '2025-06-02'],
+ seriesDataAssigned: [
+ { name: 'pau', data: [3, 4] },
+ { name: 'lluis', data: [5, 6] },
+ ],
+ });
+ });
+
+ test('getRadarAndPieDataAssigned returns radarData and pieData excluding non_assigned and total', () => {
+ expect(getRadarAndPieDataIssuesAssigned(mockData)).toEqual({
+ radarData: { pau: 5, lluis: 5 },
+ pieData: [['pau', 5], ['lluis', 5]],
+ });
+ });
+
+ test('getGaugeDataAssigned returns correct overall assigned percentage', () => {
+ expect(getGaugeDataIssuesAssigned(mockData)).toEqual((20 - 10) / 20);
+ });
+
+ test('getGaugeDataAssignedPerUser returns array with user and percentage', () => {
+ expect(getGaugeDataAssignedIssuesPerUser(mockData)).toEqual([
+ { user: 'pau', percentage: 5 / 10 },
+ { user: 'lluis', percentage: 5 / 10 },
+ ]);
+ });
+
+ test('getGaugeDataClosedPerUser returns array with user and percentage', () => {
+ expect(getGaugeDataClosedIssuesPerUser(mockData)).toEqual([
+ { user: 'pau', percentage: 3 / 5 },
+ { user: 'lluis', percentage: 2 / 5 },
+ ]);
+ });
+
+ test('getGaugeDataIssuesHavePR returns correct percentage of PRs', () => {
+ expect(getGaugeDataIssuesHavePR(mockData)).toEqual(7 / 10);
+ });
+ test('transformAssignedIssuesDataForUser returns correct data for given user', () => {
+ const { xDataAssignedIssues, yDataAssignedIssues } = transformAssignedIssuesDataForUser(mockHistoricData, 'pau');
+ expect(xDataAssignedIssues).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataAssignedIssues).toEqual([3, 4]);
+ });
+
+ test('transformClosedIssuesDataForUser returns correct data for given user', () => {
+ const { xDataClosedIssues, yDataClosedIssues } = transformClosedIssuesDataForUser(mockHistoricData, 'pau');
+ expect(xDataClosedIssues).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataClosedIssues).toEqual([2, 2]);
+ });
+
+ test('transformIssuesDataForAreaChart transforms correctly', () => {
+ const result = transformIssuesDataForAreaChart(mockHistoricData);
+ expect(result.xDataIssues).toEqual(['2025-06-01', '2025-06-02']);
+ expect(result.closedIssues).toEqual([5,5]);
+ expect(result.openIssues).toEqual([4,7]);
+
+ });
+ test('getPieDataIssuesStatus returns correct open and closed issues data and colors', () => {
+ expect(getPieDataIssuesStatus(mockData)).toEqual({
+ pieDataIssuesStatus: [
+ ['Open', 10],
+ ['Closed', 10],
+ ],
+ pieDataIssuesStatusColor: ['red', 'green'],
+ });
+});
+});
diff --git a/src/tests/projects.test.jsx b/src/tests/projects.test.jsx
new file mode 100644
index 0000000..338f3b7
--- /dev/null
+++ b/src/tests/projects.test.jsx
@@ -0,0 +1,300 @@
+import {
+ getActiveIteration,
+ filterHistoricDataByIteration,
+ transformAssignedTasksDataForLineChart,
+ transformFeatureDataForAreaChart,
+ getRadarPieDataAssignedTasks,
+ getDateRangeForIteration,
+ getIssueTypeDataForChart,
+ getFeatureDataForChart,
+ getGaugeDataTasksAssigned,
+ getGaugeDataTasksStandardStatus,
+ getGaugeDataItemsIssues,
+ getGaugeDataItemIssuesWithType,
+ getGaugeDataItemIssuesWithIteration,
+ getGaugeDataAssignedTasksPerUser,
+ getGaugeDataInProgressTasksPerUser,
+ getGaugeDataDoneTasksPerUser,
+ getGaugeDataStandardStatusTasksPerUser,
+ transformTasksAssignedDataForUser,
+ transformTasksToDoDataForUser,
+ transformTasksInProgressDataForUser,
+ transformTasksDoneDataForUser,
+ transformTasksStandardDataForUser,
+ transformTaskDataForAreaChart,
+ getPieDataTasksStatus
+} from '../domain/projects';
+
+describe('projects', () => {
+ const mockData = {
+ project: {
+ has_iterations: true,
+ iterations: {
+ it1: {
+ startDate: '2025-06-01',
+ endDate: '2025-06-03',
+ },
+ },
+ metrics_by_iteration: {
+ it1: {
+ assigned_per_member: {
+ pau: 3,
+ lluis: 2,
+ non_assigned: 6,
+ },
+ in_progress_per_member: {
+ pau: 1,
+ lluis: 0,
+ },
+ done_per_member: {
+ pau: 2,
+ lluis: 1,
+ },
+ todo_per_member: {
+ pau: 0,
+ lluis: 1,
+ },
+ new_state_per_member: {
+ pau: 0,
+ lluis: 1,
+ },
+ in_progress: 3,
+ done: 4,
+ todo: 3,
+ new_state: 1,
+ total_tasks: 11,
+ total_issues: 4,
+ total_issues_with_type: 3,
+ total: 12,
+ total_features_todo: 1,
+ total_features_in_progress: 2,
+ total_features_done: 3,
+ total_features_new_state: 5,
+ total_bugs: 2,
+ total_features: 3
+ },
+ no_iteration: {
+ total: 1
+ },
+ total: {
+ assigned_per_member: {
+ pau: 3,
+ lluis: 2,
+ non_assigned: 6,
+ },
+ in_progress_per_member: {
+ pau: 1,
+ lluis: 0,
+ },
+ done_per_member: {
+ pau: 2,
+ lluis: 1,
+ },
+ todo_per_member: {
+ pau: 0,
+ lluis: 1,
+ },
+ new_state_per_member: {
+ pau: 0,
+ lluis: 1,
+ },
+ in_progress: 3,
+ done: 4,
+ todo: 3,
+ new_state: 1,
+ total_tasks: 12,
+ total_issues: 4,
+ total_issues_with_type: 3,
+ total: 13,
+ total_features_todo: 1,
+ total_features_in_progress: 2,
+ total_features_done: 3,
+ total_features_new_state: 5,
+ total_bugs: 2,
+ total_features: 11 }
+ }
+ }
+ };
+
+ const mockHistoricData = {
+ '2025-06-01': mockData,
+ '2025-06-02': mockData,
+ };
+
+test('getActiveIteration returns correct iteration with mocked date', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-06-02'));
+
+ const result = getActiveIteration(mockData);
+ expect(result).toBe('it1');
+
+ vi.useRealTimers();
+});
+
+ test('filterHistoricDataByIteration filters data correctly', () => {
+ const result = filterHistoricDataByIteration(mockData, mockHistoricData, 'it1');
+ expect(Object.keys(result)).toEqual(['2025-06-01', '2025-06-02']);
+ });
+
+ test('getRadarPieDataAssignedTasks returns radar and pie data', () => {
+ const result = getRadarPieDataAssignedTasks(mockData, 'it1');
+ expect(result).toEqual({
+ radarDataAssigned: { pau: 3, lluis: 2 },
+ pieDataAssigned: [['pau', 3], ['lluis', 2]],
+ });
+ });
+
+ test('getGaugeDataTasksAssigned returns correct percentage', () => {
+ expect(getGaugeDataTasksAssigned(mockData, 'it1')).toBe((5 / 11));
+ });
+
+ test('getGaugeDataTasksStandardStatus returns correct percentage', () => {
+ expect(getGaugeDataTasksStandardStatus(mockData, 'it1')).toBe((10 / 11));
+ });
+
+ test('getGaugeDataItemsIssues returns correct percentage', () => {
+ expect(getGaugeDataItemsIssues(mockData, 'it1')).toBe(4 / 12);
+ });
+
+ test('getGaugeDataItemIssuesWithType returns correct percentage', () => {
+ expect(getGaugeDataItemIssuesWithType(mockData, 'it1')).toBe(3 / 4);
+ });
+
+ test('getGaugeDataItemIssuesWithIteration returns correct percentage', () => {
+ expect(getGaugeDataItemIssuesWithIteration(mockData)).toBe((13 - 1) / 13);
+ });
+
+ test('getGaugeDataAssignedTasksPerUser returns per-user assignment percentages', () => {
+ expect(getGaugeDataAssignedTasksPerUser(mockData, 'it1')).toEqual([
+ { user: 'pau', percentage: 3 / 5 },
+ { user: 'lluis', percentage: 2 / 5 },
+ ]);
+ });
+
+ test('getGaugeDataInProgressTasksPerUser returns 1 if in progress > 0', () => {
+ expect(getGaugeDataInProgressTasksPerUser(mockData, 'it1')).toEqual([
+ { user: 'pau', percentage: 1 },
+ { user: 'lluis', percentage: 0 },
+ ]);
+ });
+
+ test('getGaugeDataDoneTasksPerUser returns done / assigned per user', () => {
+ expect(getGaugeDataDoneTasksPerUser(mockData, 'it1')).toEqual([
+ { user: 'pau', percentage: 2 / 3 },
+ { user: 'lluis', percentage: 1 / 2 },
+ ]);
+ });
+
+ test('getGaugeDataStandardStatusTasksPerUser returns correct per user', () => {
+ expect(getGaugeDataStandardStatusTasksPerUser(mockData, 'it1')).toEqual([
+ { user: 'pau', percentage: 1 },
+ { user: 'lluis', percentage: 1 },
+ ]);
+ });
+
+ test('getDateRangeForIteration returns formatted date range', () => {
+ expect(getDateRangeForIteration(mockData, 'it1')).toBe('01/06/2025 - 03/06/2025');
+ });
+
+ test('getIssueTypeDataForChart returns pie data and colors', () => {
+ const { typePieChartData, typeColorsPieChart } = getIssueTypeDataForChart(mockData, 'it1');
+ expect(typePieChartData).toEqual([
+ ['Tasks', 11],
+ ['Features', 3],
+ ['Bugs', 2],
+ ]);
+ expect(typeColorsPieChart).toEqual(['orange', '#0f58ff', 'red']);
+ });
+
+ test('getFeatureDataForChart returns correct pie data and colors', () => {
+ const { featurePieChartData, featureColorsPieChart } = getFeatureDataForChart(mockData, 'it1');
+ expect(featurePieChartData).toEqual([
+ ['To Do', 1],
+ ['In Progress', 2],
+ ['Done', 3],
+ ['New State', 5],
+ ]);
+ expect(featureColorsPieChart).toEqual(['green', 'orange', 'red']);
+ });
+
+ test('transformFeatureDataForAreaChart transforms correctly with selectedIteration "total"', () => {
+ const result = transformFeatureDataForAreaChart(mockHistoricData, 'total', {});
+ expect(result.xDataFeature).toEqual(['2025-06-01', '2025-06-02']);
+
+ const doneSeries = result.allSeries.find(s => s.label === 'Done');
+ const inProgressSeries = result.allSeries.find(s => s.label === 'In Progress');
+ const toDoSeries = result.allSeries.find(s => s.label === 'To Do');
+ const customSeries = result.allSeries.find(s => s.label === 'New State');
+
+ expect(doneSeries.data).toEqual([3, 3]);
+ expect(inProgressSeries.data).toEqual([2, 2]);
+ expect(toDoSeries.data).toEqual([1, 1]);
+ expect(customSeries.data).toEqual([5, 5]);
+ });
+
+ test('transformTasksAssignedDataForUser returns correct assigned tasks data for pau', () => {
+ const { xDataTasksAssigned, yDataTasksAssigned } = transformTasksAssignedDataForUser(mockHistoricData, 'pau');
+ expect(xDataTasksAssigned).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataTasksAssigned).toEqual([3, 3]);
+ });
+
+ test('transformTasksToDoDataForUser returns correct todo tasks data for lluis', () => {
+ const { xDataTasksToDo, yDataTasksToDo } = transformTasksToDoDataForUser(mockHistoricData, 'lluis');
+ expect(xDataTasksToDo).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataTasksToDo).toEqual([1, 1]);
+ });
+
+ test('transformTasksInProgressDataForUser returns correct in-progress tasks data for pau', () => {
+ const { xDataTasksInProgress, yDataTasksInProgress } = transformTasksInProgressDataForUser(mockHistoricData, 'pau');
+ expect(xDataTasksInProgress).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataTasksInProgress).toEqual([1, 1]);
+ });
+
+ test('transformTasksDoneDataForUser returns correct done tasks data for lluis', () => {
+ const { xDataTasksDone, yDataTasksDone } = transformTasksDoneDataForUser(mockHistoricData, 'lluis');
+ expect(xDataTasksDone).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataTasksDone).toEqual([1, 1]);
+ });
+
+ test('transformTasksStandardDataForUser returns sum todo + in_progress + done for pau', () => {
+ const { xDataTasksStandard, yDataTasksStandard } = transformTasksStandardDataForUser(mockHistoricData, 'pau');
+ expect(xDataTasksStandard).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataTasksStandard).toEqual([
+ 0 + 1 + 2,
+ 0 + 1 + 2,
+ ]);
+ });
+
+ test('transformTaskDataForAreaChart estructura correcta con métricas adicionales', () => {
+ const result = transformTaskDataForAreaChart(mockHistoricData);
+
+ expect(result.xDataTask).toEqual(['2025-06-01', '2025-06-02']);
+
+ const doneSeries = result.allSeries.find(s => s.label === 'Done');
+ const inProgressSeries = result.allSeries.find(s => s.label === 'In Progress');
+ const toDoSeries = result.allSeries.find(s => s.label === 'To Do');
+ const newStateSeries = result.allSeries.find(s => s.label === 'New State');
+
+ expect(doneSeries?.data).toEqual([4, 4]);
+ expect(inProgressSeries?.data).toEqual([3, 3]);
+ expect(toDoSeries?.data).toEqual([3, 3]);
+ expect(newStateSeries?.data).toEqual([1, 1]);
+ });
+
+ test('getPieDataTasksStatus genera datos correctos de pastel con métricas adicionales', () => {
+ const pie = getPieDataTasksStatus(mockData);
+
+ expect(pie.pieDataTasksStatus).toEqual(expect.arrayContaining([
+ ['Todo', 3],
+ ['In Progress', 3],
+ ['Done', 4],
+ ['New State', 1],
+ ]));
+
+ expect(pie.pieDataTasksStatusColor).toEqual(expect.arrayContaining([
+ 'red',
+ 'orange',
+ 'green',
+ ]));
+ });
+});
diff --git a/src/tests/pullRequests.test.jsx b/src/tests/pullRequests.test.jsx
new file mode 100644
index 0000000..8c8c005
--- /dev/null
+++ b/src/tests/pullRequests.test.jsx
@@ -0,0 +1,162 @@
+import {
+ transformCreatedPRsDataForLineChart,
+ transformMergedPRsDataForLineChart,
+ getRadarAndPieDataForCreatedPRs,
+ getRadarAndPieDataForMergedPRs,
+ getGaugeChartDataMergedPRs,
+ getGaugeChartDataReviewedPRs,
+ getGaugeChartDataMergesPRs,
+ getGaugeDataCreatedPRsPerUser,
+ getGaugeDataMergedPRsPerUser,
+ transformCreatedPRsDataForUser,
+ transformMergedPRsDataForUser,
+ transformPRDataForAreaChart,
+ getPieDataPullRequestStatus
+} from '../domain/pullRequests';
+
+describe('pullRequests', () => {
+ const mockPRsData = {
+ '2025-06-01': {
+ pull_requests: {
+ created: {
+ pau: 2,
+ lluis: 3,
+ },
+ merged_per_member: {
+ pau: 1,
+ lluis: 2,
+ },
+ total: 5,
+ merged: 3,
+ closed: 1,
+ },
+ },
+ '2025-06-02': {
+ pull_requests: {
+ created: {
+ pau: 3,
+ lluis: 4,
+ },
+ merged_per_member: {
+ pau: 2,
+ lluis: 3,
+ },
+ total: 7,
+ merged: 5,
+ closed: 1,
+ },
+ },
+ };
+
+ test('transformCreatedPRsDataForLineChart works as expected', () => {
+ const { xDataCreated, seriesDataCreated } = transformCreatedPRsDataForLineChart(mockPRsData);
+ expect(xDataCreated).toEqual(['2025-06-01', '2025-06-02']);
+ expect(seriesDataCreated).toEqual([
+ { name: 'pau', data: [2, 3] },
+ { name: 'lluis', data: [3, 4] },
+ ]);
+ });
+
+ test('transformMergedPRsDataForLineChart works as expected', () => {
+ const { xDataMerged, seriesDataMerged } = transformMergedPRsDataForLineChart(mockPRsData);
+ expect(xDataMerged).toEqual(['2025-06-01', '2025-06-02']);
+ expect(seriesDataMerged).toEqual([
+ { name: 'pau', data: [1, 2] },
+ { name: 'lluis', data: [2, 3] },
+ ]);
+ });
+
+ const aggregatedData = {
+ pull_requests: {
+ created: {
+ pau: 3,
+ lluis: 5,
+ },
+ merged_per_member: {
+ pau: 2,
+ lluis: 3,
+ },
+ total: 10,
+ merged: 5,
+ closed: 2,
+ not_merged_by_author: 3,
+ },
+ commit_merges: 6,
+ };
+
+ test('getRadarAndPieDataForCreatedPRs returns correct radar and pie data', () => {
+ const { radarDataCreated, pieDataCreated } = getRadarAndPieDataForCreatedPRs(aggregatedData);
+ expect(radarDataCreated).toEqual({ pau: 3, lluis: 5 });
+ expect(pieDataCreated).toEqual([['pau', 3], ['lluis', 5]]);
+ });
+
+ test('getRadarAndPieDataForMergedPRs returns correct radar and pie data', () => {
+ const { radarDataMerged, pieDataMerged } = getRadarAndPieDataForMergedPRs(aggregatedData);
+ expect(radarDataMerged).toEqual({ pau: 2, lluis: 3 });
+ expect(pieDataMerged).toEqual([['pau', 2], ['lluis', 3]]);
+ });
+
+ test('getGaugeChartDataMergedPRs returns correct percentage', () => {
+ const result = getGaugeChartDataMergedPRs(aggregatedData);
+ expect(result).toBe(5 / (10 - 2));
+ });
+
+ test('getGaugeChartDataReviewedPRs returns correct percentage', () => {
+ const result = getGaugeChartDataReviewedPRs(aggregatedData);
+ expect(result).toBe(3 / 5);
+ });
+
+ test('getGaugeChartDataMergesPRs returns correct percentage', () => {
+ const result = getGaugeChartDataMergesPRs(aggregatedData);
+ expect(result).toBe(5 / 6);
+ });
+
+ test('getGaugeDataCreatedPRsPerUser returns correct user percentages', () => {
+ const result = getGaugeDataCreatedPRsPerUser(aggregatedData);
+ expect(result).toEqual([
+ { user: 'pau', percentage: 3 / 10 },
+ { user: 'lluis', percentage: 5 / 10 },
+ ]);
+ });
+
+ test('getGaugeDataMergedPRsPerUser returns correct user percentages', () => {
+ const result = getGaugeDataMergedPRsPerUser(aggregatedData);
+ expect(result).toEqual([
+ { user: 'pau', percentage: 2 / 5 },
+ { user: 'lluis', percentage: 3 / 5 },
+ ]);
+ });
+ test('transformCreatedPRsDataForUser returns correct created PRs data for pau', () => {
+ const { xDataCreatedPRs, yDataCreatedPRs } = transformCreatedPRsDataForUser(mockPRsData, 'pau');
+ expect(xDataCreatedPRs).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataCreatedPRs).toEqual([2, 3]);
+ });
+
+ test('transformMergedPRsDataForUser returns correct merged PRs data for lluis', () => {
+ const { xDataMergedPRs, yDataMergedPRs } = transformMergedPRsDataForUser(mockPRsData, 'lluis');
+ expect(xDataMergedPRs).toEqual(['2025-06-01', '2025-06-02']);
+ expect(yDataMergedPRs).toEqual([2, 3]);
+ });
+
+ test('transformPRDataForAreaChart works as expected', () => {
+ const { xDataPRs, areaPRData } = transformPRDataForAreaChart(mockPRsData);
+
+ expect(xDataPRs).toEqual(['2025-06-01', '2025-06-02']);
+ expect(areaPRData).toEqual([
+ { label: 'Merged', data: [3, 5], color: 'rgb(0, 255, 0)' },
+ { label: 'Closed', data: [1, 1], color: 'orange' },
+ { label: 'Open', data: [1, 1], color: 'rgb(255, 0, 0)' },
+ ]);
+});
+
+test('getPieDataPullRequestStatus returns correct pie data and colors', () => {
+ const { pieDataPullRequestStatus, pieDataPullRequestStatusColor } = getPieDataPullRequestStatus(aggregatedData);
+
+ expect(pieDataPullRequestStatus).toEqual([
+ ['Open', aggregatedData.pull_requests.total - aggregatedData.pull_requests.merged - aggregatedData.pull_requests.closed],
+ ['Closed', aggregatedData.pull_requests.closed],
+ ['Merged', aggregatedData.pull_requests.merged],
+ ]);
+ expect(pieDataPullRequestStatusColor).toEqual(['red', 'orange', 'green']);
+});
+});
diff --git a/src/tests/utils.test.jsx b/src/tests/utils.test.jsx
new file mode 100644
index 0000000..19a240b
--- /dev/null
+++ b/src/tests/utils.test.jsx
@@ -0,0 +1,63 @@
+import { filterHistoricData,truncateName } from '../domain/utils';
+
+describe('utils', () => {
+ const mockHistoricData = {
+ '2025-06-01': {
+ commits: {
+ pau: 3,
+ lluis: 5,
+ total: 8,
+ anonymous: 1,
+ },
+ modified_lines: {
+ pau: { modified: 100 },
+ lluis: { modified: 200 },
+ total: { modified: 300 },
+ },
+ },
+ '2025-06-02': {
+ commits: {
+ pau: 4,
+ lluis: 6,
+ total: 10,
+ anonymous: 2,
+ },
+ modified_lines: {
+ pau: { modified: 150 },
+ lluis: { modified: 250 },
+ total: { modified: 400 },
+ },
+ },
+ };
+
+ test('filterHistoricData returns full data for "lifetime"', () => {
+ expect(filterHistoricData(mockHistoricData, 'lifetime')).toEqual(mockHistoricData);
+ });
+
+ test('filterHistoricData filters correctly by days', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-06-03'));
+
+ const filtered = filterHistoricData(mockHistoricData, '1');
+ expect(Object.keys(filtered)).toEqual(['2025-06-02']);
+
+ vi.useRealTimers();
+ });
+
+
+ test('truncateName works correctly with long strings', () => {
+
+ const string = truncateName("thisShouldBeTruncated");
+ expect(string).toEqual("thisShouldBeTrunca...");
+
+ });
+
+ test('truncateName works correctly with short strings', () => {
+
+ const string = truncateName("Pau");
+ expect(string).toEqual("Pau");
+
+ });
+});
+
+