627645056b
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>
146 lines
4.5 KiB
Dart
146 lines
4.5 KiB
Dart
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),
|
|
);
|
|
}
|
|
}
|