791653f45c
Changed buildMonitorEntries signature to accept Map<String, LeaveOfAbsence> and Map<String, PassSlip> instead of List types, matching the spec requirement that callers pass pre-computed maps from activeLeaveByUser() and activePassSlipByUser(). - Updated function signature for both parameters - Removed internal calls to activeLeaveByUser/activePassSlipByUser - Updated state-determination to use direct map lookups - Updated all test cases to pass computed maps - All 19 tests passing - No new analyzer issues Co-Authored-By: claude-flow <ruv@ruv.net>
395 lines
11 KiB
Dart
395 lines
11 KiB
Dart
import 'package:tasq/models/leave_of_absence.model.dart';
|
|
import 'package:tasq/models/pass_slip.model.dart';
|
|
import 'package:tasq/models/profile.model.dart';
|
|
import 'package:tasq/models/programmer_task.model.dart';
|
|
import 'package:tasq/models/programmer_task_work_log.model.dart';
|
|
import 'package:tasq/providers/programmer_day_data_provider.dart';
|
|
import 'package:tasq/utils/programmer_day_rows.dart';
|
|
|
|
enum MonitorState { working, paused, idle, onLeave, passSlip }
|
|
|
|
class HelpEdge {
|
|
final String helperId;
|
|
final String assigneeId;
|
|
final String taskId;
|
|
final String taskTitle;
|
|
final int minutes;
|
|
final DateTime at;
|
|
|
|
HelpEdge({
|
|
required this.helperId,
|
|
required this.assigneeId,
|
|
required this.taskId,
|
|
required this.taskTitle,
|
|
required this.minutes,
|
|
required this.at,
|
|
});
|
|
}
|
|
|
|
/// Build help edges from work logs for a given day.
|
|
///
|
|
/// Returns edges where:
|
|
/// - author != assignee (author is helping)
|
|
/// - minutes > 0
|
|
/// - workDate == day
|
|
///
|
|
/// Sorted by createdAt newest first.
|
|
List<HelpEdge> buildHelpEdges({
|
|
required List<ProgrammerTaskWorkLog> workLogs,
|
|
required Map<String, ProgrammerTask> taskById,
|
|
required DateTime day,
|
|
}) {
|
|
final edges = <HelpEdge>[];
|
|
|
|
for (final log in workLogs) {
|
|
final task = taskById[log.taskId];
|
|
if (task == null) continue;
|
|
|
|
// Filter: author != assignee AND minutes > 0 AND workDate == day
|
|
if (log.authorId == task.assigneeId) continue;
|
|
if (log.minutes == null || log.minutes! <= 0) continue;
|
|
if (log.workDate != day) continue;
|
|
|
|
edges.add(HelpEdge(
|
|
helperId: log.authorId,
|
|
assigneeId: task.assigneeId ?? '',
|
|
taskId: log.taskId,
|
|
taskTitle: task.title,
|
|
minutes: log.minutes!,
|
|
at: log.createdAt,
|
|
));
|
|
}
|
|
|
|
// Sort by createdAt newest first
|
|
edges.sort((a, b) => b.at.compareTo(a.at));
|
|
|
|
return edges;
|
|
}
|
|
|
|
class ProgrammerMonitorEntry {
|
|
final Profile programmer;
|
|
final MonitorState state;
|
|
final ProgrammerTask? currentTask;
|
|
final Duration currentTaskToday;
|
|
final Duration todayTotal;
|
|
final int inProgress;
|
|
final int paused;
|
|
final int notStarted;
|
|
final String? sheetStatus;
|
|
final List<HelpEdge> helped;
|
|
final List<HelpEdge> helpedBy;
|
|
|
|
ProgrammerMonitorEntry({
|
|
required this.programmer,
|
|
required this.state,
|
|
required this.currentTask,
|
|
required this.currentTaskToday,
|
|
required this.todayTotal,
|
|
required this.inProgress,
|
|
required this.paused,
|
|
required this.notStarted,
|
|
this.sheetStatus,
|
|
required this.helped,
|
|
required this.helpedBy,
|
|
});
|
|
}
|
|
|
|
/// Build monitor entries for all programmers for a given day.
|
|
///
|
|
/// Sort order: working -> paused -> idle -> passSlip -> onLeave,
|
|
/// then alphabetical by programmer.fullName within each state group.
|
|
///
|
|
/// Parameters [leaves] and [passSlips] should be pre-computed maps from
|
|
/// activeLeaveByUser() and activePassSlipByUser() respectively.
|
|
List<ProgrammerMonitorEntry> buildMonitorEntries({
|
|
required List<Profile> programmers,
|
|
required DayActivityData today,
|
|
required Map<String, ProgrammerRunInfo> runStates,
|
|
required Map<String, LeaveOfAbsence> leaves,
|
|
required Map<String, PassSlip> passSlips,
|
|
required Map<String, String> sheetStatusByUser,
|
|
required Map<String, String> projectNames,
|
|
required Map<String, String> profileNames,
|
|
required DateTime now,
|
|
}) {
|
|
final taskById = {for (final t in today.tasks) t.id: t};
|
|
|
|
final entries = <ProgrammerMonitorEntry>[];
|
|
|
|
for (final programmer in programmers) {
|
|
final programmerId = programmer.id;
|
|
|
|
// Step 1: Determine state
|
|
// Priority: onLeave > passSlip > working/paused/idle
|
|
MonitorState state;
|
|
if (leaves.containsKey(programmerId)) {
|
|
state = MonitorState.onLeave;
|
|
} else if (passSlips.containsKey(programmerId)) {
|
|
state = MonitorState.passSlip;
|
|
} else {
|
|
// Determine from task states
|
|
state = _determineState(programmerId, today.tasks, runStates);
|
|
}
|
|
|
|
// Step 2: Find current task (oldest running task)
|
|
ProgrammerTask? currentTask;
|
|
if (state == MonitorState.working) {
|
|
currentTask = _findCurrentTask(programmerId, today.tasks, runStates);
|
|
}
|
|
|
|
// Step 3: Count tasks by state
|
|
final (inProgress, paused, notStarted) =
|
|
_countTasksByState(programmerId, today.tasks, runStates);
|
|
|
|
// Step 4: Calculate durations
|
|
final allTasksRows = buildDayRows(
|
|
userId: programmerId,
|
|
day: today.day,
|
|
tasks: today.tasks,
|
|
logsByTask: today.logsByTask,
|
|
workLogs: today.workLogs,
|
|
projectNames: projectNames,
|
|
profileNames: profileNames,
|
|
now: now,
|
|
);
|
|
final todayTotal = Duration(seconds: totalSeconds(allTasksRows));
|
|
|
|
// For current task, filter to only that task's work logs
|
|
final currentTaskToday = _calculateCurrentTaskDuration(
|
|
programmerId,
|
|
currentTask,
|
|
today,
|
|
projectNames,
|
|
profileNames,
|
|
now,
|
|
);
|
|
|
|
// Step 5: Build helped and helpedBy edges
|
|
final helped = _buildHelpedEdges(programmerId, today.workLogs, taskById, today.day);
|
|
final helpedBy = _buildHelpedByEdges(programmerId, today.workLogs, taskById, today.day);
|
|
|
|
entries.add(ProgrammerMonitorEntry(
|
|
programmer: programmer,
|
|
state: state,
|
|
currentTask: currentTask,
|
|
currentTaskToday: currentTaskToday,
|
|
todayTotal: todayTotal,
|
|
inProgress: inProgress,
|
|
paused: paused,
|
|
notStarted: notStarted,
|
|
sheetStatus: sheetStatusByUser[programmerId],
|
|
helped: helped,
|
|
helpedBy: helpedBy,
|
|
));
|
|
}
|
|
|
|
// Sort by state then by name
|
|
entries.sort((a, b) {
|
|
final stateOrder = [
|
|
MonitorState.working,
|
|
MonitorState.paused,
|
|
MonitorState.idle,
|
|
MonitorState.passSlip,
|
|
MonitorState.onLeave,
|
|
];
|
|
final aStateIdx = stateOrder.indexOf(a.state);
|
|
final bStateIdx = stateOrder.indexOf(b.state);
|
|
|
|
if (aStateIdx != bStateIdx) {
|
|
return aStateIdx.compareTo(bStateIdx);
|
|
}
|
|
|
|
return a.programmer.fullName.compareTo(b.programmer.fullName);
|
|
});
|
|
|
|
return entries;
|
|
}
|
|
|
|
/// Determine state from task states
|
|
MonitorState _determineState(
|
|
String programmerId,
|
|
List<ProgrammerTask> tasks,
|
|
Map<String, ProgrammerRunInfo> runStates,
|
|
) {
|
|
final programmerTasks = tasks.where((t) => t.assigneeId == programmerId).toList();
|
|
|
|
var hasRunning = false;
|
|
var hasPaused = false;
|
|
|
|
for (final task in programmerTasks) {
|
|
final runInfo = runStates[task.id];
|
|
if (runInfo == null) continue;
|
|
|
|
if (runInfo.state == ProgrammerRunState.running) {
|
|
hasRunning = true;
|
|
} else if (runInfo.state == ProgrammerRunState.paused) {
|
|
hasPaused = true;
|
|
}
|
|
}
|
|
|
|
if (hasRunning) return MonitorState.working;
|
|
if (hasPaused) return MonitorState.paused;
|
|
return MonitorState.idle;
|
|
}
|
|
|
|
/// Find the current task (oldest running task by since)
|
|
ProgrammerTask? _findCurrentTask(
|
|
String programmerId,
|
|
List<ProgrammerTask> tasks,
|
|
Map<String, ProgrammerRunInfo> runStates,
|
|
) {
|
|
final runningTasks = tasks
|
|
.where((t) =>
|
|
t.assigneeId == programmerId &&
|
|
runStates[t.id]?.state == ProgrammerRunState.running)
|
|
.toList();
|
|
|
|
if (runningTasks.isEmpty) return null;
|
|
|
|
// Sort by since (earliest first)
|
|
runningTasks.sort((a, b) {
|
|
final aSince = runStates[a.id]?.since ?? DateTime(0);
|
|
final bSince = runStates[b.id]?.since ?? DateTime(0);
|
|
return aSince.compareTo(bSince);
|
|
});
|
|
|
|
return runningTasks.first;
|
|
}
|
|
|
|
/// Count tasks by state
|
|
(int inProgress, int paused, int notStarted) _countTasksByState(
|
|
String programmerId,
|
|
List<ProgrammerTask> tasks,
|
|
Map<String, ProgrammerRunInfo> runStates,
|
|
) {
|
|
var inProgress = 0;
|
|
var paused = 0;
|
|
var notStarted = 0;
|
|
|
|
final programmerTasks = tasks.where((t) => t.assigneeId == programmerId).toList();
|
|
|
|
for (final task in programmerTasks) {
|
|
final runInfo = runStates[task.id];
|
|
if (runInfo == null) continue;
|
|
|
|
if (runInfo.state == ProgrammerRunState.running ||
|
|
runInfo.state == ProgrammerRunState.paused) {
|
|
// This is still "in progress" (running or paused)
|
|
// We count these separately
|
|
if (runInfo.state == ProgrammerRunState.running) {
|
|
inProgress++;
|
|
} else {
|
|
paused++;
|
|
}
|
|
} else if (runInfo.state == ProgrammerRunState.notStarted) {
|
|
notStarted++;
|
|
}
|
|
}
|
|
|
|
return (inProgress, paused, notStarted);
|
|
}
|
|
|
|
/// Calculate duration for current task
|
|
Duration _calculateCurrentTaskDuration(
|
|
String programmerId,
|
|
ProgrammerTask? currentTask,
|
|
DayActivityData today,
|
|
Map<String, String> projectNames,
|
|
Map<String, String> profileNames,
|
|
DateTime now,
|
|
) {
|
|
if (currentTask == null) return Duration.zero;
|
|
|
|
// Filter work logs to only this task
|
|
final currentTaskLogs =
|
|
today.workLogs.where((w) => w.taskId == currentTask.id).toList();
|
|
|
|
// Filter activity logs to only this task
|
|
final currentTaskActivityLogs = {
|
|
currentTask.id: today.logsByTask[currentTask.id] ?? []
|
|
};
|
|
|
|
final rows = buildDayRows(
|
|
userId: programmerId,
|
|
day: today.day,
|
|
tasks: [currentTask],
|
|
logsByTask: currentTaskActivityLogs,
|
|
workLogs: currentTaskLogs,
|
|
projectNames: projectNames,
|
|
profileNames: profileNames,
|
|
now: now,
|
|
);
|
|
|
|
return Duration(seconds: totalSeconds(rows));
|
|
}
|
|
|
|
/// Build helped edges (where programmer helped others)
|
|
List<HelpEdge> _buildHelpedEdges(
|
|
String programmerId,
|
|
List<ProgrammerTaskWorkLog> workLogs,
|
|
Map<String, ProgrammerTask> taskById,
|
|
DateTime day,
|
|
) {
|
|
final edges = <HelpEdge>[];
|
|
|
|
for (final log in workLogs) {
|
|
// Filter: author == programmer AND author != assignee AND minutes > 0 AND workDate == day
|
|
if (log.authorId != programmerId) continue;
|
|
|
|
final task = taskById[log.taskId];
|
|
if (task == null) continue;
|
|
|
|
if (log.authorId == task.assigneeId) continue;
|
|
if (log.minutes == null || log.minutes! <= 0) continue;
|
|
if (log.workDate != day) continue;
|
|
|
|
edges.add(HelpEdge(
|
|
helperId: log.authorId,
|
|
assigneeId: task.assigneeId ?? '',
|
|
taskId: log.taskId,
|
|
taskTitle: task.title,
|
|
minutes: log.minutes!,
|
|
at: log.createdAt,
|
|
));
|
|
}
|
|
|
|
// Sort by createdAt newest first
|
|
edges.sort((a, b) => b.at.compareTo(a.at));
|
|
return edges;
|
|
}
|
|
|
|
/// Build helpedBy edges (where others helped programmer)
|
|
List<HelpEdge> _buildHelpedByEdges(
|
|
String programmerId,
|
|
List<ProgrammerTaskWorkLog> workLogs,
|
|
Map<String, ProgrammerTask> taskById,
|
|
DateTime day,
|
|
) {
|
|
final edges = <HelpEdge>[];
|
|
|
|
for (final log in workLogs) {
|
|
final task = taskById[log.taskId];
|
|
if (task == null) continue;
|
|
|
|
// Filter: task assigned to programmer AND author != programmer AND minutes > 0 AND workDate == day
|
|
if (task.assigneeId != programmerId) continue;
|
|
|
|
if (log.authorId == programmerId) continue;
|
|
if (log.minutes == null || log.minutes! <= 0) continue;
|
|
if (log.workDate != day) continue;
|
|
|
|
edges.add(HelpEdge(
|
|
helperId: log.authorId,
|
|
assigneeId: programmerId,
|
|
taskId: log.taskId,
|
|
taskTitle: task.title,
|
|
minutes: log.minutes!,
|
|
at: log.createdAt,
|
|
));
|
|
}
|
|
|
|
// Sort by createdAt newest first
|
|
edges.sort((a, b) => b.at.compareTo(a.at));
|
|
return edges;
|
|
}
|