1+ import 'dart:async' ;
2+
13import 'package:equatable/equatable.dart' ;
24import 'package:flutter/material.dart' ;
35import 'package:flutter_bloc/flutter_bloc.dart' ;
46import 'package:injectable/injectable.dart' ;
7+ import 'package:on_time_front/domain/entities/preparation_entity.dart' ;
58import 'package:on_time_front/domain/entities/schedule_entity.dart' ;
69import 'package:on_time_front/domain/use-cases/delete_schedule_use_case.dart' ;
10+ import 'package:on_time_front/domain/use-cases/get_preparation_by_schedule_id_use_case.dart' ;
711import 'package:on_time_front/domain/use-cases/get_schedules_by_date_use_case.dart' ;
12+ import 'package:on_time_front/domain/use-cases/load_preparation_by_schedule_id_use_case.dart' ;
813import 'package:on_time_front/domain/use-cases/load_schedules_for_month_use_case.dart' ;
14+ import 'package:on_time_front/domain/use-cases/stream_preparations_use_case.dart' ;
915
1016part 'monthly_schedules_event.dart' ;
1117part 'monthly_schedules_state.dart' ;
@@ -17,15 +23,40 @@ class MonthlySchedulesBloc
1723 this ._loadSchedulesForMonthUseCase,
1824 this ._getSchedulesByDateUseCase,
1925 this ._deleteScheduleUseCase,
26+ this ._loadPreparationByScheduleIdUseCase,
27+ this ._getPreparationByScheduleIdUseCase,
28+ this ._streamPreparationsUseCase,
2029 ) : super (MonthlySchedulesState ()) {
2130 on < MonthlySchedulesSubscriptionRequested > (_onSubscriptionRequested);
2231 on < MonthlySchedulesMonthAdded > (_onMonthAdded);
2332 on < MonthlySchedulesScheduleDeleted > (_onScheduleDeleted);
33+ on < MonthlySchedulesVisibleDateChanged > (_onVisibleDateChanged);
34+ on < MonthlySchedulesPreparationsPrefetchRequested > (
35+ _onPreparationsPrefetchRequested,
36+ );
37+ on < MonthlySchedulesPreparationsStreamChanged > (
38+ _onPreparationsStreamChanged,
39+ );
40+
41+ _preparationSubscription = _streamPreparationsUseCase ().listen (
42+ (preparations) {
43+ add (
44+ MonthlySchedulesPreparationsStreamChanged (
45+ preparations: preparations,
46+ ),
47+ );
48+ },
49+ );
2450 }
2551
2652 final LoadSchedulesForMonthUseCase _loadSchedulesForMonthUseCase;
2753 final GetSchedulesByDateUseCase _getSchedulesByDateUseCase;
2854 final DeleteScheduleUseCase _deleteScheduleUseCase;
55+ final LoadPreparationByScheduleIdUseCase _loadPreparationByScheduleIdUseCase;
56+ final GetPreparationByScheduleIdUseCase _getPreparationByScheduleIdUseCase;
57+ final StreamPreparationsUseCase _streamPreparationsUseCase;
58+
59+ StreamSubscription <Map <String , PreparationEntity >>? _preparationSubscription;
2960
3061 Future <void > _onSubscriptionRequested (
3162 MonthlySchedulesSubscriptionRequested event,
@@ -37,27 +68,17 @@ class MonthlySchedulesBloc
3768
3869 await emit.forEach (
3970 _getSchedulesByDateUseCase (event.startDate, event.endDate),
40- onData: (schedules) => state.copyWith (
41- status: () => MonthlySchedulesStatus .success,
42- schedules: () => schedules.fold <Map <DateTime , List <ScheduleEntity >>>(
43- {},
44- (previousValue, element) {
45- final scheduleTime = DateTime (
46- element.scheduleTime.year,
47- element.scheduleTime.month,
48- element.scheduleTime.day,
49- );
50- if (previousValue.containsKey (scheduleTime)) {
51- previousValue[scheduleTime]! .add (element);
52- } else {
53- previousValue[scheduleTime] = [element];
54- }
55- return previousValue;
56- },
57- ),
58- startDate: () => event.startDate,
59- endDate: () => event.endDate,
60- ),
71+ onData: (schedules) {
72+ final groupedSchedules = _groupSchedulesByDate (schedules);
73+ final nextState = state.copyWith (
74+ status: () => MonthlySchedulesStatus .success,
75+ schedules: () => groupedSchedules,
76+ startDate: () => event.startDate,
77+ endDate: () => event.endDate,
78+ );
79+ _requestVisibleDatePreparationPrefetch (nextState);
80+ return nextState;
81+ },
6182 onError: (error, stackTrace) => state.copyWith (
6283 status: () => MonthlySchedulesStatus .error,
6384 ),
@@ -106,27 +127,15 @@ class MonthlySchedulesBloc
106127 await emit.forEach (
107128 _getSchedulesByDateUseCase (startDate, endDate),
108129 onData: (schedules) {
109- return state.copyWith (
130+ final groupedSchedules = _groupSchedulesByDate (schedules);
131+ final nextState = state.copyWith (
110132 status: () => MonthlySchedulesStatus .success,
111- schedules: () => schedules.fold <Map <DateTime , List <ScheduleEntity >>>(
112- {},
113- (previousValue, element) {
114- final scheduleTime = DateTime (
115- element.scheduleTime.year,
116- element.scheduleTime.month,
117- element.scheduleTime.day,
118- );
119- if (previousValue.containsKey (scheduleTime)) {
120- previousValue[scheduleTime]! .add (element);
121- } else {
122- previousValue[scheduleTime] = [element];
123- }
124- return previousValue;
125- },
126- ),
133+ schedules: () => groupedSchedules,
127134 startDate: () => startDate,
128135 endDate: () => endDate,
129136 );
137+ _requestVisibleDatePreparationPrefetch (nextState);
138+ return nextState;
130139 },
131140 onError: (error, stackTrace) {
132141 return state.copyWith (
@@ -141,8 +150,12 @@ class MonthlySchedulesBloc
141150 Emitter <MonthlySchedulesState > emit,
142151 ) async {
143152 try {
153+ final updatedPreparationMap =
154+ Map <String , Duration >.from (state.preparationDurationByScheduleId)
155+ ..remove (event.schedule.id);
144156 emit (state.copyWith (
145157 lastDeletedSchedule: () => event.schedule,
158+ preparationDurationByScheduleId: () => updatedPreparationMap,
146159 ));
147160 await _deleteScheduleUseCase (event.schedule);
148161 } catch (e) {
@@ -151,4 +164,160 @@ class MonthlySchedulesBloc
151164 ));
152165 }
153166 }
167+
168+ void _onVisibleDateChanged (
169+ MonthlySchedulesVisibleDateChanged event,
170+ Emitter <MonthlySchedulesState > emit,
171+ ) {
172+ final normalizedDate =
173+ DateTime (event.date.year, event.date.month, event.date.day);
174+ final nextState = state.copyWith (visibleDate: () => normalizedDate);
175+ emit (nextState);
176+ _requestVisibleDatePreparationPrefetch (nextState);
177+ }
178+
179+ Future <void > _onPreparationsPrefetchRequested (
180+ MonthlySchedulesPreparationsPrefetchRequested event,
181+ Emitter <MonthlySchedulesState > emit,
182+ ) async {
183+ final missingIds = event.scheduleIds
184+ .where ((id) => ! state.preparationDurationByScheduleId.containsKey (id))
185+ .toSet ();
186+ if (missingIds.isEmpty) {
187+ return ;
188+ }
189+
190+ final fetchedDurations = < String , Duration > {};
191+ var hasUpdates = false ;
192+
193+ for (final scheduleId in missingIds) {
194+ try {
195+ await _loadPreparationByScheduleIdUseCase (scheduleId);
196+ if (state.preparationDurationByScheduleId.containsKey (scheduleId)) {
197+ // Stream update already has fresher data.
198+ continue ;
199+ }
200+ final preparation =
201+ await _getPreparationByScheduleIdUseCase (scheduleId);
202+ fetchedDurations[scheduleId] = preparation.totalDuration;
203+ hasUpdates = true ;
204+ } catch (_) {
205+ // Keep fallback UI when loading fails.
206+ }
207+ }
208+
209+ if (hasUpdates) {
210+ final updatedMap = Map <String , Duration >.from (
211+ state.preparationDurationByScheduleId,
212+ )..addAll (fetchedDurations);
213+ emit (
214+ state.copyWith (
215+ preparationDurationByScheduleId: () => updatedMap,
216+ ),
217+ );
218+ }
219+ }
220+
221+ void _onPreparationsStreamChanged (
222+ MonthlySchedulesPreparationsStreamChanged event,
223+ Emitter <MonthlySchedulesState > emit,
224+ ) {
225+ final cachedScheduleIds = _getCachedScheduleIds (state.schedules);
226+ if (cachedScheduleIds.isEmpty) {
227+ return ;
228+ }
229+
230+ final nextDurations = Map <String , Duration >.from (
231+ state.preparationDurationByScheduleId,
232+ );
233+ var hasChange = false ;
234+
235+ for (final entry in event.preparations.entries) {
236+ if (! cachedScheduleIds.contains (entry.key)) {
237+ continue ;
238+ }
239+
240+ final nextDuration = entry.value.totalDuration;
241+ if (nextDurations[entry.key] != nextDuration) {
242+ nextDurations[entry.key] = nextDuration;
243+ hasChange = true ;
244+ }
245+ }
246+
247+ if (! hasChange) {
248+ return ;
249+ }
250+
251+ emit (
252+ state.copyWith (
253+ preparationDurationByScheduleId: () => nextDurations,
254+ ),
255+ );
256+ }
257+
258+ void _requestVisibleDatePreparationPrefetch (
259+ MonthlySchedulesState sourceState) {
260+ final visibleDate = sourceState.visibleDate;
261+ if (visibleDate == null ) {
262+ return ;
263+ }
264+ final scheduleIds = _getScheduleIdsForDate (
265+ sourceState.schedules,
266+ visibleDate,
267+ );
268+ if (scheduleIds.isEmpty) {
269+ return ;
270+ }
271+ add (MonthlySchedulesPreparationsPrefetchRequested (
272+ scheduleIds: scheduleIds));
273+ }
274+
275+ Map <DateTime , List <ScheduleEntity >> _groupSchedulesByDate (
276+ List <ScheduleEntity > schedules,
277+ ) {
278+ return schedules.fold <Map <DateTime , List <ScheduleEntity >>>(
279+ {},
280+ (previousValue, element) {
281+ final scheduleTime = DateTime (
282+ element.scheduleTime.year,
283+ element.scheduleTime.month,
284+ element.scheduleTime.day,
285+ );
286+ if (previousValue.containsKey (scheduleTime)) {
287+ previousValue[scheduleTime]! .add (element);
288+ } else {
289+ previousValue[scheduleTime] = [element];
290+ }
291+ return previousValue;
292+ },
293+ );
294+ }
295+
296+ List <String > _getScheduleIdsForDate (
297+ Map <DateTime , List <ScheduleEntity >> schedules,
298+ DateTime date,
299+ ) {
300+ final normalizedDate = DateTime (date.year, date.month, date.day);
301+ return schedules[normalizedDate]
302+ ? .map ((schedule) => schedule.id)
303+ .toList (growable: false ) ??
304+ const < String > [];
305+ }
306+
307+ Set <String > _getCachedScheduleIds (
308+ Map <DateTime , List <ScheduleEntity >> schedules) {
309+ final ids = < String > {};
310+ for (final scheduleList in schedules.values) {
311+ for (final schedule in scheduleList) {
312+ ids.add (schedule.id);
313+ }
314+ }
315+ return ids;
316+ }
317+
318+ @override
319+ Future <void > close () async {
320+ await _preparationSubscription? .cancel ();
321+ return super .close ();
322+ }
154323}
0 commit comments