Skip to content

Commit 0fe0137

Browse files
committed
feat: add activity log screen and API integration
- Introduced a new Activity Log screen in the settings for users to review their audit trail. - Updated the router to include the activity log path, enhancing navigation within the app. - Implemented the AuditLogEntry and AuditLogPage classes to handle audit log data from the API. - Added a method in SettingsApi to fetch audit logs, improving the application's logging capabilities.
1 parent 2c354d2 commit 0fe0137

4 files changed

Lines changed: 509 additions & 1 deletion

File tree

mobile/lib/app/router.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import '../features/customers/presentation/customers_screen.dart';
1616
import '../features/debts/presentation/debt_detail_screen.dart';
1717
import '../features/debts/presentation/debts_screen.dart';
1818
import '../features/notifications/presentation/notifications_inbox_screen.dart';
19+
import '../features/settings/presentation/activity_log_screen.dart';
1920
import '../features/settings/presentation/connect_paystack_screen.dart';
2021
import '../features/settings/presentation/business_profile_screen.dart';
2122
import '../features/settings/presentation/settings_screen.dart';
@@ -36,6 +37,7 @@ enum AppRoute {
3637
businessProfile('/settings/business-profile'),
3738
staff('/settings/staff'),
3839
paystack('/settings/payments/paystack'),
40+
activity('/settings/activity'),
3941
customers('/customers'),
4042
customerDetail('/customers/:id'),
4143
debtDetail('/debts/:id'),
@@ -84,7 +86,8 @@ bool _isProtectedPath(String path) {
8486
bool _isOwnerOnlySettingsPath(String path) {
8587
return path == AppRoute.businessProfile.path ||
8688
path == AppRoute.staff.path ||
87-
path == AppRoute.paystack.path;
89+
path == AppRoute.paystack.path ||
90+
path == AppRoute.activity.path;
8891
}
8992

9093
Future<String?> _redirectGuard(Ref ref, GoRouterState state) async {
@@ -190,6 +193,10 @@ GoRouter createAppRouter(Ref ref) {
190193
path: AppRoute.paystack.path,
191194
builder: (context, state) => const ConnectPaystackScreen(),
192195
),
196+
GoRoute(
197+
path: AppRoute.activity.path,
198+
builder: (context, state) => const ActivityLogScreen(),
199+
),
193200
GoRoute(
194201
path: AppRoute.customers.path,
195202
builder: (context, state) => const CustomersScreen(),

mobile/lib/features/settings/data/settings_api.dart

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,62 @@ class PaystackModeState {
117117
}
118118
}
119119

120+
class AuditLogEntry {
121+
const AuditLogEntry({
122+
required this.auditLogId,
123+
required this.action,
124+
required this.createdAt,
125+
this.actorUserId,
126+
this.actorLabel,
127+
this.entityType,
128+
this.entityId,
129+
this.meta,
130+
});
131+
132+
final String auditLogId;
133+
final String? actorUserId;
134+
final String? actorLabel;
135+
final String action;
136+
final String? entityType;
137+
final String? entityId;
138+
final Map<String, dynamic>? meta;
139+
final DateTime createdAt;
140+
141+
factory AuditLogEntry.fromJson(Map<String, dynamic> json) => AuditLogEntry(
142+
auditLogId: json['audit_log_id'] as String,
143+
actorUserId: json['actor_user_id'] as String?,
144+
actorLabel: json['actor_label'] as String?,
145+
action: json['action'] as String,
146+
entityType: json['entity_type'] as String?,
147+
entityId: json['entity_id'] as String?,
148+
meta: json['meta'] is Map<String, dynamic>
149+
? json['meta'] as Map<String, dynamic>
150+
: null,
151+
createdAt: DateTime.parse(json['created_at'] as String),
152+
);
153+
}
154+
155+
class AuditLogPage {
156+
const AuditLogPage({required this.items, this.nextCursor});
157+
158+
final List<AuditLogEntry> items;
159+
final String? nextCursor;
160+
161+
factory AuditLogPage.fromJson(Map<String, dynamic> json) {
162+
final items = json['items'];
163+
if (items is! List) {
164+
throw const FormatException('Unexpected activity payload.');
165+
}
166+
return AuditLogPage(
167+
items: items
168+
.cast<Map<String, dynamic>>()
169+
.map(AuditLogEntry.fromJson)
170+
.toList(growable: false),
171+
nextCursor: json['next_cursor'] as String?,
172+
);
173+
}
174+
}
175+
120176
class SettingsApi {
121177
SettingsApi(this._apiClient, {KvCacheRepository? cache}) : _cache = cache;
122178

@@ -302,6 +358,26 @@ class SettingsApi {
302358
return PaystackConnectionSettings.fromJson(body);
303359
}
304360

361+
Future<AuditLogPage> fetchAuditLogs({
362+
int limit = 50,
363+
String? cursor,
364+
String? action,
365+
}) async {
366+
final response = await _apiClient.dio.get<dynamic>(
367+
'/audit-logs',
368+
queryParameters: {
369+
'limit': limit,
370+
if (cursor != null) 'cursor': cursor,
371+
if (action != null && action.trim().isNotEmpty) 'action': action.trim(),
372+
},
373+
);
374+
final body = response.data;
375+
if (body is! Map<String, dynamic>) {
376+
throw const FormatException('Unexpected activity payload.');
377+
}
378+
return AuditLogPage.fromJson(body);
379+
}
380+
305381
bool _isOfflineish(DioException e) {
306382
if (e.type == DioExceptionType.connectionError) return true;
307383
if (e.type == DioExceptionType.connectionTimeout) return true;

0 commit comments

Comments
 (0)