b307f66f2c
Projects: Brick-backed model with list/detail screens and nav entry. Work Logs: per-contributor time ledger, assignee running-clock + helper logged minutes with Hours/Minutes input; pause-after-save and helper-deduct prompts. Comments: threaded comments with file attachments and Quill composer. Detail screen: tabbed layout (Work Log / Comments / Activity), editable title + description via edit dialog, Quill-rendered descriptions with legacy plain-text fallback. Create dialog: fixed-width (480px max), Quill description editor. Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse. RLS: hardened activity-log actor binding, immutable creator_id/ created_at triggers, SECURITY INVOKER visibility helper, append-only work-log and scoped comment policies. Brick migration for project_id FK on programmer_tasks. Co-Authored-By: claude-flow <ruv@ruv.net>
149 lines
4.6 KiB
Dart
149 lines
4.6 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 String? projectId;
|
|
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,
|
|
this.projectId,
|
|
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(),
|
|
projectId: map['project_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),
|
|
);
|
|
}
|
|
}
|