Skip to content

Commit

Permalink
progress for import and fix for update users on import
Browse files Browse the repository at this point in the history
  • Loading branch information
miakh committed Nov 21, 2024
1 parent 0b094d4 commit 7006466
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 98 deletions.
2 changes: 1 addition & 1 deletion lib/dataModels/OccasionUserModel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class OccasionUserModel extends IPlutoRowModel {
factory OccasionUserModel.fromImportedJson(Map<String, dynamic> json, [OccasionUserModel? original]) {
return OccasionUserModel(
occasion: RightsService.currentOccasion!,
user: json[Tb.occasion_users.user],
user: original?.user ?? json[Tb.occasion_users.user],
role: json[Tb.occasion_users.role],
data: {
Tb.occasion_users.data_email: json[Tb.occasion_users.data_email],
Expand Down
4 changes: 0 additions & 4 deletions lib/pages/AdministrationOccasion/UsersTab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ class _UsersTabState extends State<UsersTab> {
);

if (confirm) {
ValueNotifier<int> invitedCount = ValueNotifier(0);
Map<OccasionUserModel, int> retryAttempts = {
for (var user in users) user: 0
};
Expand All @@ -177,8 +176,6 @@ class _UsersTabState extends State<UsersTab> {
try {
// Send sign-in code and update progress
await AuthService.sendSignInCode(user);
invitedCount.value++;

ToastHelper.Show(
context,
"Invited: {user}.".tr(namedArgs: {
Expand Down Expand Up @@ -218,7 +215,6 @@ class _UsersTabState extends State<UsersTab> {
context,
"Invite".tr(),
users.length,
invitedCount,
futures: inviteFutures,
);
}
Expand Down
43 changes: 33 additions & 10 deletions lib/services/DialogHelper.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:fstapp/dataModels/LanguageModel.dart';
import 'package:fstapp/dataModels/UserGroupInfoModel.dart';
import 'package:fstapp/dataModels/UserInfoModel.dart';
Expand Down Expand Up @@ -308,18 +310,14 @@ class DialogHelper{
static Future<void> showProgressDialogAsync(
BuildContext context,
String title,
int total,
ValueNotifier<int> progressNotifier, {
int total, {
List<Future<void>>? futures,
}) async {
if (futures != null) {
for (var future in futures) {
await future;
progressNotifier.value++;
}
}

await showDialog(
// Completer to control the dialog lifecycle
final completer = Completer<void>();
var progressNotifier = ValueNotifier<int>(0);
// Show the dialog immediately
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
Expand All @@ -334,12 +332,37 @@ class DialogHelper{
Text("${"Progress".tr()}: $progress/$total"),
SizedBox(height: 20),
LinearProgressIndicator(value: total > 0 ? progress / total : 0),
if (progress >= total) ...[
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
completer.complete();
},
child: Text("Ok".tr()),
),
],
],
),
);
},
);
},
);

// Process the futures, updating progress
if (futures != null) {
for (var future in futures) {
await future;
progressNotifier.value++;
}
}

// Wait for the user to press "OK" if not already completed
if (!completer.isCompleted) {
await completer.future;
}
}


}
215 changes: 132 additions & 83 deletions lib/services/UserManagementHelper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:fstapp/dataServices/AuthService.dart';
import 'package:fstapp/dataServices/DbUsers.dart';
import 'package:fstapp/services/DialogHelper.dart';
import 'package:fstapp/services/ImportHelper.dart';
import 'package:fstapp/services/NavigationService.dart';
import 'package:fstapp/services/ToastHelper.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
Expand All @@ -13,103 +12,153 @@ import 'package:flutter/cupertino.dart';
class UserManagementHelper{
static Future<void> import(BuildContext context) async {
var file = await DialogHelper.dropFilesHere(context, "Import of users from CSV".tr(), "Ok".tr(), "Storno".tr());
if(file==null) {
return;
}
if (file == null) return;

var users = await ImportHelper.getUsersFromFile(file);
var addOrUpdateUsers = users.where((element) => element[Tb.occasion_users.data_text4]?.toLowerCase() != "storno");
var deleteUsers = users.where((element) => element[Tb.occasion_users.data_text4]?.toLowerCase() == "storno");

var really = await DialogHelper.showConfirmationDialogAsync(context,
"Importing users".tr(),
"${"Users".tr()} (${users.length}):\n${users.map((value) => value[Tb.occasion_users.data_email]).toList().join(",\n")}",
confirmButtonMessage: "Proceed".tr());
var proceed = await DialogHelper.showConfirmationDialogAsync(
context,
"Importing users".tr(),
"${"Users".tr()} (${users.length}):\n${users.map((value) => value[Tb.occasion_users.data_email]).toList().join(",\n")}",
confirmButtonMessage: "Proceed".tr(),
);

if(!really)
{
return;
}
if (!proceed) return;

var existingUsers = await DbUsers.getOccasionUsers();
var toBeCreated = _getUsersToBeCreated(users, existingUsers);
var toBeUpdated = _getUsersToBeUpdated(users, existingUsers);
var toBeDeleted = _getUsersToBeDeleted(deleteUsers, addOrUpdateUsers, existingUsers);

List<Map<String, dynamic>> toBeCreated = [];
List<Map<String, dynamic>> toBeUpdated = [];
for(var u in users)
{
var existing = existingUsers.firstWhereOrNull((element) => element.data?[Tb.occasion_users.data_email]?.toLowerCase() == u[Tb.occasion_users.data_email]?.toLowerCase());
if(existing == null) {
toBeCreated.add(u);
continue;
}
else if(existing.importedEquals(u)) {
continue;
}
else{
u[Tb.occasion_users.user] = existing.user;
toBeUpdated.add(u);
}
}
await _handleCreateUsers(context, toBeCreated);
await _handleUpdateUsers(context, toBeUpdated, existingUsers);
await _handleDeleteUsers(context, toBeDeleted);
}

if(toBeCreated.isNotEmpty) {
var really = await DialogHelper.showConfirmationDialogAsync(context,
"Creating users".tr(),
"New users found. Do you want to create them?".tr() +
// Helper to get users to be created
static List<Map<String, dynamic>> _getUsersToBeCreated(
Iterable<Map<String, dynamic>> users, List<OccasionUserModel> existingUsers) {
return users.where((u) {
return existingUsers.firstWhereOrNull(
(e) => e.data?[Tb.occasion_users.data_email]?.toLowerCase() ==
u[Tb.occasion_users.data_email]?.toLowerCase(),
) ==
null;
}).toList();
}

// Helper to get users to be updated
static List<Map<String, dynamic>> _getUsersToBeUpdated(
Iterable<Map<String, dynamic>> users, List<OccasionUserModel> existingUsers) {
return users.where((u) {
var existing = existingUsers.firstWhereOrNull(
(e) => e.data?[Tb.occasion_users.data_email]?.toLowerCase() ==
u[Tb.occasion_users.data_email]?.toLowerCase(),
);
return existing != null && !existing.importedEquals(u);
}).toList();
}

// Helper to get users to be deleted
static List<OccasionUserModel> _getUsersToBeDeleted(
Iterable<Map<String, dynamic>> deleteUsers,
Iterable<Map<String, dynamic>> addOrUpdateUsers,
List<OccasionUserModel> existingUsers) {
return deleteUsers.map((u) {
var existing = existingUsers.firstWhereOrNull(
(e) => e.data?[Tb.occasion_users.data_email] == u[Tb.occasion_users.data_email],
);
var duplicated = addOrUpdateUsers.firstWhereOrNull(
(e) => e[Tb.occasion_users.data_email] == u[Tb.occasion_users.data_email],
);
return (existing != null && duplicated == null) ? existing : null;
}).whereNotNull().toList();
}

// Handle creating users with progress dialog
static Future<void> _handleCreateUsers(BuildContext context, List<Map<String, dynamic>> toBeCreated) async {
if (toBeCreated.isEmpty) return;

var proceed = await DialogHelper.showConfirmationDialogAsync(
context,
"Creating users".tr(),
"New users found. Do you want to create them?".tr() +
"\n" +
"${"Users".tr()} (${toBeCreated.length}):\n${toBeCreated.map((value) => value[Tb.occasion_users.data_email]).toList().join(",\n")}",
confirmButtonMessage: "Proceed".tr());

if(really) {
toBeCreated.forEach((u) async {
await DbUsers.updateOccasionUser(OccasionUserModel.fromImportedJson(u));
ToastHelper.Show(context, "Created {item}.".tr(namedArgs: {"item": u[Tb.occasion_users.data_email]}));
});
}
}
"${"Users".tr()} (${toBeCreated.length}):\n${toBeCreated.map((u) => u[Tb.occasion_users.data_email]).join(",\n")}",
confirmButtonMessage: "Proceed".tr(),
);

if(toBeUpdated.isNotEmpty) {
var really = await DialogHelper.showConfirmationDialogAsync(context,
"Updating users".tr(),
"These users have some changes. Do you want to update them?".tr() +
if (!proceed) return;

await DialogHelper.showProgressDialogAsync(
context,
"Creating users".tr(),
toBeCreated.length,
futures: toBeCreated.map((u) async {
await DbUsers.updateOccasionUser(OccasionUserModel.fromImportedJson(u));
ToastHelper.Show(context, "Created {item}.".tr(namedArgs: {"item": u[Tb.occasion_users.data_email]}));
}).toList(),
);
}

// Handle updating users with progress dialog
static Future<void> _handleUpdateUsers(
BuildContext context, List<Map<String, dynamic>> toBeUpdated, List<OccasionUserModel> existingUsers) async {
if (toBeUpdated.isEmpty) return;

var proceed = await DialogHelper.showConfirmationDialogAsync(
context,
"Updating users".tr(),
"These users have some changes. Do you want to update them?".tr() +
"\n" +
"${"Users".tr()} (${toBeUpdated.length}):\n${toBeUpdated.map((value) => value[Tb.occasion_users.data_email]).toList().join(",\n")}",
confirmButtonMessage: "Proceed".tr());

if(really) {
toBeUpdated.forEach((u) async {
await DbUsers.updateExistingImportedOccasionUser(
OccasionUserModel.fromImportedJson(u,
existingUsers.firstWhereOrNull((e)=>e.data?[Tb.occasion_users.data_email] == u[Tb.occasion_users.data_email])));
ToastHelper.Show(context, "Updated {item}.".tr(namedArgs: {"item": u[Tb.occasion_users.data_email]}));
});
}
}
"${"Users".tr()} (${toBeUpdated.length}):\n${toBeUpdated.map((u) => u[Tb.occasion_users.data_email]).join(",\n")}",
confirmButtonMessage: "Proceed".tr(),
);

List<OccasionUserModel> toBeDeleted = [];
for(var u in deleteUsers)
{
var existing = existingUsers.firstWhereOrNull((element) => element.data?[Tb.occasion_users.data_email] == u[Tb.occasion_users.data_email]);
var duplicated = addOrUpdateUsers.firstWhereOrNull((element) => element[Tb.occasion_users.data_email] == u[Tb.occasion_users.data_email]);
if (!proceed) return;

if(existing != null && duplicated == null) {
toBeDeleted.add(existing);
}
}
await DialogHelper.showProgressDialogAsync(
context,
"Updating users".tr(),
toBeUpdated.length,
futures: toBeUpdated.map((u) async {
var existing = existingUsers.firstWhere(
(e) => e.data?[Tb.occasion_users.data_email] == u[Tb.occasion_users.data_email],
);
var fromExisting = OccasionUserModel.fromImportedJson(u, existing);
await DbUsers.updateExistingImportedOccasionUser(fromExisting);
ToastHelper.Show(context, "Updated {item}.".tr(namedArgs: {"item": u[Tb.occasion_users.data_email]}));
}).toList(),
);
}

if(toBeDeleted.isNotEmpty) {
var reallyDelete = await DialogHelper.showConfirmationDialogAsync(context,
"Removing users",
"These users have been removed, but they still exist in the application. Do you want to remove them?".tr() +
"\n" +
"${"Users".tr()} (${toBeDeleted.length}):\n${toBeDeleted.map((value) => value.toBasicString()).toList().join(",\n")}",
confirmButtonMessage: "Proceed".tr());

if(reallyDelete) {
toBeDeleted.forEach((existing) async {
await DbUsers.deleteUser(existing.user!, existing.occasion!);
ToastHelper.Show(context, "Removed {item}.".tr(namedArgs: {"item": existing.toBasicString()}));
});
}
}
// Handle deleting users with progress dialog
static Future<void> _handleDeleteUsers(BuildContext context, List<OccasionUserModel> toBeDeleted) async {
if (toBeDeleted.isEmpty) return;

var proceed = await DialogHelper.showConfirmationDialogAsync(
context,
"Removing users".tr(),
"These users have been removed, but they still exist in the application. Do you want to remove them?".tr() +
"\n" +
"${"Users".tr()} (${toBeDeleted.length}):\n${toBeDeleted.map((u) => u.toBasicString()).join(",\n")}",
confirmButtonMessage: "Proceed".tr(),
);

if (!proceed) return;


await DialogHelper.showProgressDialogAsync(
context,
"Removing users".tr(),
toBeDeleted.length,
futures: toBeDeleted.map((existing) async {
await DbUsers.deleteUser(existing.user!, existing.occasion!);
ToastHelper.Show(context, "Removed {item}.".tr(namedArgs: {"item": existing.toBasicString()}));
}).toList(),
);
}

static Future<bool> unsafeChangeUserPassword(BuildContext context, OccasionUserModel user) async {
Expand Down

0 comments on commit 7006466

Please sign in to comment.