diff --git a/lib/brick/adapters/notification_item_adapter.g.dart b/lib/brick/adapters/notification_item_adapter.g.dart index f467b6af..ec5a6851 100644 --- a/lib/brick/adapters/notification_item_adapter.g.dart +++ b/lib/brick/adapters/notification_item_adapter.g.dart @@ -23,6 +23,9 @@ Future _$NotificationItemFromSupabase( ? null : data['announcement_id'] as String?, messageId: data['message_id'] == null ? null : data['message_id'] as int?, + daySheetId: data['day_sheet_id'] == null + ? null + : data['day_sheet_id'] as String?, type: data['type'] as String, createdAt: DateTime.parse(data['created_at'] as String), readAt: data['read_at'] == null @@ -49,6 +52,7 @@ Future> _$NotificationItemToSupabase( 'it_service_request_id': instance.itServiceRequestId, 'announcement_id': instance.announcementId, 'message_id': instance.messageId, + 'day_sheet_id': instance.daySheetId, 'type': instance.type, 'created_at': instance.createdAt.toIso8601String(), 'read_at': instance.readAt?.toIso8601String(), @@ -77,6 +81,9 @@ Future _$NotificationItemFromSqlite( ? null : data['announcement_id'] as String?, messageId: data['message_id'] == null ? null : data['message_id'] as int?, + daySheetId: data['day_sheet_id'] == null + ? null + : data['day_sheet_id'] as String?, type: data['type'] as String, createdAt: DateTime.parse(data['created_at'] as String), readAt: data['read_at'] == null @@ -103,6 +110,7 @@ Future> _$NotificationItemToSqlite( 'it_service_request_id': instance.itServiceRequestId, 'announcement_id': instance.announcementId, 'message_id': instance.messageId, + 'day_sheet_id': instance.daySheetId, 'type': instance.type, 'created_at': instance.createdAt.toIso8601String(), 'read_at': instance.readAt?.toIso8601String(), @@ -160,6 +168,10 @@ class NotificationItemAdapter association: false, columnName: 'message_id', ), + 'daySheetId': const RuntimeSupabaseColumnDefinition( + association: false, + columnName: 'day_sheet_id', + ), 'type': const RuntimeSupabaseColumnDefinition( association: false, columnName: 'type', @@ -245,6 +257,12 @@ class NotificationItemAdapter iterable: false, type: int, ), + 'daySheetId': const RuntimeSqliteColumnDefinition( + association: false, + columnName: 'day_sheet_id', + iterable: false, + type: String, + ), 'type': const RuntimeSqliteColumnDefinition( association: false, columnName: 'type', diff --git a/lib/brick/brick.g.dart b/lib/brick/brick.g.dart index aa3e5263..b8f72a1e 100644 --- a/lib/brick/brick.g.dart +++ b/lib/brick/brick.g.dart @@ -11,6 +11,12 @@ import 'package:brick_supabase/brick_supabase.dart'; // ignore: unused_import, unused_shown_name, unnecessary_import import '../utils/app_time.dart'; // ignore: unused_import, unused_shown_name, unnecessary_import +import 'package:tasq/utils/programmer_daily_time.dart'; +// ignore: unused_import, unused_shown_name, unnecessary_import +import 'package:tasq/utils/programmer_day_rows.dart'; +// ignore: unused_import, unused_shown_name, unnecessary_import +import '../utils/programmer_daily_time.dart'; +// ignore: unused_import, unused_shown_name, unnecessary_import import 'package:tasq/models/team_member.dart';// GENERATED CODE DO NOT EDIT // ignore: unused_import import 'dart:convert'; diff --git a/lib/brick/db/20260928065256.migration.dart b/lib/brick/db/20260928065256.migration.dart new file mode 100644 index 00000000..73ee6ed8 --- /dev/null +++ b/lib/brick/db/20260928065256.migration.dart @@ -0,0 +1,36 @@ +// GENERATED CODE EDIT WITH CAUTION +// THIS FILE **WILL NOT** BE REGENERATED +// This file should be version controlled and can be manually edited. +part of 'schema.g.dart'; + +// While migrations are intelligently created, the difference between some commands, such as +// DropTable vs. RenameTable, cannot be determined. For this reason, please review migrations after +// they are created to ensure the correct inference was made. + +// The migration version must **always** mirror the file name + +const List _migration_20260928065256_up = [ + InsertColumn('day_sheet_id', Column.varchar, onTable: 'NotificationItem') +]; + +const List _migration_20260928065256_down = [ + DropColumn('day_sheet_id', onTable: 'NotificationItem') +]; + +// +// DO NOT EDIT BELOW THIS LINE +// + +@Migratable( + version: '20260928065256', + up: _migration_20260928065256_up, + down: _migration_20260928065256_down, +) +class Migration20260928065256 extends Migration { + const Migration20260928065256() + : super( + version: 20260928065256, + up: _migration_20260928065256_up, + down: _migration_20260928065256_down, + ); +} diff --git a/lib/brick/db/schema.g.dart b/lib/brick/db/schema.g.dart index 1ebf7fa4..3484fb06 100644 --- a/lib/brick/db/schema.g.dart +++ b/lib/brick/db/schema.g.dart @@ -1,22 +1,24 @@ // GENERATED CODE DO NOT EDIT // This file should be version controlled import 'package:brick_sqlite/db.dart'; -part '20260927030348.migration.dart'; +part '20260928065256.migration.dart'; part '20260411041808.migration.dart'; part '20260411042152.migration.dart'; part '20260926162926.migration.dart'; +part '20260927030348.migration.dart'; /// All intelligently-generated migrations from all `@Migratable` classes on disk final migrations = { - const Migration20260927030348(), + const Migration20260928065256(), const Migration20260411041808(), const Migration20260411042152(), const Migration20260926162926(), + const Migration20260927030348(), }; /// A consumable database structure including the latest generated migration. final schema = Schema( - 20260926162926, + 20260927030348, generatorVersion: 1, tables: { SchemaTable( @@ -271,6 +273,7 @@ final schema = Schema( SchemaColumn('it_service_request_id', Column.varchar), SchemaColumn('announcement_id', Column.varchar), SchemaColumn('message_id', Column.integer), + SchemaColumn('day_sheet_id', Column.varchar), SchemaColumn('type', Column.varchar), SchemaColumn('created_at', Column.datetime), SchemaColumn('read_at', Column.datetime), diff --git a/lib/models/notification_item.model.dart b/lib/models/notification_item.model.dart index 9d4c2ca9..f0fc8c04 100644 --- a/lib/models/notification_item.model.dart +++ b/lib/models/notification_item.model.dart @@ -19,6 +19,8 @@ class NotificationItem extends OfflineFirstWithSupabaseModel { final String? itServiceRequestId; final String? announcementId; final int? messageId; + @Sqlite(name: 'day_sheet_id') + final String? daySheetId; final String type; final DateTime createdAt; final DateTime? readAt; @@ -38,6 +40,7 @@ class NotificationItem extends OfflineFirstWithSupabaseModel { required this.itServiceRequestId, this.announcementId, required this.messageId, + this.daySheetId, required this.type, required this.createdAt, required this.readAt, @@ -55,6 +58,7 @@ class NotificationItem extends OfflineFirstWithSupabaseModel { itServiceRequestId: map['it_service_request_id'] as String?, announcementId: map['announcement_id'] as String?, messageId: map['message_id'] as int?, + daySheetId: map['day_sheet_id'] as String?, type: map['type'] as String? ?? 'mention', createdAt: AppTime.parse(map['created_at'] as String), readAt: map['read_at'] == null diff --git a/lib/screens/notifications/notifications_screen.dart b/lib/screens/notifications/notifications_screen.dart index 825e548d..c5c7d801 100644 --- a/lib/screens/notifications/notifications_screen.dart +++ b/lib/screens/notifications/notifications_screen.dart @@ -16,6 +16,48 @@ import '../../widgets/responsive_body.dart'; import '../../theme/app_surfaces.dart'; import '../../theme/m3_motion.dart'; +String notificationTitle(String type, String actorName) { + return switch (type) { + 'assignment' => '$actorName assigned you', + 'created' => '$actorName created a new item', + 'swap_request' => '$actorName requested a shift swap', + 'swap_update' => '$actorName updated a swap request', + 'announcement' => '$actorName posted an announcement', + 'announcement_comment' => '$actorName commented on an announcement', + 'it_job_reminder' => 'IT Job submission reminder', + 'isr_approved' => 'IT Service Request approved', + 'isr_status_changed' => 'IT Service Request status updated', + 'isr_assigned' => '$actorName assigned you an IT Service Request', + 'day_sheet_submitted' => '$actorName submitted a day sheet', + 'day_sheet_disapproved' => 'Day sheet disapproved', + 'day_sheet_justified' => '$actorName justified a day sheet', + 'day_sheet_approved' => 'Day sheet approved', + 'day_sheet_digest' => 'Day sheets awaiting review', + _ => '$actorName mentioned you', + }; +} + +IconData notificationIcon(String type) { + return switch (type) { + 'assignment' => Icons.assignment_ind_outlined, + 'created' => Icons.campaign_outlined, + 'swap_request' => Icons.swap_horiz, + 'swap_update' => Icons.update, + 'announcement' => Icons.campaign, + 'announcement_comment' => Icons.comment_outlined, + 'it_job_reminder' => Icons.print, + 'isr_approved' => Icons.check_circle_outline, + 'isr_status_changed' => Icons.sync_alt_outlined, + 'isr_assigned' => Icons.engineering_outlined, + 'day_sheet_submitted' => Icons.assignment_turned_in_outlined, + 'day_sheet_disapproved' => Icons.report_outlined, + 'day_sheet_justified' => Icons.reply_outlined, + 'day_sheet_approved' => Icons.verified_outlined, + 'day_sheet_digest' => Icons.inbox_outlined, + _ => Icons.alternate_email, + }; +} + class NotificationsScreen extends ConsumerStatefulWidget { const NotificationsScreen({super.key}); @@ -80,42 +122,11 @@ class _NotificationsScreenState extends ConsumerState { }; } - String _notificationTitle(String type, String actorName) { - return switch (type) { - 'assignment' => '$actorName assigned you', - 'created' => '$actorName created a new item', - 'swap_request' => '$actorName requested a shift swap', - 'swap_update' => '$actorName updated a swap request', - 'announcement' => '$actorName posted an announcement', - 'announcement_comment' => '$actorName commented on an announcement', - 'it_job_reminder' => 'IT Job submission reminder', - 'isr_approved' => 'IT Service Request approved', - 'isr_status_changed' => 'IT Service Request status updated', - 'isr_assigned' => '$actorName assigned you an IT Service Request', - _ => '$actorName mentioned you', - }; - } - - IconData _notificationIcon(String type) { - return switch (type) { - 'assignment' => Icons.assignment_ind_outlined, - 'created' => Icons.campaign_outlined, - 'swap_request' => Icons.swap_horiz, - 'swap_update' => Icons.update, - 'announcement' => Icons.campaign, - 'announcement_comment' => Icons.comment_outlined, - 'it_job_reminder' => Icons.print, - 'isr_approved' => Icons.check_circle_outline, - 'isr_status_changed' => Icons.sync_alt_outlined, - 'isr_assigned' => Icons.engineering_outlined, - _ => Icons.alternate_email, - }; - } - Future _handleTap(BuildContext context, NotificationItem item) async { final ticketId = item.ticketId; final taskId = item.taskId; final isrId = item.itServiceRequestId; + final daySheetId = item.daySheetId; if (ticketId != null) { await ref @@ -136,6 +147,10 @@ class _NotificationsScreenState extends ConsumerState { context.go('/tickets/$ticketId'); } else if (isrId != null) { context.go('/it-service-requests/$isrId'); + } else if (item.type == 'day_sheet_digest') { + context.go('/programmer-tasks?tab=approvals'); + } else if (daySheetId != null) { + context.go('/programmer-tasks/day-sheets/$daySheetId'); } } @@ -248,8 +263,8 @@ class _NotificationsScreenState extends ConsumerState { animDelay: Duration( milliseconds: animIndexes[index].clamp(0, 6) * 50, ), - title: _notificationTitle(item.type, actorName), - icon: _notificationIcon(item.type), + title: notificationTitle(item.type, actorName), + icon: notificationIcon(item.type), iconContainerColor: _iconContainerColor(context, item.type), iconForegroundColor: @@ -331,6 +346,7 @@ class _NotificationCard extends StatelessWidget { if (item.announcementId != null) return 'Announcement'; if (item.taskId != null) return taskTitle ?? 'Task'; if (item.itServiceRequestId != null) return 'IT Service Request'; + if (item.daySheetId != null) return 'Day Sheet'; return ticketSubject ?? 'Ticket'; } diff --git a/lib/services/notification_bridge.dart b/lib/services/notification_bridge.dart index 93c12207..d00cab2b 100644 --- a/lib/services/notification_bridge.dart +++ b/lib/services/notification_bridge.dart @@ -106,6 +106,11 @@ class _NotificationBridgeState extends ConsumerState 'isr_approved' => 'IT Service Request approved', 'isr_status_changed' => 'IT Service Request updated', 'isr_assigned' => 'New IT Service Request assigned', + 'day_sheet_submitted' => 'Day sheet submitted', + 'day_sheet_disapproved' => 'Day sheet disapproved', + 'day_sheet_justified' => 'Day sheet justified', + 'day_sheet_approved' => 'Day sheet approved', + 'day_sheet_digest' => 'Day sheets awaiting review', _ => 'New notification', }; @@ -121,7 +126,13 @@ class _NotificationBridgeState extends ConsumerState action: SnackBarAction( label: 'View', onPressed: () => _goToDetail( - route: item.announcementId != null ? '/announcements' : null, + route: item.announcementId != null + ? '/announcements' + : item.type == 'day_sheet_digest' + ? '/programmer-tasks?tab=approvals' + : item.daySheetId != null + ? '/programmer-tasks/day-sheets/${item.daySheetId}' + : null, taskId: item.taskId, ticketId: item.ticketId, itServiceRequestId: item.itServiceRequestId, diff --git a/test/notification_titles_test.dart b/test/notification_titles_test.dart new file mode 100644 index 00000000..c8db26fa --- /dev/null +++ b/test/notification_titles_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tasq/screens/notifications/notifications_screen.dart'; + +void main() { + group('notificationTitle', () { + test('assignment guard (existing case unchanged)', () { + expect(notificationTitle('assignment', 'Alice'), 'Alice assigned you'); + }); + + test('day_sheet_submitted', () { + expect( + notificationTitle('day_sheet_submitted', 'Bob'), + 'Bob submitted a day sheet', + ); + }); + + test('day_sheet_disapproved', () { + expect( + notificationTitle('day_sheet_disapproved', 'Alice'), + 'Day sheet disapproved', + ); + }); + + test('day_sheet_justified', () { + expect( + notificationTitle('day_sheet_justified', 'Bob'), + 'Bob justified a day sheet', + ); + }); + + test('day_sheet_approved', () { + expect( + notificationTitle('day_sheet_approved', 'Alice'), + 'Day sheet approved', + ); + }); + + test('day_sheet_digest', () { + expect( + notificationTitle('day_sheet_digest', 'System'), + 'Day sheets awaiting review', + ); + }); + }); + + group('notificationIcon', () { + test('assignment guard (existing case unchanged)', () { + expect(notificationIcon('assignment'), Icons.assignment_ind_outlined); + }); + + test('day_sheet_submitted', () { + expect( + notificationIcon('day_sheet_submitted'), + Icons.assignment_turned_in_outlined, + ); + }); + + test('day_sheet_disapproved', () { + expect( + notificationIcon('day_sheet_disapproved'), + Icons.report_outlined, + ); + }); + + test('day_sheet_justified', () { + expect( + notificationIcon('day_sheet_justified'), + Icons.reply_outlined, + ); + }); + + test('day_sheet_approved', () { + expect( + notificationIcon('day_sheet_approved'), + Icons.verified_outlined, + ); + }); + + test('day_sheet_digest', () { + expect( + notificationIcon('day_sheet_digest'), + Icons.inbox_outlined, + ); + }); + }); +}