refactor(device_management): migrate scheduled_activities to Riverpod
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'activity_form_provider.g.dart';
|
||||
|
||||
class ActivityFormState {
|
||||
const ActivityFormState({
|
||||
required this.weekDay,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.showStartPicker = false,
|
||||
this.showEndPicker = false,
|
||||
});
|
||||
|
||||
final int weekDay;
|
||||
final TimeOfDay startTime;
|
||||
final TimeOfDay endTime;
|
||||
final bool showStartPicker;
|
||||
final bool showEndPicker;
|
||||
|
||||
bool get isTimeValid {
|
||||
final startMinutes = startTime.hour * 60 + startTime.minute;
|
||||
final endMinutes = endTime.hour * 60 + endTime.minute;
|
||||
return startMinutes < endMinutes;
|
||||
}
|
||||
|
||||
String get period =>
|
||||
'${startTime.hour.toString().padLeft(2, '0')}'
|
||||
'${startTime.minute.toString().padLeft(2, '0')}'
|
||||
'${endTime.hour.toString().padLeft(2, '0')}'
|
||||
'${endTime.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
ActivityFormState copyWith({
|
||||
int? weekDay,
|
||||
TimeOfDay? startTime,
|
||||
TimeOfDay? endTime,
|
||||
bool? showStartPicker,
|
||||
bool? showEndPicker,
|
||||
}) {
|
||||
return ActivityFormState(
|
||||
weekDay: weekDay ?? this.weekDay,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
showStartPicker: showStartPicker ?? this.showStartPicker,
|
||||
showEndPicker: showEndPicker ?? this.showEndPicker,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ActivityForm extends _$ActivityForm {
|
||||
@override
|
||||
ActivityFormState build(ScheduledActivityEntity? initial, int fallbackWeekDay) {
|
||||
if (initial != null && initial.hasValidPeriod) {
|
||||
return ActivityFormState(
|
||||
weekDay: initial.weekDay,
|
||||
startTime: TimeOfDay(
|
||||
hour: int.parse(initial.period.substring(0, 2)),
|
||||
minute: int.parse(initial.period.substring(2, 4)),
|
||||
),
|
||||
endTime: TimeOfDay(
|
||||
hour: int.parse(initial.period.substring(4, 6)),
|
||||
minute: int.parse(initial.period.substring(6, 8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ActivityFormState(
|
||||
weekDay: fallbackWeekDay,
|
||||
startTime: const TimeOfDay(hour: 8, minute: 0),
|
||||
endTime: const TimeOfDay(hour: 9, minute: 0),
|
||||
);
|
||||
}
|
||||
|
||||
void setWeekDay(int value) {
|
||||
if (value == state.weekDay) return;
|
||||
state = state.copyWith(weekDay: value);
|
||||
}
|
||||
|
||||
void setStartTime(TimeOfDay value) {
|
||||
state = state.copyWith(startTime: value);
|
||||
}
|
||||
|
||||
void setEndTime(TimeOfDay value) {
|
||||
state = state.copyWith(endTime: value);
|
||||
}
|
||||
|
||||
void toggleStartPicker() {
|
||||
state = state.copyWith(
|
||||
showStartPicker: !state.showStartPicker,
|
||||
showEndPicker: false,
|
||||
);
|
||||
}
|
||||
|
||||
void toggleEndPicker() {
|
||||
state = state.copyWith(
|
||||
showEndPicker: !state.showEndPicker,
|
||||
showStartPicker: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'activity_form_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ActivityForm)
|
||||
const activityFormProvider = ActivityFormFamily._();
|
||||
|
||||
final class ActivityFormProvider
|
||||
extends $NotifierProvider<ActivityForm, ActivityFormState> {
|
||||
const ActivityFormProvider._({
|
||||
required ActivityFormFamily super.from,
|
||||
required (ScheduledActivityEntity?, int) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'activityFormProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$activityFormHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'activityFormProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ActivityForm create() => ActivityForm();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ActivityFormState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ActivityFormState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ActivityFormProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$activityFormHash() => r'4b379b0412538ac5ac849c2e4fa4efa334dbdb3c';
|
||||
|
||||
final class ActivityFormFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
ActivityForm,
|
||||
ActivityFormState,
|
||||
ActivityFormState,
|
||||
ActivityFormState,
|
||||
(ScheduledActivityEntity?, int)
|
||||
> {
|
||||
const ActivityFormFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'activityFormProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
ActivityFormProvider call(
|
||||
ScheduledActivityEntity? initial,
|
||||
int fallbackWeekDay,
|
||||
) => ActivityFormProvider._(argument: (initial, fallbackWeekDay), from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'activityFormProvider';
|
||||
}
|
||||
|
||||
abstract class _$ActivityForm extends $Notifier<ActivityFormState> {
|
||||
late final _$args = ref.$arg as (ScheduledActivityEntity?, int);
|
||||
ScheduledActivityEntity? get initial => _$args.$1;
|
||||
int get fallbackWeekDay => _$args.$2;
|
||||
|
||||
ActivityFormState build(
|
||||
ScheduledActivityEntity? initial,
|
||||
int fallbackWeekDay,
|
||||
);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args.$1, _$args.$2);
|
||||
final ref = this.ref as $Ref<ActivityFormState, ActivityFormState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ActivityFormState, ActivityFormState>,
|
||||
ActivityFormState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:device_management/src/core/data/models/create_scheduled_activity_request_dto.dart';
|
||||
import 'package:device_management/src/core/data/models/update_scheduled_activity_request_dto.dart';
|
||||
import 'package:device_management/src/core/providers/scheduled_activities_repository_provider.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'scheduled_activities_controller.g.dart';
|
||||
|
||||
class ScheduledActivityOverlapException implements Exception {
|
||||
const ScheduledActivityOverlapException(this.overlap);
|
||||
final ScheduledActivityEntity overlap;
|
||||
}
|
||||
|
||||
enum ScheduledActivityAction { create, update, delete }
|
||||
|
||||
@riverpod
|
||||
class ScheduledActivitiesController extends _$ScheduledActivitiesController {
|
||||
static const _uuid = Uuid();
|
||||
|
||||
ScheduledActivityAction? _lastAction;
|
||||
|
||||
ScheduledActivityAction? get lastAction => _lastAction;
|
||||
|
||||
@override
|
||||
FutureOr<void> build() {}
|
||||
|
||||
ScheduledActivityEntity? _findOverlap({
|
||||
required List<ScheduledActivityEntity> current,
|
||||
required int weekDay,
|
||||
required String period,
|
||||
String? excludeId,
|
||||
}) {
|
||||
final candidate = ScheduledActivityEntity(
|
||||
id: '',
|
||||
deviceId: '',
|
||||
weekDay: weekDay,
|
||||
name: '',
|
||||
period: period,
|
||||
createdAt: 0,
|
||||
);
|
||||
for (final activity in current) {
|
||||
if (activity.id == excludeId) continue;
|
||||
if (activity.overlapsWith(candidate)) return activity;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> createActivity({
|
||||
required String deviceId,
|
||||
required String name,
|
||||
required int weekDay,
|
||||
required String period,
|
||||
required List<ScheduledActivityEntity> current,
|
||||
}) async {
|
||||
_lastAction = ScheduledActivityAction.create;
|
||||
state = const AsyncLoading();
|
||||
final overlap =
|
||||
_findOverlap(current: current, weekDay: weekDay, period: period);
|
||||
if (overlap != null) {
|
||||
state = AsyncError(
|
||||
ScheduledActivityOverlapException(overlap),
|
||||
StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(scheduledActivitiesRepositoryProvider).createActivity(
|
||||
request: CreateScheduledActivityRequestDto(
|
||||
id: _uuid.v4(),
|
||||
deviceId: deviceId,
|
||||
weekDay: weekDay,
|
||||
name: name,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
ref.invalidate(scheduledActivitiesProvider(deviceId));
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyDeviceScheduledActivityAdded(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateActivity({
|
||||
required String deviceId,
|
||||
required String activityId,
|
||||
required int weekDay,
|
||||
required String name,
|
||||
required String period,
|
||||
required List<ScheduledActivityEntity> current,
|
||||
}) async {
|
||||
_lastAction = ScheduledActivityAction.update;
|
||||
state = const AsyncLoading();
|
||||
final overlap = _findOverlap(
|
||||
current: current,
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
excludeId: activityId,
|
||||
);
|
||||
if (overlap != null) {
|
||||
state = AsyncError(
|
||||
ScheduledActivityOverlapException(overlap),
|
||||
StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(scheduledActivitiesRepositoryProvider).updateActivity(
|
||||
activityId: activityId,
|
||||
request: UpdateScheduledActivityRequestDto(
|
||||
deviceId: deviceId,
|
||||
name: name,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
ref.invalidate(scheduledActivitiesProvider(deviceId));
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyDeviceScheduledActivityUpdated(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteActivity({
|
||||
required String deviceId,
|
||||
required String activityId,
|
||||
}) async {
|
||||
_lastAction = ScheduledActivityAction.delete;
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref
|
||||
.read(scheduledActivitiesRepositoryProvider)
|
||||
.deleteActivity(activityId: activityId);
|
||||
ref.invalidate(scheduledActivitiesProvider(deviceId));
|
||||
unawaited(
|
||||
ref.read(sfTrackingProvider).legacyDeviceScheduledActivityRemoved(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scheduled_activities_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ScheduledActivitiesController)
|
||||
const scheduledActivitiesControllerProvider =
|
||||
ScheduledActivitiesControllerProvider._();
|
||||
|
||||
final class ScheduledActivitiesControllerProvider
|
||||
extends $AsyncNotifierProvider<ScheduledActivitiesController, void> {
|
||||
const ScheduledActivitiesControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'scheduledActivitiesControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$scheduledActivitiesControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ScheduledActivitiesController create() => ScheduledActivitiesController();
|
||||
}
|
||||
|
||||
String _$scheduledActivitiesControllerHash() =>
|
||||
r'0add05c19aae539637b858f13168a886879042c9';
|
||||
|
||||
abstract class _$ScheduledActivitiesController extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
build();
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:device_management/src/core/providers/scheduled_activities_repository_provider.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'scheduled_activities_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<ScheduledActivityEntity>> scheduledActivities(
|
||||
Ref ref,
|
||||
String deviceId,
|
||||
) async {
|
||||
final activities = await ref
|
||||
.read(scheduledActivitiesRepositoryProvider)
|
||||
.getActivities(deviceId: deviceId);
|
||||
return [...activities]..sort((a, b) {
|
||||
final dayCompare = a.weekDay.compareTo(b.weekDay);
|
||||
if (dayCompare != 0) return dayCompare;
|
||||
return a.startMinutes.compareTo(b.startMinutes);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scheduled_activities_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(scheduledActivities)
|
||||
const scheduledActivitiesProvider = ScheduledActivitiesFamily._();
|
||||
|
||||
final class ScheduledActivitiesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<ScheduledActivityEntity>>,
|
||||
List<ScheduledActivityEntity>,
|
||||
FutureOr<List<ScheduledActivityEntity>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<ScheduledActivityEntity>>,
|
||||
$FutureProvider<List<ScheduledActivityEntity>> {
|
||||
const ScheduledActivitiesProvider._({
|
||||
required ScheduledActivitiesFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'scheduledActivitiesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$scheduledActivitiesHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'scheduledActivitiesProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<ScheduledActivityEntity>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<ScheduledActivityEntity>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return scheduledActivities(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ScheduledActivitiesProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$scheduledActivitiesHash() =>
|
||||
r'bf2e33cd408ea36967046d0c46323fc49ef628f1';
|
||||
|
||||
final class ScheduledActivitiesFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<List<ScheduledActivityEntity>>,
|
||||
String
|
||||
> {
|
||||
const ScheduledActivitiesFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'scheduledActivitiesProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
ScheduledActivitiesProvider call(String deviceId) =>
|
||||
ScheduledActivitiesProvider._(argument: deviceId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'scheduledActivitiesProvider';
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_controller.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_provider.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/widgets/day_timeline.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import 'state/scheduled_activities_view_model.dart';
|
||||
import 'widgets/activity_form_sheet.dart';
|
||||
import 'widgets/day_timeline.dart';
|
||||
|
||||
class ScheduledActivitiesScreen extends ConsumerStatefulWidget {
|
||||
final NavigationContract navigationContract;
|
||||
|
||||
@@ -46,60 +46,90 @@ class _ScheduledActivitiesScreenState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(scheduledActivitiesViewModelProvider);
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
|
||||
ref.listen(
|
||||
scheduledActivitiesViewModelProvider.select((s) => s.errorMessage),
|
||||
(previous, next) {
|
||||
if (next.isNotEmpty) {
|
||||
showTopSnackbar(context, message: next, type: MessageType.error);
|
||||
ref.listen(scheduledActivitiesControllerProvider, (prev, next) async {
|
||||
if (prev == null || !prev.isLoading || next.isLoading) return;
|
||||
if (next.hasError) {
|
||||
final error = next.error;
|
||||
if (error is ScheduledActivityOverlapException) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
I18n.scheduledActivityOverlap,
|
||||
args: {
|
||||
'name': error.overlap.name,
|
||||
'time': '${error.overlap.startTime}-${error.overlap.endTime}',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await next.showErrorOn(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
final action = ref
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.lastAction;
|
||||
final key = switch (action) {
|
||||
ScheduledActivityAction.create => I18n.scheduledActivityCreated,
|
||||
ScheduledActivityAction.update => I18n.scheduledActivityUpdated,
|
||||
ScheduledActivityAction.delete => I18n.scheduledActivityDeleted,
|
||||
null => I18n.deviceUpdatedSuccess,
|
||||
};
|
||||
await showSuccessDialog(context, key);
|
||||
});
|
||||
|
||||
if (device == null) {
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.activityScheduleTitle),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final activitiesAsync =
|
||||
ref.watch(scheduledActivitiesProvider(device.id));
|
||||
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.activityScheduleTitle),
|
||||
body: state.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: context.sfColors.legacyPrimary,
|
||||
unselectedLabelColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
indicatorColor: context.sfColors.legacyPrimary,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
tabs: weekDayI18nKeys.entries
|
||||
.map(
|
||||
(e) => Tab(text: weekDayShortLabel(context, e.value)),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: List.generate(7, (index) {
|
||||
final weekDay = index + 1;
|
||||
final dayActivities = state.activities
|
||||
.where((a) => a.weekDay == weekDay)
|
||||
.toList();
|
||||
return DayTimeline(activities: dayActivities);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: activitiesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, _) => Center(child: Text(err.toString())),
|
||||
data: (activities) => Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: primaryColor,
|
||||
unselectedLabelColor: Theme.of(context).colorScheme.onSurface,
|
||||
indicatorColor: primaryColor,
|
||||
indicatorWeight: 3,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
tabs: weekDayI18nKeys.entries
|
||||
.map((e) => Tab(text: weekDayShortLabel(context, e.value)))
|
||||
.toList(),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: List.generate(7, (index) {
|
||||
final weekDay = index + 1;
|
||||
final dayActivities =
|
||||
activities.where((a) => a.weekDay == weekDay).toList();
|
||||
return DayTimeline(activities: dayActivities);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
footer: Material(
|
||||
color: context.sfColors.legacyPrimary,
|
||||
color: primaryColor,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../../core/data/models/create_scheduled_activity_request_dto.dart';
|
||||
import '../../../../core/data/models/update_scheduled_activity_request_dto.dart';
|
||||
import '../../../../core/domain/repositories/scheduled_activities_repository.dart';
|
||||
import '../../../../core/providers/scheduled_activities_repository_provider.dart';
|
||||
import '../../domain/entities/scheduled_activity_entity.dart';
|
||||
import 'scheduled_activities_view_state.dart';
|
||||
|
||||
final scheduledActivitiesViewModelProvider =
|
||||
NotifierProvider.autoDispose<
|
||||
ScheduledActivitiesViewModel,
|
||||
ScheduledActivitiesViewState
|
||||
>(ScheduledActivitiesViewModel.new);
|
||||
|
||||
class ScheduledActivitiesViewModel
|
||||
extends Notifier<ScheduledActivitiesViewState> {
|
||||
late final ScheduledActivitiesRepository _repository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
static const _uuid = Uuid();
|
||||
|
||||
@override
|
||||
ScheduledActivitiesViewState build() {
|
||||
_repository = ref.read(scheduledActivitiesRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
_init();
|
||||
return const ScheduledActivitiesViewState();
|
||||
}
|
||||
|
||||
String? get _deviceId => ref.read(selectedDeviceProvider).value?.id;
|
||||
|
||||
Future<void> _init() async {
|
||||
if (_deviceId == null) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final activities = await _repository.getActivities(deviceId: _deviceId!);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
activities: _sortActivities(activities),
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledActivityEntity? _findOverlap({
|
||||
required int weekDay,
|
||||
required String period,
|
||||
String? excludeId,
|
||||
}) {
|
||||
final candidate = ScheduledActivityEntity(
|
||||
id: '',
|
||||
deviceId: '',
|
||||
weekDay: weekDay,
|
||||
name: '',
|
||||
period: period,
|
||||
createdAt: 0,
|
||||
);
|
||||
|
||||
for (final activity in state.activities) {
|
||||
if (activity.id == excludeId) continue;
|
||||
if (activity.overlapsWith(candidate)) return activity;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> createActivity({
|
||||
required String name,
|
||||
required int weekDay,
|
||||
required String period,
|
||||
}) async {
|
||||
if (_deviceId == null) return;
|
||||
|
||||
final overlap = _findOverlap(weekDay: weekDay, period: period);
|
||||
if (overlap != null) {
|
||||
state = state.copyWith(errorMessage: _overlapMessage(overlap));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, errorMessage: '');
|
||||
|
||||
final request = CreateScheduledActivityRequestDto(
|
||||
id: _uuid.v4(),
|
||||
deviceId: _deviceId!,
|
||||
weekDay: weekDay,
|
||||
name: name,
|
||||
period: period,
|
||||
);
|
||||
|
||||
await _repository.createActivity(request: request);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyDeviceScheduledActivityAdded(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateActivity({
|
||||
required String activityId,
|
||||
required int weekDay,
|
||||
required String name,
|
||||
required String period,
|
||||
}) async {
|
||||
if (_deviceId == null) return;
|
||||
|
||||
final overlap = _findOverlap(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
excludeId: activityId,
|
||||
);
|
||||
if (overlap != null) {
|
||||
state = state.copyWith(errorMessage: _overlapMessage(overlap));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, errorMessage: '');
|
||||
|
||||
final request = UpdateScheduledActivityRequestDto(
|
||||
deviceId: _deviceId!,
|
||||
name: name,
|
||||
period: period,
|
||||
);
|
||||
|
||||
await _repository.updateActivity(
|
||||
activityId: activityId,
|
||||
request: request,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyDeviceScheduledActivityUpdated(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteActivity(String activityId) async {
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, errorMessage: '');
|
||||
|
||||
await _repository.deleteActivity(activityId: activityId);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(_tracking.legacyDeviceScheduledActivityRemoved());
|
||||
|
||||
await _reload();
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
if (_deviceId == null) return;
|
||||
|
||||
try {
|
||||
final activities = await _repository.getActivities(deviceId: _deviceId!);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
activities: _sortActivities(activities),
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
List<ScheduledActivityEntity> _sortActivities(
|
||||
List<ScheduledActivityEntity> activities,
|
||||
) {
|
||||
return List.of(activities)..sort((a, b) {
|
||||
final dayCompare = a.weekDay.compareTo(b.weekDay);
|
||||
if (dayCompare != 0) return dayCompare;
|
||||
return a.startMinutes.compareTo(b.startMinutes);
|
||||
});
|
||||
}
|
||||
|
||||
String _overlapMessage(ScheduledActivityEntity overlap) {
|
||||
return I18n.scheduledActivityOverlap.tr(
|
||||
args: {
|
||||
'name': overlap.name,
|
||||
'time': '${overlap.startTime}-${overlap.endTime}',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatError(Object e) {
|
||||
final msg = e.toString();
|
||||
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'scheduled_activities_view_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ScheduledActivitiesViewState
|
||||
with _$ScheduledActivitiesViewState {
|
||||
const factory ScheduledActivitiesViewState({
|
||||
@Default([]) List<ScheduledActivityEntity> activities,
|
||||
@Default(true) bool isLoading,
|
||||
@Default('') String errorMessage,
|
||||
}) = _ScheduledActivitiesViewState;
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'scheduled_activities_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ScheduledActivitiesViewState {
|
||||
|
||||
List<ScheduledActivityEntity> get activities; bool get isLoading; String get errorMessage;
|
||||
/// Create a copy of ScheduledActivitiesViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ScheduledActivitiesViewStateCopyWith<ScheduledActivitiesViewState> get copyWith => _$ScheduledActivitiesViewStateCopyWithImpl<ScheduledActivitiesViewState>(this as ScheduledActivitiesViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ScheduledActivitiesViewState&&const DeepCollectionEquality().equals(other.activities, activities)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(activities),isLoading,errorMessage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ScheduledActivitiesViewState(activities: $activities, isLoading: $isLoading, errorMessage: $errorMessage)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ScheduledActivitiesViewStateCopyWith<$Res> {
|
||||
factory $ScheduledActivitiesViewStateCopyWith(ScheduledActivitiesViewState value, $Res Function(ScheduledActivitiesViewState) _then) = _$ScheduledActivitiesViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<ScheduledActivityEntity> activities, bool isLoading, String errorMessage
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ScheduledActivitiesViewStateCopyWithImpl<$Res>
|
||||
implements $ScheduledActivitiesViewStateCopyWith<$Res> {
|
||||
_$ScheduledActivitiesViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ScheduledActivitiesViewState _self;
|
||||
final $Res Function(ScheduledActivitiesViewState) _then;
|
||||
|
||||
/// Create a copy of ScheduledActivitiesViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? activities = null,Object? isLoading = null,Object? errorMessage = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
activities: null == activities ? _self.activities : activities // ignore: cast_nullable_to_non_nullable
|
||||
as List<ScheduledActivityEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ScheduledActivitiesViewState].
|
||||
extension ScheduledActivitiesViewStatePatterns on ScheduledActivitiesViewState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ScheduledActivitiesViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ScheduledActivitiesViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ScheduledActivitiesViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ScheduledActivitiesViewState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ScheduledActivitiesViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ScheduledActivitiesViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ScheduledActivityEntity> activities, bool isLoading, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ScheduledActivitiesViewState() when $default != null:
|
||||
return $default(_that.activities,_that.isLoading,_that.errorMessage);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ScheduledActivityEntity> activities, bool isLoading, String errorMessage) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ScheduledActivitiesViewState():
|
||||
return $default(_that.activities,_that.isLoading,_that.errorMessage);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ScheduledActivityEntity> activities, bool isLoading, String errorMessage)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ScheduledActivitiesViewState() when $default != null:
|
||||
return $default(_that.activities,_that.isLoading,_that.errorMessage);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ScheduledActivitiesViewState implements ScheduledActivitiesViewState {
|
||||
const _ScheduledActivitiesViewState({final List<ScheduledActivityEntity> activities = const [], this.isLoading = true, this.errorMessage = ''}): _activities = activities;
|
||||
|
||||
|
||||
final List<ScheduledActivityEntity> _activities;
|
||||
@override@JsonKey() List<ScheduledActivityEntity> get activities {
|
||||
if (_activities is EqualUnmodifiableListView) return _activities;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_activities);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final String errorMessage;
|
||||
|
||||
/// Create a copy of ScheduledActivitiesViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ScheduledActivitiesViewStateCopyWith<_ScheduledActivitiesViewState> get copyWith => __$ScheduledActivitiesViewStateCopyWithImpl<_ScheduledActivitiesViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ScheduledActivitiesViewState&&const DeepCollectionEquality().equals(other._activities, _activities)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_activities),isLoading,errorMessage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ScheduledActivitiesViewState(activities: $activities, isLoading: $isLoading, errorMessage: $errorMessage)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ScheduledActivitiesViewStateCopyWith<$Res> implements $ScheduledActivitiesViewStateCopyWith<$Res> {
|
||||
factory _$ScheduledActivitiesViewStateCopyWith(_ScheduledActivitiesViewState value, $Res Function(_ScheduledActivitiesViewState) _then) = __$ScheduledActivitiesViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<ScheduledActivityEntity> activities, bool isLoading, String errorMessage
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ScheduledActivitiesViewStateCopyWithImpl<$Res>
|
||||
implements _$ScheduledActivitiesViewStateCopyWith<$Res> {
|
||||
__$ScheduledActivitiesViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ScheduledActivitiesViewState _self;
|
||||
final $Res Function(_ScheduledActivitiesViewState) _then;
|
||||
|
||||
/// Create a copy of ScheduledActivitiesViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? activities = null,Object? isLoading = null,Object? errorMessage = null,}) {
|
||||
return _then(_ScheduledActivitiesViewState(
|
||||
activities: null == activities ? _self._activities : activities // ignore: cast_nullable_to_non_nullable
|
||||
as List<ScheduledActivityEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,21 +1,23 @@
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/activity_form_provider.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_controller.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_provider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../domain/entities/scheduled_activity_entity.dart';
|
||||
import '../state/scheduled_activities_view_model.dart';
|
||||
|
||||
void showActivityFormSheet(
|
||||
BuildContext context, {
|
||||
ScheduledActivityEntity? activity,
|
||||
int? weekDay,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -35,94 +37,82 @@ class ActivityFormSheet extends ConsumerStatefulWidget {
|
||||
|
||||
class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
late final TextEditingController _nameController;
|
||||
late int _selectedWeekDay;
|
||||
late TimeOfDay _startTime;
|
||||
late TimeOfDay _endTime;
|
||||
bool _showStartPicker = false;
|
||||
bool _showEndPicker = false;
|
||||
|
||||
bool get _isEditing => widget.activity != null;
|
||||
|
||||
bool get _isFormValid {
|
||||
if (_nameController.text.trim().isEmpty) return false;
|
||||
return _timeToMinutes(_startTime) < _timeToMinutes(_endTime);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final activity = widget.activity;
|
||||
_nameController = TextEditingController(text: activity?.name ?? '');
|
||||
_nameController.addListener(() => setState(() {}));
|
||||
_selectedWeekDay = activity?.weekDay ?? widget.weekDay ?? 1;
|
||||
|
||||
if (activity != null && activity.hasValidPeriod) {
|
||||
_startTime = TimeOfDay(
|
||||
hour: int.parse(activity.period.substring(0, 2)),
|
||||
minute: int.parse(activity.period.substring(2, 4)),
|
||||
);
|
||||
_endTime = TimeOfDay(
|
||||
hour: int.parse(activity.period.substring(4, 6)),
|
||||
minute: int.parse(activity.period.substring(6, 8)),
|
||||
);
|
||||
} else {
|
||||
_startTime = const TimeOfDay(hour: 8, minute: 0);
|
||||
_endTime = const TimeOfDay(hour: 9, minute: 0);
|
||||
}
|
||||
_nameController = TextEditingController(text: widget.activity?.name ?? '');
|
||||
_nameController.addListener(_onNameChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.removeListener(_onNameChanged);
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _buildPeriod() {
|
||||
return '${_startTime.hour.toString().padLeft(2, '0')}'
|
||||
'${_startTime.minute.toString().padLeft(2, '0')}'
|
||||
'${_endTime.hour.toString().padLeft(2, '0')}'
|
||||
'${_endTime.minute.toString().padLeft(2, '0')}';
|
||||
void _onNameChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
int _timeToMinutes(TimeOfDay time) => time.hour * 60 + time.minute;
|
||||
|
||||
void _togglePicker({required bool isStart}) {
|
||||
setState(() {
|
||||
if (isStart) {
|
||||
_showStartPicker = !_showStartPicker;
|
||||
_showEndPicker = false;
|
||||
} else {
|
||||
_showEndPicker = !_showEndPicker;
|
||||
_showStartPicker = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_isFormValid) return;
|
||||
Future<void> _submit({
|
||||
required ActivityFormState formState,
|
||||
required List<ScheduledActivityEntity> currentActivities,
|
||||
}) async {
|
||||
if (!formState.isTimeValid) return;
|
||||
if (_nameController.text.trim().isEmpty) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
if (device == null) return;
|
||||
|
||||
final name = _nameController.text.trim();
|
||||
final period = _buildPeriod();
|
||||
final vm = ref.read(scheduledActivitiesViewModelProvider.notifier);
|
||||
final controller =
|
||||
ref.read(scheduledActivitiesControllerProvider.notifier);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (_isEditing) {
|
||||
vm.updateActivity(
|
||||
controller.updateActivity(
|
||||
deviceId: device.id,
|
||||
activityId: widget.activity!.id,
|
||||
weekDay: widget.activity!.weekDay,
|
||||
name: name,
|
||||
period: period,
|
||||
period: formState.period,
|
||||
current: currentActivities,
|
||||
);
|
||||
} else {
|
||||
vm.createActivity(name: name, weekDay: _selectedWeekDay, period: period);
|
||||
controller.createActivity(
|
||||
deviceId: device.id,
|
||||
name: name,
|
||||
weekDay: formState.weekDay,
|
||||
period: formState.period,
|
||||
current: currentActivities,
|
||||
);
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final fallbackWeekDay = widget.weekDay ?? 1;
|
||||
final formProvider = activityFormProvider(widget.activity, fallbackWeekDay);
|
||||
final formState = ref.watch(formProvider);
|
||||
final formNotifier = ref.read(formProvider.notifier);
|
||||
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
final currentActivities = device == null
|
||||
? const <ScheduledActivityEntity>[]
|
||||
: (ref.watch(scheduledActivitiesProvider(device.id)).value ??
|
||||
const []);
|
||||
|
||||
final canSubmit = _nameController.text.trim().isNotEmpty &&
|
||||
formState.isTimeValid;
|
||||
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
return Padding(
|
||||
@@ -136,7 +126,8 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 22, big: 20)),
|
||||
padding:
|
||||
EdgeInsets.all(SizeUtils.getByScreen(small: 22, big: 20)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -163,7 +154,7 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.sfColors.legacyPrimary,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)),
|
||||
@@ -176,12 +167,9 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
WeekDayChips.single(
|
||||
selectedWeekDay: _isEditing
|
||||
? widget.activity!.weekDay
|
||||
: _selectedWeekDay,
|
||||
selectedWeekDay: formState.weekDay,
|
||||
enabled: !_isEditing,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedWeekDay = value),
|
||||
onChanged: formNotifier.setWeekDay,
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
Row(
|
||||
@@ -191,39 +179,45 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
label: context.translate(
|
||||
I18n.scheduledActivityStartTime,
|
||||
),
|
||||
time: _startTime,
|
||||
isExpanded: _showStartPicker,
|
||||
onTap: () => _togglePicker(isStart: true),
|
||||
time: formState.startTime,
|
||||
isExpanded: formState.showStartPicker,
|
||||
onTap: formNotifier.toggleStartPicker,
|
||||
),
|
||||
),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 12, big: 10)),
|
||||
Expanded(
|
||||
child: _TimeSelector(
|
||||
label: context.translate(I18n.scheduledActivityEndTime),
|
||||
time: _endTime,
|
||||
isExpanded: _showEndPicker,
|
||||
onTap: () => _togglePicker(isStart: false),
|
||||
label: context
|
||||
.translate(I18n.scheduledActivityEndTime),
|
||||
time: formState.endTime,
|
||||
isExpanded: formState.showEndPicker,
|
||||
onTap: formNotifier.toggleEndPicker,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showStartPicker)
|
||||
if (formState.showStartPicker)
|
||||
_InlineTimePicker(
|
||||
initialTime: _startTime,
|
||||
onChanged: (time) => setState(() => _startTime = time),
|
||||
initialTime: formState.startTime,
|
||||
onChanged: formNotifier.setStartTime,
|
||||
),
|
||||
if (_showEndPicker)
|
||||
if (formState.showEndPicker)
|
||||
_InlineTimePicker(
|
||||
initialTime: _endTime,
|
||||
onChanged: (time) => setState(() => _endTime = time),
|
||||
initialTime: formState.endTime,
|
||||
onChanged: formNotifier.setEndTime,
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)),
|
||||
SizedBox(
|
||||
height: SizeUtils.getByScreen(small: 48, big: 46),
|
||||
child: ElevatedButton(
|
||||
onPressed: _isFormValid ? _submit : null,
|
||||
onPressed: canSubmit
|
||||
? () => _submit(
|
||||
formState: formState,
|
||||
currentActivities: currentActivities,
|
||||
)
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.sfColors.legacyPrimary,
|
||||
backgroundColor: primaryColor,
|
||||
disabledBackgroundColor: Colors.grey[300],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
@@ -234,7 +228,7 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
child: Text(
|
||||
context.translate(I18n.save),
|
||||
style: TextStyle(
|
||||
color: _isFormValid ? Colors.white : Colors.grey,
|
||||
color: canSubmit ? Colors.white : Colors.grey,
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 15),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -323,7 +317,10 @@ class _InlineTimePicker extends StatelessWidget {
|
||||
final TimeOfDay initialTime;
|
||||
final ValueChanged<TimeOfDay> onChanged;
|
||||
|
||||
const _InlineTimePicker({required this.initialTime, required this.onChanged});
|
||||
const _InlineTimePicker({
|
||||
required this.initialTime,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../domain/entities/scheduled_activity_entity.dart';
|
||||
import '../state/scheduled_activities_view_model.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
|
||||
class ConfirmDeleteActivityDialog extends ConsumerWidget {
|
||||
final ScheduledActivityEntity activity;
|
||||
|
||||
const ConfirmDeleteActivityDialog({super.key, required this.activity});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
return Container(
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: const EdgeInsets.symmetric(horizontal: 32, vertical: 30),
|
||||
big: const EdgeInsets.symmetric(horizontal: 30, vertical: 28),
|
||||
),
|
||||
width: SizeUtils.getByScreen(small: 360, big: 350),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(
|
||||
SizeUtils.getByScreen(small: 16, big: 14),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
I18n.scheduledActivityDeleteMessage.tr(
|
||||
args: {'name': activity.name},
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 19, big: 18),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
text: context.translate(I18n.cancel),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
height: SizeUtils.getByScreen(small: 38, big: 36),
|
||||
radius: SizeUtils.getByScreen(small: 32, big: 34),
|
||||
),
|
||||
),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 4, big: 16)),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
ref
|
||||
.read(scheduledActivitiesViewModelProvider.notifier)
|
||||
.deleteActivity(activity.id);
|
||||
},
|
||||
text: context.translate(I18n.delete),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
height: SizeUtils.getByScreen(small: 38, big: 36),
|
||||
radius: SizeUtils.getByScreen(small: 32, big: 34),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_controller.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../domain/entities/scheduled_activity_entity.dart';
|
||||
import 'activity_form_sheet.dart';
|
||||
import 'confirm_delete_activity_dialog.dart';
|
||||
|
||||
class DayTimeline extends ConsumerWidget {
|
||||
final List<ScheduledActivityEntity> activities;
|
||||
|
||||
@@ -17,7 +17,6 @@ class DayTimeline extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
if (activities.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -70,9 +69,7 @@ class DayTimeline extends ConsumerWidget {
|
||||
),
|
||||
_TimelineDotAndLine(isLast: isLast, color: primaryColor),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 10, big: 8)),
|
||||
Expanded(
|
||||
child: _ActivityTimelineCard(activity: activity),
|
||||
),
|
||||
Expanded(child: _ActivityTimelineCard(activity: activity)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -85,10 +82,7 @@ class _TimeLabels extends StatelessWidget {
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
|
||||
const _TimeLabels({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
const _TimeLabels({required this.startTime, required this.endTime});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -105,7 +99,7 @@ class _TimeLabels extends StatelessWidget {
|
||||
children: [
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 10, big: 8)),
|
||||
Text(startTime, style: style),
|
||||
Spacer(),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: SizeUtils.getByScreen(small: 14, big: 12),
|
||||
@@ -113,7 +107,9 @@ class _TimeLabels extends StatelessWidget {
|
||||
child: Text(
|
||||
endTime,
|
||||
style: style.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
@@ -160,7 +156,8 @@ class _ActivityTimelineCard extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: SizeUtils.getByScreen(small: 10, big: 8)),
|
||||
margin:
|
||||
EdgeInsets.only(bottom: SizeUtils.getByScreen(small: 10, big: 8)),
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: const EdgeInsets.fromLTRB(14, 10, 6, 10),
|
||||
big: const EdgeInsets.fromLTRB(12, 8, 4, 8),
|
||||
@@ -193,20 +190,47 @@ class _ActivityTimelineCard extends ConsumerWidget {
|
||||
color: context.sfColors.legacyPrimary,
|
||||
size: SizeUtils.getByScreen(small: 20, big: 18),
|
||||
),
|
||||
constraints: BoxConstraints(),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 6)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ConfirmDeleteActivityDialog(activity: activity),
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(
|
||||
context.translate(
|
||||
I18n.scheduledActivityDeleteMessage,
|
||||
args: {'name': activity.name},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.translate(I18n.cancel)),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text(context.translate(I18n.delete)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
if (device == null) return;
|
||||
ref
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.deleteActivity(
|
||||
deviceId: device.id,
|
||||
activityId: activity.id,
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.delete_outlined,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:device_management/src/core/data/models/create_scheduled_activity_request_dto.dart';
|
||||
import 'package:device_management/src/core/data/models/update_scheduled_activity_request_dto.dart';
|
||||
import 'package:device_management/src/core/domain/repositories/scheduled_activities_repository.dart';
|
||||
import 'package:device_management/src/core/providers/scheduled_activities_repository_provider.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/domain/entities/scheduled_activity_entity.dart';
|
||||
import 'package:device_management/src/features/scheduled_activities/presentation/providers/scheduled_activities_controller.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
class MockScheduledActivitiesRepository extends Mock
|
||||
implements ScheduledActivitiesRepository {}
|
||||
|
||||
const _a = ScheduledActivityEntity(
|
||||
id: 'a',
|
||||
deviceId: 'd1',
|
||||
weekDay: 1,
|
||||
name: 'English class',
|
||||
period: '08000900',
|
||||
createdAt: 0,
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
const CreateScheduledActivityRequestDto(
|
||||
id: 'x',
|
||||
deviceId: 'd',
|
||||
weekDay: 1,
|
||||
name: 'n',
|
||||
period: 'p',
|
||||
),
|
||||
);
|
||||
registerFallbackValue(
|
||||
const UpdateScheduledActivityRequestDto(
|
||||
deviceId: 'd',
|
||||
name: 'n',
|
||||
period: 'p',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
ProviderContainer buildContainer(ScheduledActivitiesRepository repo) {
|
||||
return makeContainer(
|
||||
overrides: [
|
||||
scheduledActivitiesRepositoryProvider.overrideWithValue(repo),
|
||||
sfTrackingProvider.overrideWithValue(
|
||||
SfTrackingRepository(clients: const []),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
group('ScheduledActivitiesController.createActivity', () {
|
||||
test('blocks on overlap', () async {
|
||||
final repo = MockScheduledActivitiesRepository();
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.createActivity(
|
||||
deviceId: 'd1',
|
||||
name: 'Math',
|
||||
weekDay: 1,
|
||||
period: '08300930',
|
||||
current: const [_a],
|
||||
);
|
||||
|
||||
final state = container.read(scheduledActivitiesControllerProvider);
|
||||
expect(state, isA<AsyncError<void>>());
|
||||
expect(state.error, isA<ScheduledActivityOverlapException>());
|
||||
verifyNever(() => repo.createActivity(request: any(named: 'request')));
|
||||
});
|
||||
|
||||
test('creates when no overlap', () async {
|
||||
final repo = MockScheduledActivitiesRepository();
|
||||
when(() => repo.createActivity(request: any(named: 'request')))
|
||||
.thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.createActivity(
|
||||
deviceId: 'd1',
|
||||
name: 'Math',
|
||||
weekDay: 1,
|
||||
period: '10001100',
|
||||
current: const [_a],
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(scheduledActivitiesControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
verify(() => repo.createActivity(request: any(named: 'request')))
|
||||
.called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('ScheduledActivitiesController.updateActivity', () {
|
||||
test('allows updating same activity (excluded from overlap)', () async {
|
||||
final repo = MockScheduledActivitiesRepository();
|
||||
when(() => repo.updateActivity(
|
||||
activityId: any(named: 'activityId'),
|
||||
request: any(named: 'request'),
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.updateActivity(
|
||||
deviceId: 'd1',
|
||||
activityId: 'a',
|
||||
weekDay: 1,
|
||||
name: 'English rename',
|
||||
period: '08000900',
|
||||
current: const [_a],
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(scheduledActivitiesControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
verify(() => repo.updateActivity(
|
||||
activityId: 'a',
|
||||
request: any(named: 'request'),
|
||||
)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('ScheduledActivitiesController.deleteActivity', () {
|
||||
test('deletes via repository on success', () async {
|
||||
final repo = MockScheduledActivitiesRepository();
|
||||
when(() => repo.deleteActivity(activityId: any(named: 'activityId')))
|
||||
.thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.deleteActivity(deviceId: 'd1', activityId: 'a');
|
||||
|
||||
expect(
|
||||
container.read(scheduledActivitiesControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
verify(() => repo.deleteActivity(activityId: 'a')).called(1);
|
||||
});
|
||||
|
||||
test('exposes AsyncError when repository fails', () async {
|
||||
final repo = MockScheduledActivitiesRepository();
|
||||
when(() => repo.deleteActivity(activityId: any(named: 'activityId')))
|
||||
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(scheduledActivitiesControllerProvider.notifier)
|
||||
.deleteActivity(deviceId: 'd1', activityId: 'a');
|
||||
|
||||
expect(
|
||||
container.read(scheduledActivitiesControllerProvider),
|
||||
isA<AsyncError<void>>(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -529,6 +529,9 @@
|
||||
"scheduledActivityEndTime": "Ende",
|
||||
"scheduledActivityStartBeforeEnd": "Die Startzeit muss vor der Endzeit liegen",
|
||||
"scheduledActivityOverlap": "Überschneidung mit „{name}\" ({time})",
|
||||
"scheduledActivityCreated": "Aktivität erfolgreich erstellt",
|
||||
"scheduledActivityUpdated": "Aktivität erfolgreich aktualisiert",
|
||||
"scheduledActivityDeleted": "Aktivität erfolgreich gelöscht",
|
||||
"scheduledActivityNewTitle": "Neue Aktivität",
|
||||
"scheduledActivityEditTitle": "Aktivität bearbeiten",
|
||||
"scheduledActivityDeleteTitle": "Aktivität löschen",
|
||||
|
||||
@@ -726,6 +726,9 @@
|
||||
"scheduledActivityEndTime": "End",
|
||||
"scheduledActivityStartBeforeEnd": "Start time must be before end time",
|
||||
"scheduledActivityOverlap": "Overlaps with \"{name}\" ({time})",
|
||||
"scheduledActivityCreated": "Activity created successfully",
|
||||
"scheduledActivityUpdated": "Activity updated successfully",
|
||||
"scheduledActivityDeleted": "Activity deleted successfully",
|
||||
"scheduledActivityNewTitle": "New activity",
|
||||
"scheduledActivityEditTitle": "Edit activity",
|
||||
"scheduledActivityDeleteTitle": "Delete activity",
|
||||
|
||||
@@ -727,6 +727,9 @@
|
||||
"scheduledActivityEndTime": "Fin",
|
||||
"scheduledActivityStartBeforeEnd": "La hora de inicio debe ser anterior a la hora de fin",
|
||||
"scheduledActivityOverlap": "Se superpone con \"{name}\" ({time})",
|
||||
"scheduledActivityCreated": "Actividad creada correctamente",
|
||||
"scheduledActivityUpdated": "Actividad actualizada correctamente",
|
||||
"scheduledActivityDeleted": "Actividad eliminada correctamente",
|
||||
"scheduledActivityNewTitle": "Nueva actividad",
|
||||
"scheduledActivityEditTitle": "Editar actividad",
|
||||
"scheduledActivityDeleteTitle": "Eliminar actividad",
|
||||
|
||||
@@ -529,6 +529,9 @@
|
||||
"scheduledActivityEndTime": "Fin",
|
||||
"scheduledActivityStartBeforeEnd": "L'heure de début doit être antérieure à l'heure de fin",
|
||||
"scheduledActivityOverlap": "Chevauche « {name} » ({time})",
|
||||
"scheduledActivityCreated": "Activité créée avec succès",
|
||||
"scheduledActivityUpdated": "Activité mise à jour avec succès",
|
||||
"scheduledActivityDeleted": "Activité supprimée avec succès",
|
||||
"scheduledActivityNewTitle": "Nouvelle activité",
|
||||
"scheduledActivityEditTitle": "Modifier l'activité",
|
||||
"scheduledActivityDeleteTitle": "Supprimer l'activité",
|
||||
|
||||
@@ -529,6 +529,9 @@
|
||||
"scheduledActivityEndTime": "Fine",
|
||||
"scheduledActivityStartBeforeEnd": "L'orario di inizio deve essere precedente all'orario di fine",
|
||||
"scheduledActivityOverlap": "Si sovrappone con \"{name}\" ({time})",
|
||||
"scheduledActivityCreated": "Attività creata con successo",
|
||||
"scheduledActivityUpdated": "Attività aggiornata con successo",
|
||||
"scheduledActivityDeleted": "Attività eliminata con successo",
|
||||
"scheduledActivityNewTitle": "Nuova attività",
|
||||
"scheduledActivityEditTitle": "Modifica attività",
|
||||
"scheduledActivityDeleteTitle": "Elimina attività",
|
||||
|
||||
@@ -529,6 +529,9 @@
|
||||
"scheduledActivityEndTime": "Fim",
|
||||
"scheduledActivityStartBeforeEnd": "A hora de início deve ser anterior à hora de fim",
|
||||
"scheduledActivityOverlap": "Sobrepõe-se com \"{name}\" ({time})",
|
||||
"scheduledActivityCreated": "Atividade criada com sucesso",
|
||||
"scheduledActivityUpdated": "Atividade atualizada com sucesso",
|
||||
"scheduledActivityDeleted": "Atividade eliminada com sucesso",
|
||||
"scheduledActivityNewTitle": "Nova atividade",
|
||||
"scheduledActivityEditTitle": "Editar atividade",
|
||||
"scheduledActivityDeleteTitle": "Eliminar atividade",
|
||||
|
||||
@@ -819,9 +819,12 @@ class I18n {
|
||||
static const String scheduledActivityEditTitle = 'scheduledActivityEditTitle';
|
||||
static const String scheduledActivityEmpty = 'scheduledActivityEmpty';
|
||||
static const String scheduledActivityEmptyHint = 'scheduledActivityEmptyHint';
|
||||
static const String scheduledActivityCreated = 'scheduledActivityCreated';
|
||||
static const String scheduledActivityDeleted = 'scheduledActivityDeleted';
|
||||
static const String scheduledActivityEndTime = 'scheduledActivityEndTime';
|
||||
static const String scheduledActivityNewTitle = 'scheduledActivityNewTitle';
|
||||
static const String scheduledActivityOverlap = 'scheduledActivityOverlap';
|
||||
static const String scheduledActivityUpdated = 'scheduledActivityUpdated';
|
||||
static const String scheduledActivityStartBeforeEnd = 'scheduledActivityStartBeforeEnd';
|
||||
static const String scheduledActivityStartTime = 'scheduledActivityStartTime';
|
||||
static const String secretCodeConfigure = 'secretCodeConfigure';
|
||||
|
||||
Reference in New Issue
Block a user