Skip to content

Commit c2b81a1

Browse files
committed
fix: disable schedule editing after preparation starts
1 parent 6db3ca1 commit c2b81a1

3 files changed

Lines changed: 168 additions & 34 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: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
44
import 'package:intl/intl.dart';
55
import 'package:on_time_front/core/di/di_setup.dart';
66
import 'package:on_time_front/l10n/app_localizations.dart';
7+
import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart';
78
import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc.dart';
89
import 'package:on_time_front/presentation/calendar/component/schedule_detail.dart';
910
import 'package:on_time_front/presentation/schedule_create/screens/schedule_create_screen.dart';
@@ -357,40 +358,55 @@ class _CalendarScreenState extends State<CalendarScreen> {
357358
final schedule =
358359
state.schedules[_selectedDate]![index];
359360

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

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

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:convert';
22

33
import 'package:flutter/material.dart';
4-
import 'package:flutter/rendering.dart';
54
import 'package:flutter/services.dart';
65
import 'package:flutter_test/flutter_test.dart';
76
import 'package:flutter_swipe_action_cell/core/cell.dart';
@@ -38,6 +37,7 @@ void main() {
3837
WidgetTester tester, {
3938
ScheduleEntity? customSchedule,
4039
Duration? preparationTime,
40+
bool isEarlyStarted = false,
4141
}) async {
4242
final targetSchedule = customSchedule ?? schedule;
4343
await tester.pumpWidget(
@@ -51,6 +51,7 @@ void main() {
5151
body: ScheduleDetail(
5252
schedule: targetSchedule,
5353
preparationTime: preparationTime,
54+
isEarlyStarted: isEarlyStarted,
5455
),
5556
),
5657
),
@@ -88,6 +89,25 @@ void main() {
8889
.reduce((a, b) => a > b ? a : b);
8990
}
9091

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+
91111
testWidgets('expanded tile shows travel, preparation, spare in order',
92112
(tester) async {
93113
await pumpScheduleDetail(tester,
@@ -176,4 +196,80 @@ void main() {
176196
expect(find.text('Office'), findsNothing);
177197
expect(find.text('09:00'), findsNothing);
178198
});
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+
});
179275
}

0 commit comments

Comments
 (0)