Files
tasq/lib/screens/programmer_projects/programmer_project_detail_screen.dart
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

155 lines
5.8 KiB
Dart

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),
),
],
),
),
),
);
}
}