Files
tasq/lib/models/programmer_task.model.dart
redz1029 b307f66f2c Add PM extension (Projects, Work Logs, Comments) + Quill rich-text UI
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>
2026-09-27 16:06:26 +08:00

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),
);
}
}