Skip to content

Commit 1ade303

Browse files
fix user bulk create WIP
1 parent cdc3139 commit 1ade303

6 files changed

Lines changed: 194 additions & 86 deletions

File tree

school_data_hub_flutter/lib/features/user/domain/user_manager.dart

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,22 +278,74 @@ class UserManager {
278278
return requests;
279279
}
280280

281-
/// Streams batch create results one-by-one (avoids HTTP timeout). Caller listens and accumulates.
282-
Stream<BatchCreateUserEvent> batchCreateUsersStreamFromImportRows(
281+
/// Creates users in small HTTP chunks, yielding a [BatchCreateResult] per chunk.
282+
///
283+
/// Each chunk is a short HTTP request to [batchCreateUsers] (not WebSocket),
284+
/// so it is unaffected by the hub stream lifecycle.
285+
Stream<BatchCreateResult> batchCreateUsersInChunks(
283286
List<StaffImportRow> rows, {
287+
int chunkSize = 3,
284288
String Function(StaffImportRow)? generatePassword,
285-
}) {
289+
}) async* {
286290
final requests = buildCreateUserRequestsFromImportRows(
287291
rows,
288292
generatePassword: generatePassword,
289293
);
290-
_log.info('[UserManager] batchCreateUsersStreamFromImportRows: rows=${rows.length} -> requests=${requests.length}, calling API stream');
291-
return _apiService.batchCreateUsersStream(requests);
294+
_log.info(
295+
'[UserManager] batchCreateUsersInChunks: rows=${rows.length} -> '
296+
'requests=${requests.length}, chunkSize=$chunkSize',
297+
);
298+
299+
for (var i = 0; i < requests.length; i += chunkSize) {
300+
final end = (i + chunkSize).clamp(0, requests.length);
301+
final chunk = requests.sublist(i, end);
302+
_log.info('[UserManager] sending chunk ${i ~/ chunkSize + 1} (indices $i..$end)');
303+
304+
final response = await _apiService.batchCreateUsers(chunk);
305+
if (response == null) {
306+
yield BatchCreateResult(
307+
credentials: [],
308+
errors: [
309+
for (var j = 0; j < chunk.length; j++)
310+
BatchCreateError(
311+
rowIndex: i + j,
312+
userNameOrKurzel: chunk[j].userName,
313+
message: 'Server-Antwort war null.',
314+
),
315+
],
316+
);
317+
continue;
318+
}
319+
320+
yield BatchCreateResult(
321+
credentials: response.credentials
322+
.map(
323+
(c) => StaffCredentialEntry(
324+
userName: c.userName,
325+
fullName: c.fullName,
326+
email: c.email,
327+
password: c.password,
328+
),
329+
)
330+
.toList(),
331+
errors: response.errors
332+
.map(
333+
(e) => BatchCreateError(
334+
rowIndex: e.rowIndex + i,
335+
userNameOrKurzel: e.userNameOrKurzel,
336+
message: e.message,
337+
),
338+
)
339+
.toList(),
340+
);
341+
}
342+
343+
_log.info('[UserManager] batchCreateUsersInChunks: done');
344+
await fetchUsersCommand.runAsync();
292345
}
293346

294-
/// Batch-creates users from import rows. Passwords are generated on the client;
295-
/// server validates and skips duplicate userName/email. Refreshes user list at the end.
296-
/// Prefer [batchCreateUsersStreamFromImportRows] for large batches to avoid timeout.
347+
/// Batch-creates users from import rows in a single HTTP call.
348+
/// For large batches prefer [batchCreateUsersInChunks] to avoid timeout.
297349
Future<BatchCreateResult> batchCreateUsersFromImportRows(
298350
List<StaffImportRow> rows, {
299351
String Function(StaffImportRow)? generatePassword,

school_data_hub_flutter/lib/features/user/presentation/batch_import_users/batch_import_users_page.dart

Lines changed: 84 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import 'package:flutter_it/flutter_it.dart';
66
import 'package:gap/gap.dart';
77
import 'package:logging/logging.dart';
88
import 'package:printing/printing.dart';
9-
import 'package:school_data_hub_client/school_data_hub_client.dart';
109
import 'package:school_data_hub_flutter/app_utils/pdf_viewer_page.dart';
1110
import 'package:school_data_hub_flutter/common/theme/app_colors.dart';
1211
import 'package:school_data_hub_flutter/common/theme/styles.dart';
@@ -16,6 +15,7 @@ import 'package:school_data_hub_flutter/features/user/data/staff_excel_import_pa
1615
import 'package:school_data_hub_flutter/features/user/domain/batch_create_result.dart';
1716
import 'package:school_data_hub_flutter/features/user/domain/user_manager.dart';
1817
import 'package:school_data_hub_flutter/features/user/presentation/batch_import_users/staff_credentials_pdf_service.dart';
18+
import 'package:wakelock_plus/wakelock_plus.dart';
1919

2020
final _log = Logger('BatchImportUsersPage');
2121

@@ -32,11 +32,12 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
3232
bool _isCreating = false;
3333
int _progressCreated = 0;
3434
int _progressErrors = 0;
35-
StreamSubscription<BatchCreateUserEvent>? _streamSubscription;
35+
StreamSubscription<BatchCreateResult>? _chunkSubscription;
3636

3737
@override
3838
void dispose() {
39-
_streamSubscription?.cancel();
39+
_chunkSubscription?.cancel();
40+
WakelockPlus.disable();
4041
super.dispose();
4142
}
4243

@@ -63,89 +64,90 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
6364
_progressCreated = 0;
6465
_progressErrors = 0;
6566
});
67+
68+
await WakelockPlus.enable();
69+
_log.info('[BatchImport] Wakelock enabled');
70+
6671
final userManager = di<UserManager>();
67-
final credentials = <StaffCredentialEntry>[];
68-
final errors = <BatchCreateError>[];
72+
final allCredentials = <StaffCredentialEntry>[];
73+
final allErrors = <BatchCreateError>[];
6974

