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>
123 lines
4.0 KiB
Dart
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);
|
|
}
|
|
}
|