UPDATE: User history and view all recent screen design
This commit is contained in:
1 parent
1ba92683fc
commit
7068c90656
9 files changed
+365
-41
No files matched your search
@@ -160,6 +160,7 @@ exports.getRecentActivities = (req, res) => {
|
||||
const offset = (page - 1) * limit;
|
||||
const dateFrom = req.query.date_from || '';
|
||||
const dateTo = req.query.date_to || '';
|
||||
const action = req.query.action || '';
|
||||
// Comma-separated entity type filter, e.g. "student,document" (teacher view)
|
||||
const entityTypesRaw = req.query.entity_types || '';
|
||||
|
||||
@@ -168,6 +169,7 @@ exports.getRecentActivities = (req, res) => {
|
||||
|
||||
if (dateFrom) { conditions.push("DATE(a.created_at) >= DATE(?)"); params.push(dateFrom); }
|
||||
if (dateTo) { conditions.push("DATE(a.created_at) <= DATE(?)"); params.push(dateTo); }
|
||||
if (action) { conditions.push("a.action = ?"); params.push(action); }
|
||||
|
||||
if (entityTypesRaw) {
|
||||
const types = entityTypesRaw.split(',').map(t => t.trim()).filter(Boolean);
|
||||
|
||||
@@ -263,12 +263,16 @@ exports.getUserHistory = (req, res) => {
|
||||
const offset = (page - 1) * limit;
|
||||
const dateFrom = req.query.date_from || '';
|
||||
const dateTo = req.query.date_to || '';
|
||||
const action = req.query.action || '';
|
||||
const role = req.query.role || '';
|
||||
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
|
||||
if (dateFrom) { conditions.push("DATE(h.created_at) >= DATE(?)"); params.push(dateFrom); }
|
||||
if (dateTo) { conditions.push("DATE(h.created_at) <= DATE(?)"); params.push(dateTo); }
|
||||
if (action) { conditions.push("h.action = ?"); params.push(action); }
|
||||
if (role) { conditions.push("h.role = ?"); params.push(role); }
|
||||
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ class ActivityRepository {
|
||||
int limit = 20,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
String? action,
|
||||
String? role,
|
||||
}) async {
|
||||
try {
|
||||
final options = await _getAuthOptions();
|
||||
@@ -28,6 +30,8 @@ class ActivityRepository {
|
||||
'limit': limit,
|
||||
if (dateFrom != null && dateFrom.isNotEmpty) 'date_from': dateFrom,
|
||||
if (dateTo != null && dateTo.isNotEmpty) 'date_to': dateTo,
|
||||
if (action != null && action.isNotEmpty) 'action': action,
|
||||
if (role != null && role.isNotEmpty) 'role': role,
|
||||
},
|
||||
options: options,
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ class DashboardRepository {
|
||||
int limit = 10,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
String? action,
|
||||
String? entityTypes, // Comma-separated, e.g. "student,document" for teacher view
|
||||
}) async {
|
||||
try {
|
||||
@@ -38,6 +39,7 @@ class DashboardRepository {
|
||||
'limit': limit,
|
||||
if (dateFrom != null && dateFrom.isNotEmpty) 'date_from': dateFrom,
|
||||
if (dateTo != null && dateTo.isNotEmpty) 'date_to': dateTo,
|
||||
if (action != null && action.isNotEmpty) 'action': action,
|
||||
if (entityTypes != null && entityTypes.isNotEmpty) 'entity_types': entityTypes,
|
||||
},
|
||||
options: options,
|
||||
|
||||
@@ -12,12 +12,16 @@ class UserHistoryQueryParams {
|
||||
final int limit;
|
||||
final String dateFrom;
|
||||
final String dateTo;
|
||||
final String action;
|
||||
final String role;
|
||||
|
||||
const UserHistoryQueryParams({
|
||||
this.page = 1,
|
||||
this.limit = 20,
|
||||
this.dateFrom = '',
|
||||
this.dateTo = '',
|
||||
this.action = '',
|
||||
this.role = '',
|
||||
});
|
||||
|
||||
UserHistoryQueryParams copyWith({
|
||||
@@ -25,12 +29,16 @@ class UserHistoryQueryParams {
|
||||
int? limit,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
String? action,
|
||||
String? role,
|
||||
}) {
|
||||
return UserHistoryQueryParams(
|
||||
page: page ?? this.page,
|
||||
limit: limit ?? this.limit,
|
||||
dateFrom: dateFrom ?? this.dateFrom,
|
||||
dateTo: dateTo ?? this.dateTo,
|
||||
action: action ?? this.action,
|
||||
role: role ?? this.role,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +57,8 @@ class UserHistoryQueryNotifier
|
||||
void setPage(int page) => state = state.copyWith(page: page);
|
||||
void setDateFrom(String v) => state = state.copyWith(dateFrom: v, page: 1);
|
||||
void setDateTo(String v) => state = state.copyWith(dateTo: v, page: 1);
|
||||
void setAction(String v) => state = state.copyWith(action: v, page: 1);
|
||||
void setRole(String v) => state = state.copyWith(role: v, page: 1);
|
||||
void reset() => state = const UserHistoryQueryParams();
|
||||
}
|
||||
|
||||
@@ -61,5 +71,7 @@ final userHistoryPageProvider =
|
||||
limit: query.limit,
|
||||
dateFrom: query.dateFrom.isEmpty ? null : query.dateFrom,
|
||||
dateTo: query.dateTo.isEmpty ? null : query.dateTo,
|
||||
action: query.action.isEmpty ? null : query.action,
|
||||
role: query.role.isEmpty ? null : query.role,
|
||||
);
|
||||
});
|
||||
@@ -36,12 +36,16 @@ class ActivityQueryParams {
|
||||
final int limit;
|
||||
final String dateFrom;
|
||||
final String dateTo;
|
||||
final String action;
|
||||
final String entityTypes;
|
||||
|
||||
const ActivityQueryParams({
|
||||
this.page = 1,
|
||||
this.limit = 15,
|
||||
this.dateFrom = '',
|
||||
this.dateTo = '',
|
||||
this.action = '',
|
||||
this.entityTypes = '',
|
||||
});
|
||||
|
||||
ActivityQueryParams copyWith({
|
||||
@@ -49,12 +53,16 @@ class ActivityQueryParams {
|
||||
int? limit,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
String? action,
|
||||
String? entityTypes,
|
||||
}) {
|
||||
return ActivityQueryParams(
|
||||
page: page ?? this.page,
|
||||
limit: limit ?? this.limit,
|
||||
dateFrom: dateFrom ?? this.dateFrom,
|
||||
dateTo: dateTo ?? this.dateTo,
|
||||
action: action ?? this.action,
|
||||
entityTypes: entityTypes ?? this.entityTypes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +79,8 @@ class ActivityQueryNotifier extends Notifier<ActivityQueryParams> {
|
||||
void setPage(int page) => state = state.copyWith(page: page);
|
||||
void setDateFrom(String v) => state = state.copyWith(dateFrom: v, page: 1);
|
||||
void setDateTo(String v) => state = state.copyWith(dateTo: v, page: 1);
|
||||
void setAction(String v) => state = state.copyWith(action: v, page: 1);
|
||||
void setEntityTypes(String v) => state = state.copyWith(entityTypes: v, page: 1);
|
||||
void reset() => state = const ActivityQueryParams();
|
||||
}
|
||||
|
||||
@@ -86,7 +96,8 @@ final recentActivitiesPageProvider =
|
||||
limit: query.limit,
|
||||
dateFrom: query.dateFrom.isEmpty ? null : query.dateFrom,
|
||||
dateTo: query.dateTo.isEmpty ? null : query.dateTo,
|
||||
action: query.action.isEmpty ? null : query.action,
|
||||
// Teachers only see student and document activities
|
||||
entityTypes: isTeacher ? 'student,document' : null,
|
||||
entityTypes: isTeacher ? 'student,document' : (query.entityTypes.isEmpty ? null : query.entityTypes),
|
||||
);
|
||||
});
|
||||
@@ -41,6 +41,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.invalidate(dashboardDataProvider);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(notificationsProvider.notifier).refreshNotifications();
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../../core/utils/date_utils.dart' as pht;
|
||||
|
||||
import '../../../domain/entities/dashboard_models.dart';
|
||||
import '../../providers/dashboard_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../shared/modals/view_activity_modal.dart';
|
||||
|
||||
class RecentActivitiesScreen extends ConsumerStatefulWidget {
|
||||
@@ -60,10 +61,126 @@ class _RecentActivitiesScreenState
|
||||
ref.read(activityQueryProvider.notifier).reset();
|
||||
}
|
||||
|
||||
void _showFilterDialog(ActivityQueryParams query, bool isTeacher) {
|
||||
String pendingAction = query.action.isEmpty ? 'All Actions' : query.action;
|
||||
String pendingEntity = query.entityTypes.isEmpty ? 'All Entities' : query.entityTypes;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.25),
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.07), blurRadius: 16, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.tune_rounded, size: 18, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Filter Activities', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Action', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: pendingAction,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: ['All Actions', 'CREATE', 'UPDATE', 'DELETE']
|
||||
.map((v) => DropdownMenuItem(value: v, child: Text(v)))
|
||||
.toList(),
|
||||
onChanged: (v) => setDialogState(() => pendingAction = v!),
|
||||
),
|
||||
if (!isTeacher) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text('Entity Type', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: pendingEntity,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: ['All Entities', 'user', 'student', 'document']
|
||||
.map((v) => DropdownMenuItem(value: v, child: Text(v.toUpperCase())))
|
||||
.toList(),
|
||||
onChanged: (v) => setDialogState(() => pendingEntity = v!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Cancel', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final actionToSet = pendingAction == 'All Actions' ? '' : pendingAction;
|
||||
final entityToSet = pendingEntity == 'All Entities' ? '' : pendingEntity;
|
||||
ref.read(activityQueryProvider.notifier).setAction(actionToSet);
|
||||
ref.read(activityQueryProvider.notifier).setEntityTypes(entityToSet);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white),
|
||||
child: const Text('Apply Filters'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activitiesAsync = ref.watch(recentActivitiesPageProvider);
|
||||
final query = ref.watch(activityQueryProvider);
|
||||
final isTeacher = ref.watch(authProvider).value?.role == 'teacher';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -92,9 +209,16 @@ class _RecentActivitiesScreenState
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.tune_rounded, size: 16, color: Colors.white),
|
||||
label: const Text('Filters', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
onPressed: () => _showFilterDialog(query, isTeacher),
|
||||
side: BorderSide.none,
|
||||
),
|
||||
_dateChip('From', _fromDate, () => _pickDate(true)),
|
||||
_dateChip('To', _toDate, () => _pickDate(false)),
|
||||
if (_fromDate != null || _toDate != null)
|
||||
if (_fromDate != null || _toDate != null || query.action.isNotEmpty || query.entityTypes.isNotEmpty)
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.clear, size: 16),
|
||||
label: const Text('Clear'),
|
||||
@@ -350,32 +474,52 @@ class _RecentActivitiesScreenState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPagination(PaginatedActivities data, int currentPage) {
|
||||
Widget _buildPagination(PaginatedActivities data, int current) {
|
||||
if (data.totalPages <= 1) return const SizedBox.shrink();
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: currentPage > 1
|
||||
? () => ref
|
||||
.read(activityQueryProvider.notifier)
|
||||
.setPage(currentPage - 1)
|
||||
onPressed: current > 1
|
||||
? () => ref.read(activityQueryProvider.notifier).setPage(current - 1)
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
'Page $currentPage of ${data.totalPages}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
...List.generate(data.totalPages, (i) => i + 1)
|
||||
.where((p) => (p - current).abs() <= 2)
|
||||
.map((p) {
|
||||
final isActive = p == current;
|
||||
return GestureDetector(
|
||||
onTap: () => ref.read(activityQueryProvider.notifier).setPage(p),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppColors.primaryGreen : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: isActive ? null : Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$p',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isActive ? Colors.white : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: currentPage < data.totalPages
|
||||
? () => ref
|
||||
.read(activityQueryProvider.notifier)
|
||||
.setPage(currentPage + 1)
|
||||
onPressed: current < data.totalPages
|
||||
? () => ref.read(activityQueryProvider.notifier).setPage(current + 1)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
@@ -386,16 +530,17 @@ class _RecentActivitiesScreenState
|
||||
Widget _actionChip(String action) {
|
||||
final color = _actionColor(action);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
action.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -58,6 +58,119 @@ class _UserHistoryScreenState extends ConsumerState<UserHistoryScreen> {
|
||||
ref.read(userHistoryQueryProvider.notifier).reset();
|
||||
}
|
||||
|
||||
void _showFilterDialog(UserHistoryQueryParams query) {
|
||||
String pendingAction = query.action.isEmpty ? 'All Actions' : query.action;
|
||||
String pendingRole = query.role.isEmpty ? 'All Roles' : query.role;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.25),
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.07), blurRadius: 16, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.tune_rounded, size: 18, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Filter User History', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Action', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: pendingAction,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: ['All Actions', 'CREATED', 'UPDATED', 'DELETED']
|
||||
.map((v) => DropdownMenuItem(value: v, child: Text(v)))
|
||||
.toList(),
|
||||
onChanged: (v) => setDialogState(() => pendingAction = v!),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Role', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: pendingRole,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: ['All Roles', 'admin', 'teacher']
|
||||
.map((v) => DropdownMenuItem(value: v, child: Text(v.toUpperCase())))
|
||||
.toList(),
|
||||
onChanged: (v) => setDialogState(() => pendingRole = v!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Cancel', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final actionToSet = pendingAction == 'All Actions' ? '' : pendingAction;
|
||||
final roleToSet = pendingRole == 'All Roles' ? '' : pendingRole;
|
||||
ref.read(userHistoryQueryProvider.notifier).setAction(actionToSet);
|
||||
ref.read(userHistoryQueryProvider.notifier).setRole(roleToSet);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white),
|
||||
child: const Text('Apply Filters'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final historyAsync = ref.watch(userHistoryPageProvider);
|
||||
@@ -90,9 +203,16 @@ class _UserHistoryScreenState extends ConsumerState<UserHistoryScreen> {
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.tune_rounded, size: 16, color: Colors.white),
|
||||
label: const Text('Filters', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
onPressed: () => _showFilterDialog(query),
|
||||
side: BorderSide.none,
|
||||
),
|
||||
_dateChip('From', _fromDate, () => _pickDate(true)),
|
||||
_dateChip('To', _toDate, () => _pickDate(false)),
|
||||
if (_fromDate != null || _toDate != null)
|
||||
if (_fromDate != null || _toDate != null || query.action.isNotEmpty || query.role.isNotEmpty)
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.clear, size: 16),
|
||||
label: const Text('Clear'),
|
||||
@@ -348,32 +468,52 @@ class _UserHistoryScreenState extends ConsumerState<UserHistoryScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPagination(PaginatedUserHistory data, int currentPage) {
|
||||
Widget _buildPagination(PaginatedUserHistory data, int current) {
|
||||
if (data.totalPages <= 1) return const SizedBox.shrink();
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: currentPage > 1
|
||||
? () => ref
|
||||
.read(userHistoryQueryProvider.notifier)
|
||||
.setPage(currentPage - 1)
|
||||
onPressed: current > 1
|
||||
? () => ref.read(userHistoryQueryProvider.notifier).setPage(current - 1)
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
'Page $currentPage of ${data.totalPages}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
...List.generate(data.totalPages, (i) => i + 1)
|
||||
.where((p) => (p - current).abs() <= 2)
|
||||
.map((p) {
|
||||
final isActive = p == current;
|
||||
return GestureDetector(
|
||||
onTap: () => ref.read(userHistoryQueryProvider.notifier).setPage(p),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppColors.primaryGreen : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: isActive ? null : Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$p',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isActive ? Colors.white : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: currentPage < data.totalPages
|
||||
? () => ref
|
||||
.read(userHistoryQueryProvider.notifier)
|
||||
.setPage(currentPage + 1)
|
||||
onPressed: current < data.totalPages
|
||||
? () => ref.read(userHistoryQueryProvider.notifier).setPage(current + 1)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
@@ -384,16 +524,17 @@ class _UserHistoryScreenState extends ConsumerState<UserHistoryScreen> {
|
||||
Widget _actionChip(String action) {
|
||||
final color = _actionColor(action);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
action.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
@@ -404,10 +545,11 @@ class _UserHistoryScreenState extends ConsumerState<UserHistoryScreen> {
|
||||
final isAdmin = role == 'admin';
|
||||
final color = isAdmin ? Colors.purple : Colors.teal;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
role.toUpperCase(),
|
||||
|
||||
Reference in new issue
Block a user