1+ import 'dart:async' ;
12import 'dart:typed_data' ;
23
34import 'package:flutter/material.dart' ;
@@ -7,8 +8,10 @@ import 'package:printing/printing.dart';
78import 'package:school_data_hub_flutter/app_utils/pdf_viewer_page.dart' ;
89import 'package:school_data_hub_flutter/common/theme/app_colors.dart' ;
910import 'package:school_data_hub_flutter/common/theme/styles.dart' ;
11+ import 'package:school_data_hub_flutter/common/widgets/bottom_nav_bar/generic_bottom_nav_bar.dart' ;
1012import 'package:school_data_hub_flutter/common/widgets/generic_components/generic_app_bar.dart' ;
1113import 'package:school_data_hub_flutter/features/user/data/staff_excel_import_parser.dart' ;
14+ import 'package:school_data_hub_client/school_data_hub_client.dart' ;
1215import 'package:school_data_hub_flutter/features/user/domain/batch_create_result.dart' ;
1316import 'package:school_data_hub_flutter/features/user/domain/user_manager.dart' ;
1417import 'package:school_data_hub_flutter/features/user/presentation/batch_import_users/staff_credentials_pdf_service.dart' ;
@@ -24,6 +27,15 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
2427 StaffImportParseResult ? _parseResult;
2528 BatchCreateResult ? _batchResult;
2629 bool _isCreating = false ;
30+ int _progressCreated = 0 ;
31+ int _progressErrors = 0 ;
32+ StreamSubscription <BatchCreateUserEvent >? _streamSubscription;
33+
34+ @override
35+ void dispose () {
36+ _streamSubscription? .cancel ();
37+ super .dispose ();
38+ }
2739
2840 Future <void > _pickFile () async {
2941 final result = await StaffExcelImportParser .pickAndParse ();
@@ -38,16 +50,63 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
3850 Future <void > _createUsers () async {
3951 final rows = _parseResult? .rows ?? [];
4052 if (rows.isEmpty) return ;
41- setState (() => _isCreating = true );
53+ setState (() {
54+ _isCreating = true ;
55+ _batchResult = null ;
56+ _progressCreated = 0 ;
57+ _progressErrors = 0 ;
58+ });
59+ final userManager = di <UserManager >();
60+ final credentials = < StaffCredentialEntry > [];
61+ final errors = < BatchCreateError > [];
62+
4263 try {
43- final userManager = di <UserManager >();
44- final result = await userManager.batchCreateUsersFromImportRows (rows);
45- if (mounted) {
46- setState (() {
47- _batchResult = result;
48- _isCreating = false ;
49- });
50- }
64+ final stream = userManager.batchCreateUsersStreamFromImportRows (rows);
65+ _streamSubscription = stream.listen (
66+ (event) {
67+ if (! mounted) return ;
68+ if (event.credential != null ) {
69+ final c = event.credential! ;
70+ credentials.add (StaffCredentialEntry (
71+ userName: c.userName,
72+ fullName: c.fullName,
73+ email: c.email,
74+ password: c.password,
75+ ));
76+ setState (() => _progressCreated = credentials.length);
77+ } else if (event.error != null ) {
78+ final e = event.error! ;
79+ errors.add (BatchCreateError (
80+ rowIndex: e.rowIndex,
81+ userNameOrKurzel: e.userNameOrKurzel,
82+ message: e.message,
83+ ));
84+ setState (() => _progressErrors = errors.length);
85+ }
86+ },
87+ onError: (Object e) {
88+ if (mounted) {
89+ setState (() => _isCreating = false );
90+ ScaffoldMessenger .of (context).showSnackBar (
91+ SnackBar (content: Text ('Fehler: $e ' )),
92+ );
93+ }
94+ },
95+ onDone: () async {
96+ if (! mounted) return ;
97+ await userManager.fetchUsersCommand.runAsync ();
98+ setState (() {
99+ _batchResult = BatchCreateResult (
100+ credentials: credentials,
101+ errors: errors,
102+ );
103+ _isCreating = false ;
104+ _progressCreated = 0 ;
105+ _progressErrors = 0 ;
106+ });
107+ },
108+ cancelOnError: false ,
109+ );
51110 } catch (e) {
52111 if (mounted) {
53112 setState (() => _isCreating = false );
@@ -61,7 +120,9 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
61120 Future <void > _printCredentials () async {
62121 final credentials = _batchResult? .credentials ?? [];
63122 if (credentials.isEmpty) return ;
64- final bytes = await StaffCredentialsPdfService .generatePdfBytes (credentials);
123+ final bytes = await StaffCredentialsPdfService .generatePdfBytes (
124+ credentials,
125+ );
65126 if (bytes.isEmpty || ! mounted) return ;
66127 await Printing .layoutPdf (
67128 onLayout: (_) async => Uint8List .fromList (bytes),
@@ -75,9 +136,7 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
75136 final file = await StaffCredentialsPdfService .generatePdfFile (credentials);
76137 if (file == null || ! mounted) return ;
77138 await Navigator .of (context).push <void >(
78- MaterialPageRoute (
79- builder: (context) => PdfViewerPage (pdfFile: file),
80- ),
139+ MaterialPageRoute (builder: (context) => PdfViewerPage (pdfFile: file)),
81140 );
82141 }
83142
@@ -98,10 +157,7 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
98157 crossAxisAlignment: CrossAxisAlignment .stretch,
99158 children: [
100159 // Step 1: Pick file
101- const Text (
102- '1. Datei auswählen' ,
103- style: AppStyles .subtitle,
104- ),
160+ const Text ('1. Datei auswählen' , style: AppStyles .subtitle),
105161 const Gap (8 ),
106162 ElevatedButton .icon (
107163 style: AppStyles .actionButtonStyle,
@@ -119,7 +175,12 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
119175 child: Column (
120176 crossAxisAlignment: CrossAxisAlignment .start,
121177 children: _parseResult! .errors
122- .map ((e) => Text (e, style: const TextStyle (fontSize: 12 )))
178+ .map (
179+ (e) => Text (
180+ e,
181+ style: const TextStyle (fontSize: 12 ),
182+ ),
183+ )
123184 .toList (),
124185 ),
125186 ),
@@ -135,7 +196,9 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
135196 scrollDirection: Axis .horizontal,
136197 child: SingleChildScrollView (
137198 child: DataTable (
138- headingRowColor: WidgetStateProperty .all (Colors .grey.shade300),
199+ headingRowColor: WidgetStateProperty .all (
200+ Colors .grey.shade300,
201+ ),
139202 columns: const [
140203 DataColumn (label: Text ('Vorname' )),
141204 DataColumn (label: Text ('Nachname' )),
@@ -165,7 +228,10 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
165228 ),
166229 const Gap (16 ),
167230 // Step 2: Create users
168- const Text ('2. Benutzer anlegen' , style: AppStyles .subtitle),
231+ const Text (
232+ '2. Benutzer anlegen' ,
233+ style: AppStyles .subtitle,
234+ ),
169235 const Gap (8 ),
170236 ElevatedButton .icon (
171237 style: AppStyles .actionButtonStyle,
@@ -177,7 +243,11 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
177243 child: CircularProgressIndicator (strokeWidth: 2 ),
178244 )
179245 : const Icon (Icons .person_add),
180- label: Text (_isCreating ? 'Wird erstellt…' : 'Benutzer anlegen' ),
246+ label: Text (
247+ _isCreating
248+ ? 'Wird erstellt… ($_progressCreated / ${_parseResult !.rows .length }, $_progressErrors Fehler)'
249+ : 'Benutzer anlegen' ,
250+ ),
181251 ),
182252 ],
183253 ],
@@ -196,14 +266,20 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
196266 padding: const EdgeInsets .only (bottom: 4 ),
197267 child: Text (
198268 'Zeile ${e .rowIndex } (${e .userNameOrKurzel }): ${e .message }' ,
199- style: TextStyle (fontSize: 12 , color: Colors .red.shade800),
269+ style: TextStyle (
270+ fontSize: 12 ,
271+ color: Colors .red.shade800,
272+ ),
200273 ),
201274 ),
202275 ),
203276 ],
204277 if (_batchResult! .credentials.isNotEmpty) ...[
205278 const Gap (16 ),
206- const Text ('3. Zugangsdaten drucken' , style: AppStyles .subtitle),
279+ const Text (
280+ '3. Zugangsdaten drucken' ,
281+ style: AppStyles .subtitle,
282+ ),
207283 const Gap (8 ),
208284 Row (
209285 children: [
@@ -228,6 +304,7 @@ class _BatchImportUsersPageState extends State<BatchImportUsersPage> {
228304 ),
229305 ),
230306 ),
307+ bottomNavigationBar: const GenericBottomNavBar (),
231308 );
232309 }
233310}
0 commit comments