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>
75 lines
2.1 KiB
Dart
75 lines
2.1 KiB
Dart
import 'dart:convert';
|
|
|
|
import '../utils/app_time.dart';
|
|
|
|
/// One file attached to a comment (stored in the task_attachments bucket).
|
|
class ProgrammerCommentAttachment {
|
|
final String path;
|
|
final String name;
|
|
|
|
const ProgrammerCommentAttachment({required this.path, required this.name});
|
|
|
|
Map<String, dynamic> toJson() => {'path': path, 'name': name};
|
|
|
|
factory ProgrammerCommentAttachment.fromJson(Map<String, dynamic> json) {
|
|
return ProgrammerCommentAttachment(
|
|
path: json['path']?.toString() ?? '',
|
|
name: json['name']?.toString() ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A comment on a programmer task (plain, online-first model). Any user who can
|
|
/// see the task may comment and attach files, whether or not they are the
|
|
/// assignee.
|
|
class ProgrammerTaskComment {
|
|
final String id;
|
|
final String taskId;
|
|
final String authorId;
|
|
final String body;
|
|
final List<ProgrammerCommentAttachment> attachments;
|
|
final DateTime createdAt;
|
|
|
|
ProgrammerTaskComment({
|
|
required this.id,
|
|
required this.taskId,
|
|
required this.authorId,
|
|
required this.body,
|
|
this.attachments = const [],
|
|
required this.createdAt,
|
|
});
|
|
|
|
factory ProgrammerTaskComment.fromMap(Map<String, dynamic> map) {
|
|
return ProgrammerTaskComment(
|
|
id: map['id'].toString(),
|
|
taskId: map['task_id'].toString(),
|
|
authorId: map['author_id'].toString(),
|
|
body: map['body'] as String? ?? '',
|
|
attachments: _parseAttachments(map['attachments']),
|
|
createdAt: AppTime.parse(map['created_at'] as String),
|
|
);
|
|
}
|
|
|
|
static List<ProgrammerCommentAttachment> _parseAttachments(dynamic raw) {
|
|
List<dynamic> list;
|
|
if (raw is List) {
|
|
list = raw;
|
|
} else if (raw is String && raw.isNotEmpty) {
|
|
try {
|
|
final decoded = jsonDecode(raw);
|
|
list = decoded is List ? decoded : const [];
|
|
} catch (_) {
|
|
list = const [];
|
|
}
|
|
} else {
|
|
return const [];
|
|
}
|
|
return list
|
|
.whereType<Map>()
|
|
.map((e) => ProgrammerCommentAttachment.fromJson(
|
|
e.map((k, v) => MapEntry(k.toString(), v)),
|
|
))
|
|
.toList();
|
|
}
|
|
}
|