Add Programmer Tasks module (work-log + time tracking)
Separate feature for the `programmer` role to record dev work by category, distinct from the IT help-desk `tasks` feature. Modeled on the IT Service Requests pattern (Brick offline-first model -> Riverpod stream/controller -> list/detail screens); seed of a future Project Management workflow. - programmer_tasks + programmer_task_activity_logs (category enum, RLS, realtime) via supabase migration - single assignee, self-logged or lead-assigned; assign/reassign logged - pause-on-switch: starting a task while another runs prompts to pause the running one, keeping one active task per user for accurate time attribution - worked-duration math shared with help-desk tasks via a generic core in task_duration.dart (existing callers unchanged) - list distinguishes running vs paused with a per-task Resume shortcut - accessible dialog labels; route /programmer-tasks + nav gated to admin/programmer - unit/widget tests: controller lifecycle, duration math, pause-switch dialog Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
/// Work categories for a programmer task. Values are the TitleCase display
|
||||
/// strings stored directly in the `programmer_task_category` Postgres enum
|
||||
/// (same convention as the IT-support request_type / request_category enums).
|
||||
class ProgrammerTaskCategory {
|
||||
static const softwareSupport = 'Software Support';
|
||||
static const softwareDevelopment = 'Software Development';
|
||||
static const softwareEnhancement = 'Software Enhancement';
|
||||
static const bugFix = 'Bug Fix';
|
||||
static const meeting = 'Meeting';
|
||||
static const facilitateEvent = 'Facilitate Event';
|
||||
static const others = 'Others';
|
||||
|
||||
static const all = [
|
||||
softwareSupport,
|
||||
softwareDevelopment,
|
||||
softwareEnhancement,
|
||||
bugFix,
|
||||
meeting,
|
||||
facilitateEvent,
|
||||
others,
|
||||
];
|
||||
|
||||
/// Enum values already read as display labels, but this keeps the call site
|
||||
/// symmetric with other models and guards unknown values.
|
||||
static String label(String category) =>
|
||||
all.contains(category) ? category : category;
|
||||
}
|
||||
|
||||
/// Status lifecycle for a programmer task.
|
||||
///
|
||||
/// `queued -> in_progress -> completed | cancelled`. Pause/resume are recorded
|
||||
/// as activity-log events (not statuses): a paused task keeps `inProgress`.
|
||||
class ProgrammerTaskStatus {
|
||||
static const queued = 'queued';
|
||||
static const inProgress = 'in_progress';
|
||||
static const completed = 'completed';
|
||||
static const cancelled = 'cancelled';
|
||||
|
||||
static const all = [queued, inProgress, completed, cancelled];
|
||||
|
||||
static const terminal = [completed, cancelled];
|
||||
|
||||
static String label(String status) {
|
||||
switch (status) {
|
||||
case queued:
|
||||
return 'Queued';
|
||||
case inProgress:
|
||||
return 'In Progress';
|
||||
case completed:
|
||||
return 'Completed';
|
||||
case cancelled:
|
||||
return 'Cancelled';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'programmer_tasks'),
|
||||
)
|
||||
class ProgrammerTask extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final String category;
|
||||
final String status;
|
||||
final int priority;
|
||||
final String? assigneeId;
|
||||
final String? creatorId;
|
||||
final DateTime createdAt;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
final DateTime? cancelledAt;
|
||||
final String? cancellationReason;
|
||||
final DateTime updatedAt;
|
||||
|
||||
ProgrammerTask({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.category,
|
||||
required this.status,
|
||||
this.priority = 1,
|
||||
this.assigneeId,
|
||||
this.creatorId,
|
||||
required this.createdAt,
|
||||
this.startedAt,
|
||||
this.completedAt,
|
||||
this.cancelledAt,
|
||||
this.cancellationReason,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
bool get isTerminal => ProgrammerTaskStatus.terminal.contains(status);
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ProgrammerTask &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
status == other.status &&
|
||||
updatedAt == other.updatedAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(id, status, updatedAt);
|
||||
|
||||
factory ProgrammerTask.fromMap(Map<String, dynamic> map) {
|
||||
return ProgrammerTask(
|
||||
id: map['id'].toString(),
|
||||
title: map['title'] as String? ?? '',
|
||||
description: map['description'] as String?,
|
||||
category: map['category'] as String? ?? ProgrammerTaskCategory.others,
|
||||
status: map['status'] as String? ?? ProgrammerTaskStatus.queued,
|
||||
priority: (map['priority'] as num?)?.toInt() ?? 1,
|
||||
assigneeId: map['assignee_id']?.toString(),
|
||||
creatorId: map['creator_id']?.toString(),
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
startedAt: map['started_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['started_at'] as String),
|
||||
completedAt: map['completed_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['completed_at'] as String),
|
||||
cancelledAt: map['cancelled_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['cancelled_at'] as String),
|
||||
cancellationReason: map['cancellation_reason'] as String?,
|
||||
updatedAt: map['updated_at'] == null
|
||||
? AppTime.parse(map['created_at'] as String)
|
||||
: AppTime.parse(map['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user