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,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../models/programmer_project.model.dart';
|
||||
import '../../models/programmer_task.model.dart';
|
||||
import '../../providers/programmer_projects_provider.dart';
|
||||
import '../../providers/programmer_tasks_provider.dart';
|
||||
import '../../theme/app_status_colors.dart';
|
||||
import '../../widgets/app_section_header.dart';
|
||||
import '../../widgets/m3_card.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../widgets/status_pill.dart';
|
||||
import '../programmer_tasks/programmer_task_create.dart';
|
||||
import '../programmer_tasks/programmer_task_status_kind.dart';
|
||||
|
||||
class ProgrammerProjectDetailScreen extends ConsumerWidget {
|
||||
const ProgrammerProjectDetailScreen({super.key, required this.projectId});
|
||||
|
||||
final String projectId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final project = ref.watch(programmerProjectByIdProvider(projectId));
|
||||
final tasks = (ref.watch(programmerTasksProvider).valueOrNull ?? const [])
|
||||
.where((t) => t.projectId == projectId)
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(project?.name ?? 'Project'),
|
||||
actions: [
|
||||
if (project != null)
|
||||
IconButton(
|
||||
tooltip: project.isArchived ? 'Unarchive' : 'Archive',
|
||||
icon: Icon(project.isArchived
|
||||
? Icons.unarchive_outlined
|
||||
: Icons.archive_outlined),
|
||||
onPressed: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await ref.read(programmerProjectsControllerProvider).setArchived(
|
||||
projectId: projectId,
|
||||
archived: !project.isArchived,
|
||||
);
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
project.isArchived ? 'Project unarchived' : 'Project archived'),
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: project == null
|
||||
? null
|
||||
: FloatingActionButton.extended(
|
||||
heroTag: 'project_new_task',
|
||||
onPressed: () => showCreateProgrammerTaskDialog(
|
||||
context,
|
||||
ref,
|
||||
initialProjectId: projectId,
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('New Task'),
|
||||
),
|
||||
body: project == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ResponsiveBody(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
children: [
|
||||
M3Card.elevated(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(project.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.w700)),
|
||||
),
|
||||
StatusPill.kind(
|
||||
project.isArchived
|
||||
? StatusKind.neutral
|
||||
: StatusKind.success,
|
||||
label: ProgrammerProjectStatus.label(
|
||||
project.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (project.description != null &&
|
||||
project.description!.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(project.description!,
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
AppSectionHeader('Tasks (${tasks.length})'),
|
||||
if (tasks.isEmpty)
|
||||
Text('No tasks in this project yet.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
else
|
||||
for (final t in tasks) _ProjectTaskTile(task: t),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProjectTaskTile extends StatelessWidget {
|
||||
const _ProjectTaskTile({required this.task});
|
||||
|
||||
final ProgrammerTask task;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: M3Card.outlined(
|
||||
onTap: () => context.go('/programmer-tasks/${task.id}'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(task.title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatusPill.kind(
|
||||
programmerTaskStatusKind(task.status),
|
||||
label: ProgrammerTaskStatus.label(task.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user