Files
tasq/lib/providers/programmer_projects_provider.dart
T
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

123 lines
4.0 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.dart';
import '../brick/cache_helpers.dart';
import '../models/programmer_project.model.dart';
import 'profile_provider.dart';
import 'realtime_controller.dart';
import 'stream_recovery.dart';
import 'supabase_provider.dart';
void _sortProjects(List<ProgrammerProject> projects) {
projects.sort((a, b) {
// Active before archived, then alphabetical.
if (a.isArchived != b.isArchived) return a.isArchived ? 1 : -1;
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
});
}
/// All programmer projects (offline-readable via Brick cache).
final programmerProjectsProvider = StreamProvider<List<ProgrammerProject>>((ref) {
final userId = ref.watch(currentUserIdProvider);
if (userId == null) return const Stream.empty();
final client = ref.watch(supabaseClientProvider);
final wrapper = StreamRecoveryWrapper<ProgrammerProject>(
stream: client
.from('programmer_projects')
.stream(primaryKey: ['id']).order('created_at', ascending: false),
onPollData: () async {
final data = await client
.from('programmer_projects')
.select()
.order('created_at', ascending: false);
return data.map(ProgrammerProject.fromMap).toList();
},
fromMap: ProgrammerProject.fromMap,
channelName: 'programmer_projects',
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
onOfflineData: () async {
final all = await cachedListFromBrick<ProgrammerProject>();
_sortProjects(all);
return all;
},
onCacheMirror: (rows) =>
mirrorBatchToBrick<ProgrammerProject>(rows, tag: 'programmer_projects'),
);
ref.onDispose(wrapper.dispose);
return wrapper.stream.map((result) {
final list = List<ProgrammerProject>.from(result.data);
_sortProjects(list);
return list;
});
});
/// Active projects only — used by the task form's project picker.
final activeProgrammerProjectsProvider = Provider<List<ProgrammerProject>>((ref) {
final all = ref.watch(programmerProjectsProvider).valueOrNull ?? const [];
return all.where((p) => !p.isArchived).toList();
});
final programmerProjectByIdProvider =
Provider.family<ProgrammerProject?, String>((ref, id) {
final projects = ref.watch(programmerProjectsProvider).valueOrNull;
if (projects == null) return null;
try {
return projects.firstWhere((p) => p.id == id);
} catch (_) {
return null;
}
});
final programmerProjectsControllerProvider =
Provider<ProgrammerProjectsController>((ref) {
return ProgrammerProjectsController(ref.watch(supabaseClientProvider));
});
/// Projects are created/edited online (inline from the task form or the
/// projects screen). Reads fall back to the Brick cache when offline.
class ProgrammerProjectsController {
ProgrammerProjectsController(this._client);
final dynamic _client;
Future<String> createProject({
required String name,
String? description,
}) async {
final userId = _client.auth.currentUser?.id;
if (userId == null) throw Exception('Not authenticated');
final id = const Uuid().v4();
await _client.from('programmer_projects').insert({
'id': id,
'name': name,
'description': ?description,
'creator_id': userId,
});
return id;
}
Future<void> updateProject({
required String projectId,
String? name,
String? description,
}) async {
final updates = <String, dynamic>{};
if (name != null) updates['name'] = name;
if (description != null) updates['description'] = description;
if (updates.isEmpty) return;
await _client.from('programmer_projects').update(updates).eq('id', projectId);
}
Future<void> setArchived({
required String projectId,
required bool archived,
}) async {
await _client.from('programmer_projects').update({
'status': archived
? ProgrammerProjectStatus.archived
: ProgrammerProjectStatus.active,
}).eq('id', projectId);
}
}