Skip to content

Commit 15abc37

Browse files
Implement hub stream in competence report feature
1 parent 5cf5cd6 commit 15abc37

9 files changed

Lines changed: 222 additions & 60 deletions

File tree

school_data_hub_client/lib/src/protocol/_features/hub/models/hub_object_type.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ enum HubObjectType implements _i1.SerializableModel {
1818
schoolList,
1919
authorization,
2020
supportCategory,
21-
competence;
21+
competence,
22+
competenceReport,
23+
competenceReportCheck,
24+
competenceReportItem;
2225

2326
static HubObjectType fromJson(int index) {
2427
switch (index) {
@@ -36,6 +39,12 @@ enum HubObjectType implements _i1.SerializableModel {
3639
return HubObjectType.supportCategory;
3740
case 6:
3841
return HubObjectType.competence;
42+
case 7:
43+
return HubObjectType.competenceReport;
44+
case 8:
45+
return HubObjectType.competenceReportCheck;
46+
case 9:
47+
return HubObjectType.competenceReportItem;
3948
default:
4049
throw ArgumentError(
4150
'Value "$index" cannot be converted to "HubObjectType"');

school_data_hub_flutter/lib/core/init/init_on_user_auth.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,13 @@ class InitOnUserAuth {
147147

148148
di.registerSingletonAsync<CompetenceReportItemManager>(
149149
() => CompetenceReportItemManager().init(),
150+
dependsOn: [HubStreamService],
150151
dispose: (m) => m.dispose(),
151152
);
152153

153-
di.registerSingletonWithDependencies<CompetenceReportManager>(
154-
() => CompetenceReportManager(),
155-
dependsOn: [CompetenceReportItemManager],
154+
di.registerSingletonAsync<CompetenceReportManager>(
155+
() => CompetenceReportManager().init(),
156+
dependsOn: [CompetenceReportItemManager, HubStreamService],
156157
dispose: (m) => m.dispose(),
157158
);
158159

school_data_hub_flutter/lib/features/learning/competence_report/domain/competence_report_item_manager.dart

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter_it/flutter_it.dart';
35
import 'package:school_data_hub_client/school_data_hub_client.dart';
6+
import 'package:school_data_hub_flutter/common/services/hub_stream_service.dart';
47
import 'package:school_data_hub_flutter/common/services/notification_service.dart';
58
import 'package:school_data_hub_flutter/features/learning/competence_report/data/competence_report_item_api_service.dart';
69
import 'package:school_data_hub_flutter/features/learning/competence_report/domain/competence_report_helper.dart';
@@ -12,17 +15,50 @@ class CompetenceReportItemManager {
1215
final _items = ValueNotifier<List<CompetenceReportItem>>([]);
1316
ValueListenable<List<CompetenceReportItem>> get items => _items;
1417

18+
StreamSubscription<dynamic>? _hubSubscription;
19+
1520
CompetenceReportItemManager();
1621

1722
Future<CompetenceReportItemManager> init() async {
1823
await fetchItems();
24+
_hubSubscription = di<HubStreamService>().events.listen(_onHubEvent);
1925
return this;
2026
}
2127

2228
void dispose() {
29+
_hubSubscription?.cancel();
30+
_hubSubscription = null;
2331
_items.dispose();
2432
}
2533

34+
void _onHubEvent(dynamic event) {
35+
if (event is CompetenceReportItem) {
36+
upsertItemFromStream(event);
37+
} else if (event is HubDeleteEvent &&
38+
event.objectType == HubObjectType.competenceReportItem) {
39+
deleteItemFromStream(event.id);
40+
} else if (event is HubReconnected) {
41+
fetchItems();
42+
}
43+
}
44+
45+
void upsertItemFromStream(CompetenceReportItem item) {
46+
final list = List<CompetenceReportItem>.from(_items.value);
47+
final index = list.indexWhere((i) =>
48+
(i.id != null && i.id == item.id) || i.publicId == item.publicId);
49+
if (index >= 0) {
50+
list[index] = item;
51+
} else {
52+
list.add(item);
53+
}
54+
_items.value = CompetenceReportHelper.sortItems(list);
55+
}
56+
57+
void deleteItemFromStream(int itemId) {
58+
final list = _items.value.where((i) => i.id != itemId).toList();
59+
_items.value = CompetenceReportHelper.sortItems(list);
60+
}
61+
2662
Future<void> fetchItems() async {
2763
final fetched = await _apiService.fetchAllCompetenceReportItems();
2864
_items.value = CompetenceReportHelper.sortItems(fetched);
@@ -39,18 +75,13 @@ class CompetenceReportItemManager {
3975
List<String>? level,
4076
int? order,
4177
}) async {
42-
final newItem = await _apiService.postCompetenceReportItem(
78+
await _apiService.postCompetenceReportItem(
4379
parentItem: parentItem,
4480
name: name,
4581
level: level,
4682
order: order,
4783
);
4884

49-
_items.value = CompetenceReportHelper.sortItems([
50-
..._items.value,
51-
newItem,
52-
]);
53-
5485
_notificationService.showSnackBar(
5586
NotificationType.success,
5687
'Zeugniskompetenz erstellt',
@@ -66,12 +97,9 @@ class CompetenceReportItemManager {
6697

6798
final item = _items.value[index];
6899
final updatedItem = item.copyWith(order: order);
69-
final verified = await _apiService.updateCompetenceReportItem(updatedItem);
70-
// Update in-place without notifying listeners.
71-
// The sortable widgets manage their own visual order via local state.
72-
// Call sortAndNotifyItems() when done (e.g. on page dispose)
73-
// to commit the sorted order for other widgets.
74-
_items.value[index] = verified;
100+
await _apiService.updateCompetenceReportItem(updatedItem);
101+
// Hub stream will deliver the updated item; sortAndNotifyItems() on page
102+
// dispose commits order for other widgets.
75103
}
76104

77105
/// Sorts the items list by order and notifies listeners.
@@ -84,14 +112,7 @@ class CompetenceReportItemManager {
84112
}
85113

86114
Future<void> updateItem(CompetenceReportItem item) async {
87-
final updated = await _apiService.updateCompetenceReportItem(item);
88-
89-
final list = List<CompetenceReportItem>.from(_items.value);
90-
final index = list.indexWhere((i) => i.publicId == updated.publicId);
91-
if (index != -1) {
92-
list[index] = updated;
93-
}
94-
_items.value = list;
115+
await _apiService.updateCompetenceReportItem(item);
95116

96117
_notificationService.showSnackBar(
97118
NotificationType.success,
@@ -102,10 +123,6 @@ class CompetenceReportItemManager {
102123
Future<void> deleteItem(int publicId) async {
103124
final success = await _apiService.deleteCompetenceReportItem(publicId);
104125
if (success) {
105-
final list = List<CompetenceReportItem>.from(_items.value);
106-
list.removeWhere((i) => i.publicId == publicId);
107-
_items.value = list;
108-
109126
_notificationService.showSnackBar(
110127
NotificationType.success,
111128
'Zeugniskompetenz gelöscht',

school_data_hub_flutter/lib/features/learning/competence_report/domain/competence_report_manager.dart

Lines changed: 121 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter_it/flutter_it.dart';
35
import 'package:school_data_hub_client/school_data_hub_client.dart';
6+
import 'package:school_data_hub_flutter/common/services/hub_stream_service.dart';
47
import 'package:school_data_hub_flutter/common/services/notification_service.dart';
58
import 'package:school_data_hub_flutter/core/session/hub_session_manager.dart';
69
import 'package:school_data_hub_flutter/features/learning/competence_report/data/competence_report_api_service.dart';
@@ -16,12 +19,128 @@ class CompetenceReportManager {
1619
ValueListenable<Map<int, List<CompetenceReport>>> get reportsByPupil =>
1720
_reportsByPupil;
1821

22+
StreamSubscription<dynamic>? _hubSubscription;
23+
1924
CompetenceReportManager();
2025

26+
Future<CompetenceReportManager> init() async {
27+
_hubSubscription = di<HubStreamService>().events.listen(_onHubEvent);
28+
return this;
29+
}
30+
2131
void dispose() {
32+
_hubSubscription?.cancel();
33+
_hubSubscription = null;
2234
_reportsByPupil.dispose();
2335
}
2436

37+
void _onHubEvent(dynamic event) {
38+
if (event is CompetenceReport) {
39+
upsertReportFromStream(event);
40+
} else if (event is CompetenceReportCheck) {
41+
upsertCheckFromStream(event);
42+
} else if (event is HubDeleteEvent) {
43+
if (event.objectType == HubObjectType.competenceReport) {
44+
deleteReportFromStream(event.id);
45+
} else if (event.objectType == HubObjectType.competenceReportCheck) {
46+
deleteCheckFromStream(event.id);
47+
}
48+
} else if (event is HubReconnected) {
49+
for (final pupilId in _reportsByPupil.value.keys.toList()) {
50+
fetchReportsForPupil(pupilId);
51+
}
52+
}
53+
}
54+
55+
void upsertReportFromStream(CompetenceReport report) {
56+
final map = Map<int, List<CompetenceReport>>.from(_reportsByPupil.value);
57+
final list = List<CompetenceReport>.from(map[report.pupilId] ?? []);
58+
final index = list.indexWhere((r) =>
59+
r.id == report.id ||
60+
(r.reportId == report.reportId && report.reportId.isNotEmpty));
61+
if (index >= 0) {
62+
list[index] = report;
63+
} else {
64+
list.add(report);
65+
}
66+
map[report.pupilId] = list;
67+
_reportsByPupil.value = map;
68+
}
69+
70+
void upsertCheckFromStream(CompetenceReportCheck check) {
71+
final map = _reportsByPupil.value;
72+
final list = map[check.pupilId];
73+
if (list == null || list.isEmpty) {
74+
fetchReportsForPupil(check.pupilId);
75+
return;
76+
}
77+
final reportIndex = list.indexWhere((r) => r.id == check.competenceReportId);
78+
if (reportIndex == -1) {
79+
fetchReportsForPupil(check.pupilId);
80+
return;
81+
}
82+
final report = list[reportIndex];
83+
final checks = List<CompetenceReportCheck>.from(
84+
report.competenceReportChecks ?? [],
85+
);
86+
final checkIndex = checks.indexWhere((c) =>
87+
c.id == check.id ||
88+
(c.publicId == check.publicId && check.publicId.isNotEmpty));
89+
if (checkIndex >= 0) {
90+
checks[checkIndex] = check;
91+
} else {
92+
checks.add(check);
93+
}
94+
final updatedReport = report.copyWith(competenceReportChecks: checks);
95+
final newList = List<CompetenceReport>.from(list);
96+
newList[reportIndex] = updatedReport;
97+
final newMap = Map<int, List<CompetenceReport>>.from(map);
98+
newMap[check.pupilId] = newList;
99+
_reportsByPupil.value = newMap;
100+
}
101+
102+
void deleteReportFromStream(int reportId) {
103+
final map = Map<int, List<CompetenceReport>>.from(_reportsByPupil.value);
104+
var changed = false;
105+
for (final entry in map.entries) {
106+
final list = entry.value;
107+
final newList = list.where((r) => r.id != reportId).toList();
108+
if (newList.length != list.length) {
109+
map[entry.key] = newList;
110+
changed = true;
111+
break;
112+
}
113+
}
114+
if (changed) {
115+
_reportsByPupil.value = map;
116+
}
117+
}
118+
119+
void deleteCheckFromStream(int checkId) {
120+
final map = Map<int, List<CompetenceReport>>.from(_reportsByPupil.value);
121+
var changed = false;
122+
for (final pupilId in map.keys) {
123+
final list = map[pupilId]!;
124+
for (var i = 0; i < list.length; i++) {
125+
final report = list[i];
126+
final checks = report.competenceReportChecks;
127+
if (checks == null) continue;
128+
final newChecks = checks.where((c) => c.id != checkId).toList();
129+
if (newChecks.length != checks.length) {
130+
final newList = List<CompetenceReport>.from(list);
131+
newList[i] = report.copyWith(competenceReportChecks: newChecks);
132+
map[pupilId] = newList;
133+
changed = true;
134+
break;
135+
}
136+
}
137+
if (changed) break;
138+
}
139+
if (changed) {
140+
_reportsByPupil.value = map;
141+
}
142+
}
143+
25144
List<CompetenceReport> getReportsForPupil(int pupilId) {
26145
return _reportsByPupil.value[pupilId] ?? [];
27146
}
@@ -40,18 +159,14 @@ class CompetenceReportManager {
40159
required DateTime achievedAt,
41160
}) async {
42161
final createdBy = di<HubSessionManager>().userName!;
43-
final report = await _reportApiService.postCompetenceReport(
162+
await _reportApiService.postCompetenceReport(
44163
pupilId: pupilId,
45164
schoolSemesterId: schoolSemesterId,
46165
achievement: achievement,
47166
achievedAt: achievedAt,
48167
createdBy: createdBy,
49168
);
50169

51-
final map = Map<int, List<CompetenceReport>>.from(_reportsByPupil.value);
52-
map[pupilId] = [...(map[pupilId] ?? []), report];
53-
_reportsByPupil.value = map;
54-
55170
_notificationService.showSnackBar(
56171
NotificationType.success,
57172
'Zeugnis erstellt',
@@ -66,23 +181,14 @@ class CompetenceReportManager {
66181
({DateTime? value})? modifiedAt,
67182
}) async {
68183
final modifiedBy = di<HubSessionManager>().userName!;
69-
final updated = await _reportApiService.updateCompetenceReport(
184+
await _reportApiService.updateCompetenceReport(
70185
reportId,
71186
achievement: achievement,
72187
achievedAt: achievedAt,
73188
modifiedBy: (value: modifiedBy),
74189
modifiedAt: modifiedAt ?? (value: DateTime.now().toUtc()),
75190
);
76191

77-
final map = Map<int, List<CompetenceReport>>.from(_reportsByPupil.value);
78-
final list = List<CompetenceReport>.from(map[pupilId] ?? []);
79-
final index = list.indexWhere((r) => r.reportId == reportId);
80-
if (index != -1) {
81-
list[index] = updated;
82-
}
83-
map[pupilId] = list;
84-
_reportsByPupil.value = map;
85-
86192
_notificationService.showSnackBar(
87193
NotificationType.success,
88194
'Zeugnis aktualisiert',
@@ -95,13 +201,6 @@ class CompetenceReportManager {
95201
}) async {
96202
final success = await _reportApiService.deleteCompetenceReport(reportId);
97203
if (success) {
98-
final map =
99-
Map<int, List<CompetenceReport>>.from(_reportsByPupil.value);
100-
final list = List<CompetenceReport>.from(map[pupilId] ?? []);
101-
list.removeWhere((r) => r.reportId == reportId);
102-
map[pupilId] = list;
103-
_reportsByPupil.value = map;
104-
105204
_notificationService.showSnackBar(
106205
NotificationType.success,
107206
'Zeugnis gelöscht',
@@ -128,8 +227,6 @@ class CompetenceReportManager {
128227
shouldPrint: shouldPrint,
129228
);
130229

131-
await fetchReportsForPupil(pupilId);
132-
133230
_notificationService.showSnackBar(
134231
NotificationType.success,
135232
'Zeugniseintrag erstellt',
@@ -150,8 +247,6 @@ class CompetenceReportManager {
150247
shouldPrint: shouldPrint,
151248
);
152249

153-
await fetchReportsForPupil(pupilId);
154-
155250
_notificationService.showSnackBar(
156251
NotificationType.success,
157252
'Zeugniseintrag aktualisiert',
@@ -165,8 +260,6 @@ class CompetenceReportManager {
165260
final success =
166261
await _checkApiService.deleteCompetenceReportCheck(publicId);
167262
if (success) {
168-
await fetchReportsForPupil(pupilId);
169-
170263
_notificationService.showSnackBar(
171264
NotificationType.success,
172265
'Zeugniseintrag gelöscht',

school_data_hub_server/lib/src/_features/hub/models/hub_object_type.spy.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ values:
77
- authorization
88
- supportCategory
99
- competence
10+
- competenceReport
11+
- competenceReportCheck
12+
- competenceReportItem

0 commit comments

Comments
 (0)