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:
2026-09-27 16:06:26 +08:00
parent 627645056b
commit b307f66f2c
33 changed files with 2960 additions and 216 deletions
@@ -0,0 +1,231 @@
// GENERATED CODE DO NOT EDIT
part of '../brick.g.dart';
Future<ProgrammerProject> _$ProgrammerProjectFromSupabase(
Map<String, dynamic> data, {
required SupabaseProvider provider,
OfflineFirstWithSupabaseRepository? repository,
}) async {
return ProgrammerProject(
id: data['id'] as String,
name: data['name'] as String,
description: data['description'] == null
? null
: data['description'] as String?,
status: data['status'] as String,
creatorId: data['creator_id'] == null
? null
: data['creator_id'] as String?,
createdAt: DateTime.parse(data['created_at'] as String),
updatedAt: DateTime.parse(data['updated_at'] as String),
);
}
Future<Map<String, dynamic>> _$ProgrammerProjectToSupabase(
ProgrammerProject instance, {
required SupabaseProvider provider,
OfflineFirstWithSupabaseRepository? repository,
}) async {
return {
'id': instance.id,
'name': instance.name,
'description': instance.description,
'status': instance.status,
'creator_id': instance.creatorId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'is_archived': instance.isArchived,
};
}
Future<ProgrammerProject> _$ProgrammerProjectFromSqlite(
Map<String, dynamic> data, {
required SqliteProvider provider,
OfflineFirstWithSupabaseRepository? repository,
}) async {
return ProgrammerProject(
id: data['id'] as String,
name: data['name'] as String,
description: data['description'] == null
? null
: data['description'] as String?,
status: data['status'] as String,
creatorId: data['creator_id'] == null
? null
: data['creator_id'] as String?,
createdAt: DateTime.parse(data['created_at'] as String),
updatedAt: DateTime.parse(data['updated_at'] as String),
)..primaryKey = data['_brick_id'] as int;
}
Future<Map<String, dynamic>> _$ProgrammerProjectToSqlite(
ProgrammerProject instance, {
required SqliteProvider provider,
OfflineFirstWithSupabaseRepository? repository,
}) async {
return {
'id': instance.id,
'name': instance.name,
'description': instance.description,
'status': instance.status,
'creator_id': instance.creatorId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'is_archived': instance.isArchived ? 1 : 0,
};
}
/// Construct a [ProgrammerProject]
class ProgrammerProjectAdapter
extends OfflineFirstWithSupabaseAdapter<ProgrammerProject> {
ProgrammerProjectAdapter();
@override
final supabaseTableName = 'programmer_projects';
@override
final defaultToNull = true;
@override
final fieldsToSupabaseColumns = {
'id': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'id',
),
'name': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'name',
),
'description': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'description',
),
'status': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'status',
),
'creatorId': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'creator_id',
),
'createdAt': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'created_at',
),
'updatedAt': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'updated_at',
),
'isArchived': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'is_archived',
),
};
@override
final ignoreDuplicates = false;
@override
final uniqueFields = {};
@override
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
'primaryKey': const RuntimeSqliteColumnDefinition(
association: false,
columnName: '_brick_id',
iterable: false,
type: int,
),
'id': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'id',
iterable: false,
type: String,
),
'name': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'name',
iterable: false,
type: String,
),
'description': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'description',
iterable: false,
type: String,
),
'status': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'status',
iterable: false,
type: String,
),
'creatorId': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'creator_id',
iterable: false,
type: String,
),
'createdAt': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'created_at',
iterable: false,
type: DateTime,
),
'updatedAt': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'updated_at',
iterable: false,
type: DateTime,
),
'isArchived': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'is_archived',
iterable: false,
type: bool,
),
};
@override
Future<int?> primaryKeyByUniqueColumns(
ProgrammerProject instance,
DatabaseExecutor executor,
) async => instance.primaryKey;
@override
final String tableName = 'ProgrammerProject';
@override
Future<ProgrammerProject> fromSupabase(
Map<String, dynamic> input, {
required provider,
covariant OfflineFirstWithSupabaseRepository? repository,
}) async => await _$ProgrammerProjectFromSupabase(
input,
provider: provider,
repository: repository,
);
@override
Future<Map<String, dynamic>> toSupabase(
ProgrammerProject input, {
required provider,
covariant OfflineFirstWithSupabaseRepository? repository,
}) async => await _$ProgrammerProjectToSupabase(
input,
provider: provider,
repository: repository,
);
@override
Future<ProgrammerProject> fromSqlite(
Map<String, dynamic> input, {
required provider,
covariant OfflineFirstWithSupabaseRepository? repository,
}) async => await _$ProgrammerProjectFromSqlite(
input,
provider: provider,
repository: repository,
);
@override
Future<Map<String, dynamic>> toSqlite(
ProgrammerProject input, {
required provider,
covariant OfflineFirstWithSupabaseRepository? repository,
}) async => await _$ProgrammerProjectToSqlite(
input,
provider: provider,
repository: repository,
);
}
@@ -21,6 +21,9 @@ Future<ProgrammerTask> _$ProgrammerTaskFromSupabase(
creatorId: data['creator_id'] == null
? null
: data['creator_id'] as String?,
projectId: data['project_id'] == null
? null
: data['project_id'] as String?,
createdAt: DateTime.parse(data['created_at'] as String),
startedAt: data['started_at'] == null
? null
@@ -58,6 +61,7 @@ Future<Map<String, dynamic>> _$ProgrammerTaskToSupabase(
'priority': instance.priority,
'assignee_id': instance.assigneeId,
'creator_id': instance.creatorId,
'project_id': instance.projectId,
'created_at': instance.createdAt.toIso8601String(),
'started_at': instance.startedAt?.toIso8601String(),
'completed_at': instance.completedAt?.toIso8601String(),
@@ -88,6 +92,9 @@ Future<ProgrammerTask> _$ProgrammerTaskFromSqlite(
creatorId: data['creator_id'] == null
? null
: data['creator_id'] as String?,
projectId: data['project_id'] == null
? null
: data['project_id'] as String?,
createdAt: DateTime.parse(data['created_at'] as String),
startedAt: data['started_at'] == null
? null
@@ -125,6 +132,7 @@ Future<Map<String, dynamic>> _$ProgrammerTaskToSqlite(
'priority': instance.priority,
'assignee_id': instance.assigneeId,
'creator_id': instance.creatorId,
'project_id': instance.projectId,
'created_at': instance.createdAt.toIso8601String(),
'started_at': instance.startedAt?.toIso8601String(),
'completed_at': instance.completedAt?.toIso8601String(),
@@ -178,6 +186,10 @@ class ProgrammerTaskAdapter
association: false,
columnName: 'creator_id',
),
'projectId': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'project_id',
),
'createdAt': const RuntimeSupabaseColumnDefinition(
association: false,
columnName: 'created_at',
@@ -267,6 +279,12 @@ class ProgrammerTaskAdapter
iterable: false,
type: String,
),
'projectId': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'project_id',
iterable: false,
type: String,
),
'createdAt': const RuntimeSqliteColumnDefinition(
association: false,
columnName: 'created_at',
+4
View File
@@ -35,6 +35,7 @@ import '../models/notification_item.model.dart';
import '../models/office.model.dart';
import '../models/pass_slip.model.dart';
import '../models/profile.model.dart';
import '../models/programmer_project.model.dart';
import '../models/programmer_task.model.dart';
import '../models/programmer_task_activity_log.model.dart';
import '../models/service.model.dart';
@@ -58,6 +59,7 @@ part 'adapters/notification_item_adapter.g.dart';
part 'adapters/office_adapter.g.dart';
part 'adapters/pass_slip_adapter.g.dart';
part 'adapters/profile_adapter.g.dart';
part 'adapters/programmer_project_adapter.g.dart';
part 'adapters/programmer_task_adapter.g.dart';
part 'adapters/programmer_task_activity_log_adapter.g.dart';
part 'adapters/service_adapter.g.dart';
@@ -83,6 +85,7 @@ final Map<Type, SupabaseAdapter<SupabaseModel>> supabaseMappings = {
Office: OfficeAdapter(),
PassSlip: PassSlipAdapter(),
Profile: ProfileAdapter(),
ProgrammerProject: ProgrammerProjectAdapter(),
ProgrammerTask: ProgrammerTaskAdapter(),
ProgrammerTaskActivityLog: ProgrammerTaskActivityLogAdapter(),
Service: ServiceAdapter(),
@@ -110,6 +113,7 @@ final Map<Type, SqliteAdapter<SqliteModel>> sqliteMappings = {
Office: OfficeAdapter(),
PassSlip: PassSlipAdapter(),
Profile: ProfileAdapter(),
ProgrammerProject: ProgrammerProjectAdapter(),
ProgrammerTask: ProgrammerTaskAdapter(),
ProgrammerTaskActivityLog: ProgrammerTaskActivityLogAdapter(),
Service: ServiceAdapter(),
@@ -0,0 +1,54 @@
// GENERATED CODE EDIT WITH CAUTION
// THIS FILE **WILL NOT** BE REGENERATED
// This file should be version controlled and can be manually edited.
part of 'schema.g.dart';
// While migrations are intelligently created, the difference between some commands, such as
// DropTable vs. RenameTable, cannot be determined. For this reason, please review migrations after
// they are created to ensure the correct inference was made.
// The migration version must **always** mirror the file name
const List<MigrationCommand> _migration_20260927030348_up = [
InsertTable('ProgrammerProject'),
InsertColumn('id', Column.varchar, onTable: 'ProgrammerProject'),
InsertColumn('name', Column.varchar, onTable: 'ProgrammerProject'),
InsertColumn('description', Column.varchar, onTable: 'ProgrammerProject'),
InsertColumn('status', Column.varchar, onTable: 'ProgrammerProject'),
InsertColumn('creator_id', Column.varchar, onTable: 'ProgrammerProject'),
InsertColumn('created_at', Column.datetime, onTable: 'ProgrammerProject'),
InsertColumn('updated_at', Column.datetime, onTable: 'ProgrammerProject'),
InsertColumn('is_archived', Column.boolean, onTable: 'ProgrammerProject'),
InsertColumn('project_id', Column.varchar, onTable: 'ProgrammerTask')
];
const List<MigrationCommand> _migration_20260927030348_down = [
DropTable('ProgrammerProject'),
DropColumn('id', onTable: 'ProgrammerProject'),
DropColumn('name', onTable: 'ProgrammerProject'),
DropColumn('description', onTable: 'ProgrammerProject'),
DropColumn('status', onTable: 'ProgrammerProject'),
DropColumn('creator_id', onTable: 'ProgrammerProject'),
DropColumn('created_at', onTable: 'ProgrammerProject'),
DropColumn('updated_at', onTable: 'ProgrammerProject'),
DropColumn('is_archived', onTable: 'ProgrammerProject'),
DropColumn('project_id', onTable: 'ProgrammerTask')
];
//
// DO NOT EDIT BELOW THIS LINE
//
@Migratable(
version: '20260927030348',
up: _migration_20260927030348_up,
down: _migration_20260927030348_down,
)
class Migration20260927030348 extends Migration {
const Migration20260927030348()
: super(
version: 20260927030348,
up: _migration_20260927030348_up,
down: _migration_20260927030348_down,
);
}
+27 -3
View File
@@ -1,20 +1,22 @@
// GENERATED CODE DO NOT EDIT
// This file should be version controlled
import 'package:brick_sqlite/db.dart';
part '20260926162926.migration.dart';
part '20260927030348.migration.dart';
part '20260411041808.migration.dart';
part '20260411042152.migration.dart';
part '20260926162926.migration.dart';
/// All intelligently-generated migrations from all `@Migratable` classes on disk
final migrations = <Migration>{
const Migration20260926162926(),
const Migration20260927030348(),
const Migration20260411041808(),
const Migration20260411042152(),
const Migration20260926162926(),
};
/// A consumable database structure including the latest generated migration.
final schema = Schema(
20260411042152,
20260926162926,
generatorVersion: 1,
tables: <SchemaTable>{
SchemaTable(
@@ -336,6 +338,27 @@ final schema = Schema(
},
indices: <SchemaIndex>{},
),
SchemaTable(
'ProgrammerProject',
columns: <SchemaColumn>{
SchemaColumn(
'_brick_id',
Column.integer,
autoincrement: true,
nullable: false,
isPrimaryKey: true,
),
SchemaColumn('id', Column.varchar),
SchemaColumn('name', Column.varchar),
SchemaColumn('description', Column.varchar),
SchemaColumn('status', Column.varchar),
SchemaColumn('creator_id', Column.varchar),
SchemaColumn('created_at', Column.datetime),
SchemaColumn('updated_at', Column.datetime),
SchemaColumn('is_archived', Column.boolean),
},
indices: <SchemaIndex>{},
),
SchemaTable(
'ProgrammerTask',
columns: <SchemaColumn>{
@@ -354,6 +377,7 @@ final schema = Schema(
SchemaColumn('priority', Column.integer),
SchemaColumn('assignee_id', Column.varchar),
SchemaColumn('creator_id', Column.varchar),
SchemaColumn('project_id', Column.varchar),
SchemaColumn('created_at', Column.datetime),
SchemaColumn('started_at', Column.datetime),
SchemaColumn('completed_at', Column.datetime),