Add Programmer Tasks module (work-log + time tracking)

Separate feature for the `programmer` role to record dev work by category,
distinct from the IT help-desk `tasks` feature. Modeled on the IT Service
Requests pattern (Brick offline-first model -> Riverpod stream/controller ->
list/detail screens); seed of a future Project Management workflow.

- programmer_tasks + programmer_task_activity_logs (category enum, RLS,
  realtime) via supabase migration
- single assignee, self-logged or lead-assigned; assign/reassign logged
- pause-on-switch: starting a task while another runs prompts to pause the
  running one, keeping one active task per user for accurate time attribution
- worked-duration math shared with help-desk tasks via a generic core in
  task_duration.dart (existing callers unchanged)
- list distinguishes running vs paused with a per-task Resume shortcut
- accessible dialog labels; route /programmer-tasks + nav gated to admin/programmer
- unit/widget tests: controller lifecycle, duration math, pause-switch dialog

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-27 01:13:33 +08:00
parent e7706058b7
commit 627645056b
18 changed files with 3214 additions and 20 deletions
+24
View File
@@ -30,6 +30,8 @@ import '../widgets/app_shell.dart';
import '../screens/teams/teams_screen.dart';
import '../screens/it_service_requests/it_service_requests_list_screen.dart';
import '../screens/it_service_requests/it_service_request_detail_screen.dart';
import '../screens/programmer_tasks/programmer_tasks_list_screen.dart';
import '../screens/programmer_tasks/programmer_task_detail_screen.dart';
import '../screens/network_map/network_map_overview_screen.dart';
import '../screens/network_map/network_map_site_screen.dart';
import '../screens/network_map/network_map_device_screen.dart';
@@ -93,6 +95,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
role == 'it_staff' ||
role == 'dispatcher' ||
role == 'programmer';
final isProgrammerTasksRoute =
state.matchedLocation.startsWith('/programmer-tasks');
final hasProgrammerTasksAccess =
role == 'admin' || role == 'programmer';
if (!isSignedIn && !isAuthRoute) {
return '/login';
@@ -125,6 +131,9 @@ final appRouterProvider = Provider<GoRouter>((ref) {
if (isNetworkMapRoute && !hasNetworkMapAccess) {
return '/tickets';
}
if (isProgrammerTasksRoute && !hasProgrammerTasksAccess) {
return '/tickets';
}
// Attendance & Whereabouts: not accessible to standard users
final isStandardOnly = role == 'standard';
final isAttendanceRoute =
@@ -207,6 +216,21 @@ final appRouterProvider = Provider<GoRouter>((ref) {
),
],
),
GoRoute(
path: '/programmer-tasks',
pageBuilder: _page(const ProgrammerTasksListScreen()),
routes: [
GoRoute(
path: ':id',
pageBuilder: (context, state) => M3ContainerTransformPage(
key: state.pageKey,
child: ProgrammerTaskDetailScreen(
taskId: state.pathParameters['id'] ?? '',
),
),
),
],
),
GoRoute(
path: '/announcements',
pageBuilder: _page(const AnnouncementsScreen()),