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:
@@ -460,13 +460,20 @@ List<NavSection> _buildSections(String role) {
|
||||
icon: Icons.task_outlined,
|
||||
selectedIcon: Icons.task,
|
||||
),
|
||||
if (role == 'admin' || role == 'programmer')
|
||||
if (role == 'admin' || role == 'programmer') ...[
|
||||
NavItem(
|
||||
label: 'Programmer Tasks',
|
||||
route: '/programmer-tasks',
|
||||
icon: Icons.developer_mode_outlined,
|
||||
selectedIcon: Icons.developer_mode,
|
||||
),
|
||||
NavItem(
|
||||
label: 'Projects',
|
||||
route: '/programmer-projects',
|
||||
icon: Icons.folder_outlined,
|
||||
selectedIcon: Icons.folder,
|
||||
),
|
||||
],
|
||||
NavItem(
|
||||
label: 'IT Service Requests',
|
||||
route: '/it-service-requests',
|
||||
@@ -723,6 +730,7 @@ String _routeToTitle(String location) {
|
||||
'/tickets' => 'Tickets',
|
||||
'/tasks' => 'Tasks',
|
||||
'/programmer-tasks' => 'Programmer Tasks',
|
||||
'/programmer-projects' => 'Projects',
|
||||
'/it-service-requests' => 'IT Service Requests',
|
||||
'/network-map' => 'Network Map',
|
||||
'/notifications' => 'Notifications',
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
|
||||
/// Parses a stored Delta-JSON string into a [quill.QuillController], or returns
|
||||
/// an empty one. Callers own the controller and must dispose it.
|
||||
quill.QuillController quillControllerFromDelta(String? deltaJson) {
|
||||
if (deltaJson != null && deltaJson.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(deltaJson);
|
||||
if (decoded is List) {
|
||||
return quill.QuillController(
|
||||
document: quill.Document.fromJson(decoded),
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return quill.QuillController.basic();
|
||||
}
|
||||
|
||||
/// Serializes a controller's document to a Delta-JSON string for storage.
|
||||
String quillToDeltaJson(quill.QuillController controller) =>
|
||||
jsonEncode(controller.document.toDelta().toJson());
|
||||
|
||||
/// The controller's plain text (trimmed) — for empty checks and previews.
|
||||
String quillPlainText(quill.QuillController controller) =>
|
||||
controller.document.toPlainText().trim();
|
||||
|
||||
/// Bordered rich-text editor with a minimal toolbar (bold/italic/underline +
|
||||
/// bullet/numbered lists). The caller owns [controller] and disposes it.
|
||||
class QuillFieldEditor extends StatelessWidget {
|
||||
const QuillFieldEditor({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.placeholder,
|
||||
this.height = 140,
|
||||
});
|
||||
|
||||
final quill.QuillController controller;
|
||||
final String? placeholder;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
height: height,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: cs.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: quill.QuillSimpleToolbar(
|
||||
controller: controller,
|
||||
config: const quill.QuillSimpleToolbarConfig(
|
||||
multiRowsDisplay: false,
|
||||
showAlignmentButtons: false,
|
||||
showBackgroundColorButton: false,
|
||||
showCenterAlignment: false,
|
||||
showColorButton: false,
|
||||
showDividers: false,
|
||||
showFontFamily: false,
|
||||
showFontSize: false,
|
||||
showHeaderStyle: false,
|
||||
showIndent: false,
|
||||
showInlineCode: false,
|
||||
showLeftAlignment: false,
|
||||
showLink: false,
|
||||
showQuote: false,
|
||||
showRightAlignment: false,
|
||||
showSearchButton: false,
|
||||
showCodeBlock: false,
|
||||
showDirection: false,
|
||||
showJustifyAlignment: false,
|
||||
showListCheck: false,
|
||||
showSubscript: false,
|
||||
showSuperscript: false,
|
||||
showStrikeThrough: false,
|
||||
showSmallButton: false,
|
||||
showClearFormat: false,
|
||||
showRedo: false,
|
||||
showUndo: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: quill.QuillEditor.basic(
|
||||
controller: controller,
|
||||
config: quill.QuillEditorConfig(
|
||||
placeholder: placeholder,
|
||||
expands: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a stored value read-only. Interprets a Delta-JSON string as rich
|
||||
/// text; falls back to a plain [Text] for legacy plain-string values.
|
||||
class QuillReadOnly extends StatelessWidget {
|
||||
const QuillReadOnly(this.deltaOrText, {super.key, this.style});
|
||||
|
||||
final String deltaOrText;
|
||||
final TextStyle? style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trimmed = deltaOrText.trim();
|
||||
if (trimmed.startsWith('[')) {
|
||||
try {
|
||||
final decoded = jsonDecode(trimmed);
|
||||
if (decoded is List) {
|
||||
final controller = quill.QuillController(
|
||||
document: quill.Document.fromJson(decoded),
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
return IgnorePointer(
|
||||
child: quill.QuillEditor.basic(
|
||||
controller: controller,
|
||||
config: const quill.QuillEditorConfig(
|
||||
showCursor: false,
|
||||
autoFocus: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return Text(deltaOrText, style: style);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user