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>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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';
|
||||
|
||||
/// Status of a programmer project.
|
||||
class ProgrammerProjectStatus {
|
||||
static const active = 'active';
|
||||
static const archived = 'archived';
|
||||
|
||||
static const all = [active, archived];
|
||||
|
||||
static String label(String status) {
|
||||
switch (status) {
|
||||
case active:
|
||||
return 'Active';
|
||||
case archived:
|
||||
return 'Archived';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'programmer_projects'),
|
||||
)
|
||||
class ProgrammerProject extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String status;
|
||||
final String? creatorId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
ProgrammerProject({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.status,
|
||||
this.creatorId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
bool get isArchived => status == ProgrammerProjectStatus.archived;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ProgrammerProject &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
status == other.status &&
|
||||
updatedAt == other.updatedAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(id, name, status, updatedAt);
|
||||
|
||||
factory ProgrammerProject.fromMap(Map<String, dynamic> map) {
|
||||
return ProgrammerProject(
|
||||
id: map['id'].toString(),
|
||||
name: map['name'] as String? ?? '',
|
||||
description: map['description'] as String?,
|
||||
status: map['status'] as String? ?? ProgrammerProjectStatus.active,
|
||||
creatorId: map['creator_id']?.toString(),
|
||||
createdAt: AppTime.parse(map['created_at'] 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