7075
try {
71-
_log.info('[BatchImport] Getting stream from UserManager');
72-
final stream = userManager.batchCreateUsersStreamFromImportRows(rows);
73-
_log.info('[BatchImport] Subscribing to batchCreateUsersStream');
74-
_streamSubscription = stream.listen(
75-
(event) {
76+
final stream = userManager.batchCreateUsersInChunks(rows);
77+
_log.info('[BatchImport] Subscribing to batchCreateUsersInChunks');
78+
_chunkSubscription = stream.listen(
79+
(chunkResult) {
7680
if (!mounted) return;
77-
if (event.credential != null) {
78-
final c = event.credential!;
79-
credentials.add(
80-
StaffCredentialEntry(
81-
userName: c.userName,
82-
fullName: c.fullName,
83-
email: c.email,
84-
password: c.password,
85-
),
86-
);
87-
setState(() => _progressCreated = credentials.length);
88-
_log.info(
89-
'[BatchImport] Event: created ${credentials.length} — ${c.userName}',
90-
);
91-
} else if (event.error != null) {
92-
final e = event.error!;
93-
errors.add(
94-
BatchCreateError(
95-
rowIndex: e.rowIndex,
96-
userNameOrKurzel: e.userNameOrKurzel,
97-
message: e.message,
98-
),
99-
);
100-
setState(() => _progressErrors = errors.length);
101-
_log.info(
102-
'[BatchImport] Event: error ${errors.length} — row ${e.rowIndex} ${e.userNameOrKurzel}: ${e.message}',
103-
);
104-
}
81+
allCredentials.addAll(chunkResult.credentials);
82+
allErrors.addAll(chunkResult.errors);
83+
setState(() {
84+
_progressCreated = allCredentials.length;
85+
_progressErrors = allErrors.length;
86+
});
87+
_log.info(
88+
'[BatchImport] Chunk done — created=${chunkResult.successCount}, '
89+
'errors=${chunkResult.failureCount}, '
90+
'total created=$_progressCreated, total errors=$_progressErrors',
91+
);
10592
},
10693
onError: (Object e, StackTrace? st) {
107-
_log.severe('[BatchImport] Stream onError', e, st);
94+
_log.severe('[BatchImport] Chunk stream onError', e, st);
10895
if (mounted) {
10996
setState(() => _isCreating = false);
110-
ScaffoldMessenger.of(
111-
context,
112-
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
97+
ScaffoldMessenger.of(context).showSnackBar(
98+
SnackBar(content: Text('Fehler: $e')),
99+
);
113100
}
101+
WakelockPlus.disable();
114102
},
115-
onDone: () async {
103+
onDone: () {
116104
_log.info(
117-
'[BatchImport] Stream onDone — credentials=${credentials.length} errors=${errors.length}',
105+
'[BatchImport] All chunks done — '
106+
'credentials=${allCredentials.length} errors=${allErrors.length}',
118107
);
119-
if (!mounted) return;
120-
_log.info('[BatchImport] Refreshing user list');
121-
await userManager.fetchUsersCommand.runAsync();
108+
WakelockPlus.disable();
122109
if (!mounted) return;
123110
setState(() {
124111
_batchResult = BatchCreateResult(
125-
credentials: credentials,
126-
errors: errors,
112+
credentials: allCredentials,
113+
errors: allErrors,
127114
);
128115
_isCreating = false;
129116
_progressCreated = 0;
130117
_progressErrors = 0;
131118
});
132-
_log.info(
133-
'[BatchImport] Batch complete. Success: ${credentials.length}, failures: ${errors.length}',
134-
);
135119
},
136120
cancelOnError: false,
137121
);
138122
} catch (e, st) {
139123
_log.severe('[BatchImport] _createUsers catch', e, st);
124+
await WakelockPlus.disable();
140125
if (mounted) {
141126
setState(() => _isCreating = false);
142-
ScaffoldMessenger.of(
143-
context,
144-
).showSnackBar(SnackBar(content: Text('Fehler: $e')));
127+
ScaffoldMessenger.of(context).showSnackBar(
128+
SnackBar(content: Text('Fehler: $e')),
129+
);
145130
}
146131
}
147132
}
148133

134+
void _abortCreate() {
135+
_log.info('[BatchImport] User aborted batch create');
136+
_chunkSubscription?.cancel();
137+
_chunkSubscription = null;
138+
WakelockPlus.disable();
139+
if (mounted) {
140+
setState(() {
141+
_isCreating = false;
142+
_progressCreated = 0;
143+
_progressErrors = 0;
144+
});
145+
ScaffoldMessenger.of(context).showSnackBar(
146+
const SnackBar(content: Text('Import abgebrochen.')),
147+
);
148+
}
149+
}
150+
149151
Future<void> _printCredentials() async {
150152
final credentials = _batchResult?.credentials ?? [];
151153
if (credentials.isEmpty) return;
@@ -262,21 +264,33 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
262264
style: AppStyles.subtitle,
263265
),
264266
const Gap(8),
265-
ElevatedButton.icon(
266-
style: AppStyles.actionButtonStyle,
267-
onPressed: _isCreating ? null : _createUsers,
268-
icon: _isCreating
269-
? const SizedBox(
270-
width: 20,
271-
height: 20,
272-
child: CircularProgressIndicator(strokeWidth: 2),
273-
)
274-
: const Icon(Icons.person_add),
275-
label: Text(
276-
_isCreating
277-
? 'Wird erstellt… ($_progressCreated / ${_parseResult!.rows.length}, $_progressErrors Fehler)'
278-
: 'Benutzer anlegen',
279-
),
267+
Row(
268+
children: [
269+
ElevatedButton.icon(
270+
style: AppStyles.actionButtonStyle,
271+
onPressed: _isCreating ? null : _createUsers,
272+
icon: _isCreating
273+
? const SizedBox(
274+
width: 20,
275+
height: 20,
276+
child: CircularProgressIndicator(strokeWidth: 2),
277+
)
278+
: const Icon(Icons.person_add),
279+
label: Text(
280+
_isCreating
281+
? 'Wird erstellt… ($_progressCreated / ${_parseResult!.rows.length}, $_progressErrors Fehler)'
282+
: 'Benutzer anlegen',
283+
),
284+
),
285+
if (_isCreating) ...[
286+
const Gap(12),
287+
OutlinedButton.icon(
288+
onPressed: _abortCreate,
289+
icon: const Icon(Icons.cancel_outlined),
290+
label: const Text('Abbrechen'),
291+
),
292+
],
293+
],
280294
),
281295
],
282296
],

school_data_hub_flutter/macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import screen_retriever_macos
2222
import shared_preferences_foundation
2323
import sqflite_darwin
2424
import url_launcher_macos
25+
import wakelock_plus
2526
import window_manager
2627

2728
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -42,5 +43,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
4243
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
4344
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
4445
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
46+
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
4547
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
4648
}

school_data_hub_flutter/pubspec.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,6 +1898,22 @@ packages:
18981898
url: "https://pub.dev"
18991899
source: hosted
19001900
version: "15.0.0"
1901+
wakelock_plus:
1902+
dependency: "direct main"
1903+
description:
1904+
name: wakelock_plus
1905+
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
1906+
url: "https://pub.dev"
1907+
source: hosted
1908+
version: "1.4.0"
1909+
wakelock_plus_platform_interface:
1910+
dependency: transitive
1911+
description:
1912+
name: wakelock_plus_platform_interface
1913+
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
1914+
url: "https://pub.dev"
1915+
source: hosted
1916+
version: "1.3.0"
19011917
watch_it:
19021918
dependency: transitive
19031919
description:

school_data_hub_flutter/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ dependencies:
9898
pdfrx: ^2.2.24
9999
flutter_svg: ^2.2.3
100100
terminate_restart: ^1.0.11
101+
wakelock_plus: ^1.4.0
101102
dev_dependencies:
102103
flutter_lints: '>=3.0.0 <6.0.0'
103104
flutter_test:

0 commit comments

Comments
 (0)