Skip to content

Commit ca4e8e6

Browse files
authored
Merge pull request #372 from DevKor-github/codex/disable-schedule-edits-after-start
[codex] Disable schedule editing after preparation starts
2 parents 487749e + 2bcd92b commit ca4e8e6

3 files changed

Lines changed: 166 additions & 33 deletions

File tree

‎lib/presentation/calendar/component/schedule_detail.dart‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ class ScheduleDetail extends StatefulWidget {
5656
{super.key,
5757
required this.schedule,
5858
this.preparationTime,
59+
this.isEarlyStarted = false,
5960
this.onDeleted,
6061
this.onEdit});
6162

6263
final ScheduleEntity schedule;
6364
final Duration? preparationTime;
65+
final bool isEarlyStarted;
6466
final VoidCallback? onEdit;
6567
final VoidCallback? onDeleted;
6668

@@ -103,8 +105,10 @@ class _ScheduleDetailState extends State<ScheduleDetail> {
103105

104106
List<SwipeAction> _buildSwipeActions(BuildContext context) {
105107
final theme = Theme.of(context);
108+
final now = DateTime.now();
106109
final canEdit = widget.schedule.doneStatus == ScheduleDoneStatus.notEnded &&
107-
!widget.schedule.scheduleTime.isBefore(DateTime.now());
110+
!_hasPreparationStarted(now) &&
111+
!widget.schedule.scheduleTime.isBefore(now);
108112
return [
109113
SwipeAction(
110114
widthSpace: _actionWidth,
@@ -136,6 +140,24 @@ class _ScheduleDetailState extends State<ScheduleDetail> {
136140
];
137141
}
138142

143+
bool _hasPreparationStarted(DateTime now) {
144+
if (widget.schedule.isStarted || widget.isEarlyStarted) {
145+
return true;
146+
}
147+
148+
final preparationTime = widget.preparationTime;
149+
if (preparationTime == null) {
150+
return false;
151+
}
152+
153+
final preparationStartTime = widget.schedule.scheduleTime.subtract(
154+
widget.schedule.moveTime +
155+
preparationTime +
156+
(widget.schedule.scheduleSpareTime ?? Duration.zero),
157+
);
158+
return !now.isBefore(preparationStartTime);
159+
}
160+
139161
Widget _buildScheduleContent(BuildContext context) {
140162
final theme = Theme.of(context);
141163
return Container(

‎lib/presentation/calendar/screens/calendar_screen.dart‎

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
66
import 'package:intl/intl.dart';
77
import 'package:on_time_front/core/di/di_setup.dart';
88
import 'package:on_time_front/l10n/app_localizations.dart';
9+
import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart';
910
import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart';
1011
import 'package:on_time_front/presentation/calendar/component/schedule_detail.dart';
1112
import 'package:on_time_front/presentation/schedule_create/screens/schedule_create_screen.dart';
@@ -363,40 +364,53 @@ class _CalendarScreenState extends State<CalendarScreen> {
363364
final schedule =
364365
state.schedules[_selectedDate]![index];
365366

366-
return ScheduleDetail(
367-
schedule: schedule,
368-
preparationTime:
369-
state.preparationDurationByScheduleId[
370-
schedule.id],
371-
onEdit: () {
372-
_openEditScheduleSheet(
373-
context,
374-
scheduleId: schedule.id,
367+
return BlocBuilder<ScheduleBloc, ScheduleState>(
368+
builder: (context, scheduleState) {
369+
final isEarlyStarted =
370+
scheduleState.isEarlyStarted &&
371+
scheduleState.schedule?.id ==
372+
schedule.id;
373+
374+
return ScheduleDetail(
375+
schedule: schedule,
376+
preparationTime:
377+
state.preparationDurationByScheduleId[
378+
schedule.id],
379+
isEarlyStarted: isEarlyStarted,
380+
onEdit: () {
381+
_openEditScheduleSheet(
382+
context,
383+
scheduleId: schedule.id,
384+
);
385+
},
386+
onDeleted: () {
387+
showTwoButtonDeleteDialog(
388+
context,
389+
title: AppLocalizations.of(context)!
390+
.scheduleDeleteConfirmTitle,
391+
description: AppLocalizations.of(
392+
context)!
393+
.scheduleDeleteConfirmDescription,
394+
cancelText:
395+
AppLocalizations.of(context)!
396+
.cancel,
397+
confirmText:
398+
AppLocalizations.of(context)!
399+
.deleteScheduleConfirmAction,
400+
).then((confirmed) {
401+
if (confirmed != true ||
402+
!context.mounted) {
403+
return;
404+
}
405+
_monthlySchedulesBloc.add(
406+
MonthlySchedulesScheduleDeleted(
407+
schedule: schedule,
408+
),
409+
);
410+
});
411+
},
375412
);
376413
},
377-
onDeleted: () {
378-
showTwoButtonDeleteDialog(
379-
context,
380-
title: AppLocalizations.of(context)!
381-
.scheduleDeleteConfirmTitle,
382-
description: AppLocalizations.of(context)!
383-
.scheduleDeleteConfirmDescription,
384-
cancelText:
385-
AppLocalizations.of(context)!.cancel,
386-
confirmText: AppLocalizations.of(context)!
387-
.deleteScheduleConfirmAction,
388-
).then((confirmed) {
389-
if (confirmed != true ||
390-
!context.mounted) {
391-
return;
392-
}
393-
_monthlySchedulesBloc.add(
394-
MonthlySchedulesScheduleDeleted(
395-
schedule: schedule,
396-
),
397-
);
398-
});
399-
},
400414
);
401415
},
402416
),

‎test/presentation/calendar/component/schedule_detail_test.dart‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ void main() {
3737
WidgetTester tester, {
3838
ScheduleEntity? customSchedule,
3939
Duration? preparationTime,
40+
bool isEarlyStarted = false,
4041
}) async {
4142
final targetSchedule = customSchedule ?? schedule;
4243
await tester.pumpWidget(
@@ -50,6 +51,7 @@ void main() {
5051
body: ScheduleDetail(
5152
schedule: targetSchedule,
5253
preparationTime: preparationTime,
54+
isEarlyStarted: isEarlyStarted,
5355
),
5456
),
5557
),
@@ -87,6 +89,25 @@ void main() {
8789
.reduce((a, b) => a > b ? a : b);
8890
}
8991

92+
int getActionButtonCount(WidgetTester tester) {
93+
final actionContainers = find.byWidgetPredicate((widget) {
94+
if (widget is! Container) {
95+
return false;
96+
}
97+
final decoration = widget.decoration;
98+
if (decoration is! BoxDecoration) {
99+
return false;
100+
}
101+
final radius = decoration.borderRadius;
102+
if (radius is! BorderRadius) {
103+
return false;
104+
}
105+
return radius.topLeft.x == 12 && decoration.color != null;
106+
});
107+
108+
return actionContainers.evaluate().length;
109+
}
110+
90111
testWidgets('expanded tile shows travel, preparation, spare in order',
91112
(tester) async {
92113
await pumpScheduleDetail(tester,
@@ -175,4 +196,80 @@ void main() {
175196
expect(find.text('Office'), findsNothing);
176197
expect(find.text('09:00'), findsNothing);
177198
});
199+
200+
testWidgets('edit action is available before preparation starts',
201+
(tester) async {
202+
final futureSchedule = ScheduleEntity(
203+
id: 'schedule-2',
204+
place: PlaceEntity(id: 'place-1', placeName: 'Office'),
205+
scheduleName: 'Planning',
206+
scheduleTime: DateTime.now().add(const Duration(hours: 3)),
207+
moveTime: const Duration(minutes: 30),
208+
isChanged: false,
209+
isStarted: false,
210+
scheduleSpareTime: const Duration(minutes: 10),
211+
scheduleNote: '',
212+
);
213+
214+
await pumpScheduleDetail(
215+
tester,
216+
customSchedule: futureSchedule,
217+
preparationTime: const Duration(minutes: 20),
218+
);
219+
220+
await openTrailingActions(tester);
221+
222+
expect(getActionButtonCount(tester), 2);
223+
});
224+
225+
testWidgets('edit action is hidden after preparation start time',
226+
(tester) async {
227+
final scheduleInPreparation = ScheduleEntity(
228+
id: 'schedule-3',
229+
place: PlaceEntity(id: 'place-1', placeName: 'Office'),
230+
scheduleName: 'Planning',
231+
scheduleTime: DateTime.now().add(const Duration(minutes: 30)),
232+
moveTime: const Duration(minutes: 30),
233+
isChanged: false,
234+
isStarted: false,
235+
scheduleSpareTime: const Duration(minutes: 10),
236+
scheduleNote: '',
237+
);
238+
239+
await pumpScheduleDetail(
240+
tester,
241+
customSchedule: scheduleInPreparation,
242+
preparationTime: const Duration(minutes: 20),
243+
);
244+
245+
await openTrailingActions(tester);
246+
247+
expect(getActionButtonCount(tester), 1);
248+
});
249+
250+
testWidgets('edit action is hidden for early-started schedule',
251+
(tester) async {
252+
final futureSchedule = ScheduleEntity(
253+
id: 'schedule-4',
254+
place: PlaceEntity(id: 'place-1', placeName: 'Office'),
255+
scheduleName: 'Planning',
256+
scheduleTime: DateTime.now().add(const Duration(hours: 3)),
257+
moveTime: const Duration(minutes: 30),
258+
isChanged: false,
259+
isStarted: false,
260+
scheduleSpareTime: const Duration(minutes: 10),
261+
scheduleNote: '',
262+
);
263+
264+
await pumpScheduleDetail(
265+
tester,
266+
customSchedule: futureSchedule,
267+
preparationTime: const Duration(minutes: 20),
268+
isEarlyStarted: true,
269+
);
270+
271+
await openTrailingActions(tester);
272+
273+
expect(getActionButtonCount(tester), 1);
274+
});
178275
}

0 commit comments

Comments
 (0)