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>
155 lines
5.8 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|