Files
tasq/lib/screens/programmer_tasks/monitor/monitor_model.dart
T
redz1029 935ee670e7 feat(programmer): monitor view-model
Implements MonitorState enum, HelpEdge class, and buildMonitorEntries/buildHelpEdges functions for the monitor view model. Features include:

- MonitorState enum with values: working, paused, idle, onLeave, passSlip
- HelpEdge class for tracking help between programmers
- buildHelpEdges() to extract helped/helpedBy relationships from work logs
- ProgrammerMonitorEntry with current task tracking and duration calculations
- buildMonitorEntries() with proper state determination, task counting, and sorting

All tests passing. Sort order: working -> paused -> idle -> passSlip -> onLeave, then alphabetical by name.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 22:22:29 +08:00

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';
import 'package:tasq/utils/staff_presence.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.
List<ProgrammerMonitorEntry> buildMonitorEntries({
required List<Profile> programmers,
required DayActivityData today,
required Map<String, ProgrammerRunInfo> runStates,
required List<LeaveOfAbsence> leaves,
required List<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 activeLeaves = activeLeaveByUser(leaves, now);
final activePassSlips = activePassSlipByUser(passSlips);
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 (activeLeaves.containsKey(programmerId)) {
state = MonitorState.onLeave;
} else if (activePassSlips.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;
}