Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 63 additions & 92 deletions backend/controllers/analytics.controller.js
Original file line number Diff line number Diff line change
@@ -1,106 +1,77 @@
import supabase from "../config/db.js";

const memberProfiles = [
{ id: "alex", name: "Alex Rivera", role: "Lead Frontend Engineer", focus: "Frontend", image: "https://i.pravatar.cc/96?img=11" },
{ id: "jordan", name: "Jordan Smith", role: "Backend Engineer", focus: "API", image: "https://i.pravatar.cc/96?img=12" },
{ id: "casey", name: "Casey Morgan", role: "Fullstack Developer", focus: "Fullstack", image: "https://i.pravatar.cc/96?img=13" },
{ id: "riley", name: "Riley Lee", role: "QA Analyst", focus: "QA", image: "https://i.pravatar.cc/96?img=14" },
{ id: "morgan", name: "Morgan Patel", role: "Product Engineer", focus: "Product", image: "https://i.pravatar.cc/96?img=15" },
{ id: "quinn", name: "Quinn Taylor", role: "DevOps Engineer", focus: "Ops", image: "https://i.pravatar.cc/96?img=16" },
];

function hashString(value) {
return String(value || "")
.split("")
.reduce((total, char) => total + char.charCodeAt(0), 0);
}

function buildMembers(tasks = [], messages = [], sprintOffset = 0) {
const taskAssignments = Object.create(null);
const completedAssignments = Object.create(null);
const messageAssignments = Object.create(null);

tasks.forEach((task, taskIndex) => {
const seed = hashString(task.id || task.title || taskIndex);
const memberIndex = seed % memberProfiles.length;
export const getAnalytics = async (req, res) => {
try {
const [
{ data: tasks, error: tasksError },
{ data: messages, error: messagesError },
] = await Promise.all([
supabase.from("tasks").select("id,title,status,position,created_at"),
supabase.from("messages").select("id,username,text,created_at"),
]);

taskAssignments[memberIndex] =
(taskAssignments[memberIndex] || 0) + 1;
if (tasksError) throw tasksError;
if (messagesError) throw messagesError;

if (task.status === "done") {
completedAssignments[memberIndex] =
(completedAssignments[memberIndex] || 0) + 1;
// Build a roster of real contributors by collecting every distinct username
// that appears in the messages table. For each contributor compute metrics
// derived entirely from the real data stored in Supabase.
const usernameSet = new Set();
for (const msg of messages || []) {
if (msg.username) usernameSet.add(msg.username);
}
});

messages.forEach((message) => {
const seed = hashString(message.username || message.id);
const memberIndex = seed % memberProfiles.length;

messageAssignments[memberIndex] =
(messageAssignments[memberIndex] || 0) + 1;
});

return memberProfiles.map((profile, index) => {
const assignedTasks = taskAssignments[index] || 0;
const completedTasks = completedAssignments[index] || 0;
const messageCount = messageAssignments[index] || 0;

const assigned = Math.min(
98,
Math.max(35, assignedTasks * 12 + 48 - sprintOffset * 6)
);

const completed = Math.min(
assigned,
Math.max(
20,
completedTasks * 18 + messageCount * 4 + 34 - sprintOffset * 5
)
);

const reviews = Math.max(
6,
messageCount * 2 + completedTasks + 8 - sprintOffset
);

const activity = Array.from({ length: 8 }, (_, day) => {
const base =
assigned +
completed +
reviews +
index * 9 +
day * 11 -
sprintOffset * 7;

return Math.min(100, Math.max(18, base % 100));
});

return {
...profile,
assigned,
completed,
reviews,
activity,
// If no messages exist yet, include a placeholder so the UI is not empty.
const usernames = usernameSet.size > 0 ? Array.from(usernameSet) : ["team"];

const buildMembers = (sprintTasks = []) => {
return usernames.map((username) => {
const userMessages = (messages || []).filter(
(m) => m.username === username
);

// Tasks are not directly linked to users in the current schema, so
// distribute them evenly across contributors as a best-effort metric
// until per-user task assignment is available in the data model.
const index = usernames.indexOf(username);
const assignedTasks = sprintTasks.filter(
(_, i) => i % usernames.length === index
);
const completedTasks = assignedTasks.filter(
(t) => t.status === "done"
);

// Build a 8-day activity histogram from real message timestamps.
const activity = Array.from({ length: 8 }, (_, day) => {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - (7 - day));
const dayStart = new Date(cutoff);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(cutoff);
dayEnd.setHours(23, 59, 59, 999);
return userMessages.filter((m) => {
const t = new Date(m.created_at);
return t >= dayStart && t <= dayEnd;
}).length;
});

return {
name: username,
assigned: assignedTasks.length,
completed: completedTasks.length,
reviews: userMessages.length,
activity,
};
});
};
});
}

export const getAnalytics = async (req, res) => {
try {
const [{ data: tasks, error: tasksError }, { data: messages, error: messagesError }] =
await Promise.all([
supabase.from("tasks").select("id,title,status,position,created_at"),
supabase.from("messages").select("id,username,text,created_at"),
]);

if (tasksError) throw tasksError;
if (messagesError) throw messagesError;
const allTasks = tasks || [];
const halfLen = Math.ceil(allTasks.length / 2);

res.status(200).json({
sprints: [
{ label: "Sprint 42", members: buildMembers(tasks || [], messages || [], 0) },
{ label: "Sprint 41", members: buildMembers(tasks || [], messages || [], 1) },
{ label: "Current Sprint", members: buildMembers(allTasks.slice(halfLen)) },
{ label: "Previous Sprint", members: buildMembers(allTasks.slice(0, halfLen)) },
],
generatedAt: new Date().toISOString(),
});
Expand Down
Loading