refactor(device_management): migrate do_not_disturb to Riverpod + add delete button

This commit is contained in:
2026-04-22 21:44:16 +02:00
parent cbaee6d597
commit a2ef28a1b5
21 changed files with 622 additions and 589 deletions

View File

@@ -1,184 +1,200 @@
import 'package:design_system/design_system.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:device_management/src/features/do_not_disturb/domain/do_not_disturb_period.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_controller.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_editor_provider.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_provider.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/widgets/do_not_disturb_period_card.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/widgets/edit_period_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: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/do_not_disturb_period.dart';
import 'state/do_not_disturb_view_model.dart';
import 'state/do_not_disturb_view_state.dart';
import 'widgets/do_not_disturb_period_card.dart';
import 'widgets/edit_period_sheet.dart';
class DoNotDisturbScreen extends ConsumerWidget {
const DoNotDisturbScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(doNotDisturbViewModelProvider.notifier);
final (periods, maxPeriods, isLoading, isSaving) = ref.watch(
doNotDisturbViewModelProvider.select(
(s) => (s.periods, s.maxPeriods, s.isLoading, s.isSaving),
),
);
ref.listen(doNotDisturbViewModelProvider.select((s) => s.errorEvent), (
_,
next,
) {
if (next == null) return;
final message = switch (next) {
DoNotDisturbErrorEvent.load || DoNotDisturbErrorEvent.save => context.translate(
I18n.doNotDisturbError,
),
DoNotDisturbErrorEvent.maxPeriods => context
.translate(I18n.doNotDisturbMaxPeriods)
.replaceAll('{max}', maxPeriods.toString()),
};
showTopSnackbar(context, message: message, type: MessageType.error);
vm.clearError();
});
ref.listen(doNotDisturbViewModelProvider.select((s) => s.saveSuccess), (
_,
success,
) {
if (success) {
showTopSnackbar(
context,
message: context.translate(I18n.doNotDisturbSaved),
type: MessageType.success,
);
vm.clearSaveSuccess();
}
});
final primaryColor = context.sfColors.legacyPrimary;
final device = ref.watch(selectedDeviceProvider).value;
ref.listen(doNotDisturbControllerProvider, (prev, next) async {
if (prev == null || !prev.isLoading || next.isLoading) return;
if (next.hasError) {
await showErrorDialog(context, I18n.doNotDisturbError);
return;
}
ref.read(doNotDisturbEditorProvider.notifier).clear();
await showSuccessDialog(context, I18n.doNotDisturbSaved);
});
if (device == null) {
return LegacyPageLayout(
title: context.translate(I18n.doNotDisturb),
body: const Center(child: CircularProgressIndicator()),
);
}
final identificator = device.identificator;
final maxPeriods = device.capabilities?.doNotDisturbs?.maxPeriods ?? 4;
final periodsAsync =
ref.watch(doNotDisturbPeriodsProvider(identificator));
final editorState = ref.watch(doNotDisturbEditorProvider);
final isSaving =
ref.watch(doNotDisturbControllerProvider.select((s) => s.isLoading));
return LegacyPageLayout(
title: context.translate(I18n.doNotDisturb),
body: isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 12, big: 10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
context.translate(I18n.doNotDisturbDescription),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.5),
height: 1.4,
),
),
SizedBox(
height: SizeUtils.getByScreen(small: 16, big: 14),
),
...periods.asMap().entries.map((entry) {
return Padding(
padding: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 8, big: 6),
),
child: DoNotDisturbPeriodCard(
period: entry.value,
onEdit: () => _editPeriod(
context,
vm,
entry.key,
entry.value,
),
),
);
}),
if (periods.isEmpty)
Padding(
padding: EdgeInsets.symmetric(
vertical: SizeUtils.getByScreen(small: 24, big: 20),
),
child: Center(
child: Text(
context.translate(I18n.doNotDisturbEmpty),
style: TextStyle(
fontSize: SizeUtils.getByScreen(
small: 14,
big: 13,
),
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.4),
),
),
),
),
if (periods.length < maxPeriods)
Padding(
padding: EdgeInsets.only(
top: SizeUtils.getByScreen(small: 8, big: 6),
),
child: OutlinedButton.icon(
onPressed: () => _addPeriod(context, vm),
icon: const Icon(Icons.add),
label: Text(
context.translate(I18n.doNotDisturbAddPeriod),
),
style: OutlinedButton.styleFrom(
foregroundColor: primaryColor,
side: BorderSide(color: primaryColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
SizeUtils.getByScreen(small: 12, big: 10),
),
),
padding: EdgeInsets.symmetric(
vertical: SizeUtils.getByScreen(
small: 12,
big: 10,
),
),
),
),
),
],
),
body: periodsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => Center(
child: Text(context.translate(I18n.doNotDisturbError)),
),
data: (serverPeriods) {
final periods = editorState ?? serverPeriods;
return SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 12, big: 10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
context.translate(I18n.doNotDisturbDescription),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.5),
height: 1.4,
),
),
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
...periods.asMap().entries.map(
(entry) => Padding(
padding: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 8, big: 6),
),
child: DoNotDisturbPeriodCard(
period: entry.value,
onEdit: () => _editPeriod(
context,
ref,
periods,
entry.key,
entry.value,
),
onDelete: () => _deletePeriod(
context,
ref,
periods,
entry.key,
),
),
),
),
if (periods.isEmpty)
Padding(
padding: EdgeInsets.symmetric(
vertical: SizeUtils.getByScreen(small: 24, big: 20),
),
child: Center(
child: Text(
context.translate(I18n.doNotDisturbEmpty),
style: TextStyle(
fontSize:
SizeUtils.getByScreen(small: 14, big: 13),
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.4),
),
),
),
),
if (periods.length < maxPeriods)
Padding(
padding: EdgeInsets.only(
top: SizeUtils.getByScreen(small: 8, big: 6),
),
child: OutlinedButton.icon(
onPressed: () => _addPeriod(context, ref, periods),
icon: const Icon(Icons.add),
label: Text(
context.translate(I18n.doNotDisturbAddPeriod),
),
style: OutlinedButton.styleFrom(
foregroundColor: primaryColor,
side: BorderSide(color: primaryColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
SizeUtils.getByScreen(small: 12, big: 10),
),
),
padding: EdgeInsets.symmetric(
vertical:
SizeUtils.getByScreen(small: 12, big: 10),
),
),
),
),
],
),
);
},
),
footer: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: isSaving
? const Center(child: CircularProgressIndicator())
: PrimaryButton(
onPressed: () async {
child: PrimaryButton(
onPressed: isSaving
? null
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.save();
if (!context.mounted) return;
final serverPeriods =
ref.read(doNotDisturbPeriodsProvider(identificator));
final periods = ref.read(doNotDisturbEditorProvider) ??
serverPeriods.value ??
const [];
ref.read(doNotDisturbControllerProvider.notifier).save(
deviceIdentificator: identificator,
periods: periods,
);
},
text: context.translate(I18n.doNotDisturbSave),
color: primaryColor,
),
text: context.translate(I18n.doNotDisturbSave),
color: primaryColor,
),
),
);
}
Future<void> _addPeriod(
BuildContext context,
DoNotDisturbViewModel vm,
WidgetRef ref,
List<DoNotDisturbPeriod> current,
) async {
final period = await showModalBottomSheet<DoNotDisturbPeriod>(
context: context,
isScrollControlled: true,
builder: (_) => const EditPeriodSheet(),
);
if (period != null) vm.addPeriod(period);
if (period != null) {
ref
.read(doNotDisturbEditorProvider.notifier)
.add(current: current, period: period);
}
}
Future<void> _editPeriod(
BuildContext context,
DoNotDisturbViewModel vm,
WidgetRef ref,
List<DoNotDisturbPeriod> current,
int index,
DoNotDisturbPeriod existing,
) async {
@@ -187,6 +203,43 @@ class DoNotDisturbScreen extends ConsumerWidget {
isScrollControlled: true,
builder: (_) => EditPeriodSheet(initial: existing),
);
if (period != null) vm.updatePeriod(index, period);
if (period != null) {
ref
.read(doNotDisturbEditorProvider.notifier)
.replace(current: current, index: index, period: period);
}
}
Future<void> _deletePeriod(
BuildContext context,
WidgetRef ref,
List<DoNotDisturbPeriod> current,
int index,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.translate(I18n.doNotDisturbDeletePeriod)),
content:
Text(context.translate(I18n.doNotDisturbDeletePeriodConfirm)),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(context.translate(I18n.cancel)),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: Text(context.translate(I18n.delete)),
),
],
),
);
if (confirmed != true) return;
ref
.read(doNotDisturbEditorProvider.notifier)
.remove(current: current, index: index);
}
}

View File

@@ -0,0 +1,36 @@
import 'dart:async';
import 'package:device_management/src/core/providers/do_not_disturb_providers.dart';
import 'package:device_management/src/features/do_not_disturb/domain/do_not_disturb_period.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_tracking/sf_tracking.dart';
part 'do_not_disturb_controller.g.dart';
@riverpod
class DoNotDisturbController extends _$DoNotDisturbController {
@override
FutureOr<void> build() {}
Future<void> save({
required String deviceIdentificator,
required List<DoNotDisturbPeriod> periods,
}) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final schedule = await ref
.read(doNotDisturbRemoteDatasourceProvider)
.updatePeriods(
identificator: deviceIdentificator,
periods: periods,
);
ref.invalidate(doNotDisturbPeriodsProvider(deviceIdentificator));
unawaited(
ref
.read(sfTrackingProvider)
.legacyDeviceDoNotDisturbScheduleSaved(schedule.periods.length),
);
});
}
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'do_not_disturb_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(DoNotDisturbController)
const doNotDisturbControllerProvider = DoNotDisturbControllerProvider._();
final class DoNotDisturbControllerProvider
extends $AsyncNotifierProvider<DoNotDisturbController, void> {
const DoNotDisturbControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'doNotDisturbControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$doNotDisturbControllerHash();
@$internal
@override
DoNotDisturbController create() => DoNotDisturbController();
}
String _$doNotDisturbControllerHash() =>
r'f84df0b051b53a5fadd8801412d45e870aee6637';
abstract class _$DoNotDisturbController 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);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:device_management/src/features/do_not_disturb/domain/do_not_disturb_period.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'do_not_disturb_editor_provider.g.dart';
@riverpod
class DoNotDisturbEditor extends _$DoNotDisturbEditor {
@override
List<DoNotDisturbPeriod>? build() => null;
void add({
required List<DoNotDisturbPeriod> current,
required DoNotDisturbPeriod period,
}) {
final base = state ?? current;
state = [...base, period];
}
void replace({
required List<DoNotDisturbPeriod> current,
required int index,
required DoNotDisturbPeriod period,
}) {
final base = state ?? current;
if (index < 0 || index >= base.length) return;
final updated = [...base];
updated[index] = period;
state = updated;
}
void remove({
required List<DoNotDisturbPeriod> current,
required int index,
}) {
final base = state ?? current;
if (index < 0 || index >= base.length) return;
state = [...base]..removeAt(index);
}
void clear() => state = null;
}

View File

@@ -0,0 +1,66 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'do_not_disturb_editor_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(DoNotDisturbEditor)
const doNotDisturbEditorProvider = DoNotDisturbEditorProvider._();
final class DoNotDisturbEditorProvider
extends $NotifierProvider<DoNotDisturbEditor, List<DoNotDisturbPeriod>?> {
const DoNotDisturbEditorProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'doNotDisturbEditorProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$doNotDisturbEditorHash();
@$internal
@override
DoNotDisturbEditor create() => DoNotDisturbEditor();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<DoNotDisturbPeriod>? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<DoNotDisturbPeriod>?>(value),
);
}
}
String _$doNotDisturbEditorHash() =>
r'a82e3bc2ca0eaf5d185d5e7624d2956acf883269';
abstract class _$DoNotDisturbEditor
extends $Notifier<List<DoNotDisturbPeriod>?> {
List<DoNotDisturbPeriod>? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<List<DoNotDisturbPeriod>?, List<DoNotDisturbPeriod>?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<List<DoNotDisturbPeriod>?, List<DoNotDisturbPeriod>?>,
List<DoNotDisturbPeriod>?,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:device_management/src/core/providers/do_not_disturb_providers.dart';
import 'package:device_management/src/features/do_not_disturb/domain/do_not_disturb_period.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'do_not_disturb_provider.g.dart';
@riverpod
Future<List<DoNotDisturbPeriod>> doNotDisturbPeriods(
Ref ref,
String deviceIdentificator,
) async {
final schedule = await ref
.read(doNotDisturbRemoteDatasourceProvider)
.getSchedule(identificator: deviceIdentificator);
return schedule.periods;
}

View File

@@ -0,0 +1,88 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'do_not_disturb_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(doNotDisturbPeriods)
const doNotDisturbPeriodsProvider = DoNotDisturbPeriodsFamily._();
final class DoNotDisturbPeriodsProvider
extends
$FunctionalProvider<
AsyncValue<List<DoNotDisturbPeriod>>,
List<DoNotDisturbPeriod>,
FutureOr<List<DoNotDisturbPeriod>>
>
with
$FutureModifier<List<DoNotDisturbPeriod>>,
$FutureProvider<List<DoNotDisturbPeriod>> {
const DoNotDisturbPeriodsProvider._({
required DoNotDisturbPeriodsFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'doNotDisturbPeriodsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$doNotDisturbPeriodsHash();
@override
String toString() {
return r'doNotDisturbPeriodsProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<DoNotDisturbPeriod>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<DoNotDisturbPeriod>> create(Ref ref) {
final argument = this.argument as String;
return doNotDisturbPeriods(ref, argument);
}
@override
bool operator ==(Object other) {
return other is DoNotDisturbPeriodsProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$doNotDisturbPeriodsHash() =>
r'da5bacd8516e788f19a356f9c9fac81c351b698d';
final class DoNotDisturbPeriodsFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<DoNotDisturbPeriod>>, String> {
const DoNotDisturbPeriodsFamily._()
: super(
retry: null,
name: r'doNotDisturbPeriodsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
DoNotDisturbPeriodsProvider call(String deviceIdentificator) =>
DoNotDisturbPeriodsProvider._(argument: deviceIdentificator, from: this);
@override
String toString() => r'doNotDisturbPeriodsProvider';
}

View File

@@ -1,132 +0,0 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../../../../core/data/datasources/do_not_disturb_remote_datasource.dart';
import '../../domain/do_not_disturb_period.dart';
import '../../../../core/providers/do_not_disturb_providers.dart';
import 'do_not_disturb_view_state.dart';
final doNotDisturbViewModelProvider =
NotifierProvider.autoDispose<
DoNotDisturbViewModel,
DoNotDisturbViewState
>(DoNotDisturbViewModel.new);
class DoNotDisturbViewModel extends Notifier<DoNotDisturbViewState> {
late final DoNotDisturbRemoteDatasource _datasource;
late final SfTrackingRepository _tracking;
@override
DoNotDisturbViewState build() {
_datasource = ref.read(doNotDisturbRemoteDatasourceProvider);
_tracking = ref.read(sfTrackingProvider);
Future.microtask(_load);
return const DoNotDisturbViewState();
}
String? get _identificator =>
ref.read(selectedDeviceProvider).value?.identificator;
Future<void> _load() async {
final identificator = _identificator;
if (identificator == null) {
state = state.copyWith(isLoading: false);
return;
}
try {
final schedule = await _datasource.getSchedule(
identificator: identificator,
);
if (!ref.mounted) return;
final device = ref.read(selectedDeviceProvider).value;
final capabilities = device?.capabilities;
final maxPeriods = capabilities?.doNotDisturbs?.maxPeriods ?? 4;
state = state.copyWith(
isLoading: false,
periods: schedule.periods,
maxPeriods: maxPeriods,
);
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorEvent: DoNotDisturbErrorEvent.load,
);
}
}
void addPeriod(DoNotDisturbPeriod period) {
if (state.periods.length >= state.maxPeriods) {
state = state.copyWith(errorEvent: DoNotDisturbErrorEvent.maxPeriods);
return;
}
state = state.copyWith(
periods: [...state.periods, period],
errorEvent: null,
);
}
void updatePeriod(int index, DoNotDisturbPeriod period) {
if (index < 0 || index >= state.periods.length) return;
final updated = [...state.periods];
updated[index] = period;
state = state.copyWith(periods: updated, errorEvent: null);
}
void removePeriod(int index) {
if (index < 0 || index >= state.periods.length) return;
final updated = [...state.periods]..removeAt(index);
state = state.copyWith(periods: updated, errorEvent: null);
}
void clearError() {
if (state.errorEvent != null) state = state.copyWith(errorEvent: null);
}
void clearSaveSuccess() {
if (state.saveSuccess) state = state.copyWith(saveSuccess: false);
}
Future<void> save() async {
final identificator = _identificator;
if (identificator == null) return;
state = state.copyWith(
isSaving: true,
saveSuccess: false,
errorEvent: null,
);
try {
final schedule = await _datasource.updatePeriods(
identificator: identificator,
periods: state.periods,
);
if (!ref.mounted) return;
state = state.copyWith(
isSaving: false,
periods: schedule.periods,
saveSuccess: true,
);
unawaited(
_tracking.legacyDeviceDoNotDisturbScheduleSaved(
schedule.periods.length,
),
);
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isSaving: false,
errorEvent: DoNotDisturbErrorEvent.save,
);
}
}
}

View File

@@ -1,19 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/do_not_disturb_period.dart';
part 'do_not_disturb_view_state.freezed.dart';
enum DoNotDisturbErrorEvent { load, save, maxPeriods }
@freezed
abstract class DoNotDisturbViewState with _$DoNotDisturbViewState {
const factory DoNotDisturbViewState({
@Default(true) bool isLoading,
@Default(false) bool isSaving,
@Default([]) List<DoNotDisturbPeriod> periods,
@Default(4) int maxPeriods,
DoNotDisturbErrorEvent? errorEvent,
@Default(false) bool saveSuccess,
}) = _DoNotDisturbViewState;
}

View File

@@ -1,292 +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 'do_not_disturb_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$DoNotDisturbViewState {
bool get isLoading; bool get isSaving; List<DoNotDisturbPeriod> get periods; int get maxPeriods; DoNotDisturbErrorEvent? get errorEvent; bool get saveSuccess;
/// Create a copy of DoNotDisturbViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$DoNotDisturbViewStateCopyWith<DoNotDisturbViewState> get copyWith => _$DoNotDisturbViewStateCopyWithImpl<DoNotDisturbViewState>(this as DoNotDisturbViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is DoNotDisturbViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&const DeepCollectionEquality().equals(other.periods, periods)&&(identical(other.maxPeriods, maxPeriods) || other.maxPeriods == maxPeriods)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,isSaving,const DeepCollectionEquality().hash(periods),maxPeriods,errorEvent,saveSuccess);
@override
String toString() {
return 'DoNotDisturbViewState(isLoading: $isLoading, isSaving: $isSaving, periods: $periods, maxPeriods: $maxPeriods, errorEvent: $errorEvent, saveSuccess: $saveSuccess)';
}
}
/// @nodoc
abstract mixin class $DoNotDisturbViewStateCopyWith<$Res> {
factory $DoNotDisturbViewStateCopyWith(DoNotDisturbViewState value, $Res Function(DoNotDisturbViewState) _then) = _$DoNotDisturbViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, bool isSaving, List<DoNotDisturbPeriod> periods, int maxPeriods, DoNotDisturbErrorEvent? errorEvent, bool saveSuccess
});
}
/// @nodoc
class _$DoNotDisturbViewStateCopyWithImpl<$Res>
implements $DoNotDisturbViewStateCopyWith<$Res> {
_$DoNotDisturbViewStateCopyWithImpl(this._self, this._then);
final DoNotDisturbViewState _self;
final $Res Function(DoNotDisturbViewState) _then;
/// Create a copy of DoNotDisturbViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isSaving = null,Object? periods = null,Object? maxPeriods = null,Object? errorEvent = freezed,Object? saveSuccess = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,periods: null == periods ? _self.periods : periods // ignore: cast_nullable_to_non_nullable
as List<DoNotDisturbPeriod>,maxPeriods: null == maxPeriods ? _self.maxPeriods : maxPeriods // ignore: cast_nullable_to_non_nullable
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as DoNotDisturbErrorEvent?,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [DoNotDisturbViewState].
extension DoNotDisturbViewStatePatterns on DoNotDisturbViewState {
/// 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( _DoNotDisturbViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _DoNotDisturbViewState() 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( _DoNotDisturbViewState value) $default,){
final _that = this;
switch (_that) {
case _DoNotDisturbViewState():
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( _DoNotDisturbViewState value)? $default,){
final _that = this;
switch (_that) {
case _DoNotDisturbViewState() 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( bool isLoading, bool isSaving, List<DoNotDisturbPeriod> periods, int maxPeriods, DoNotDisturbErrorEvent? errorEvent, bool saveSuccess)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _DoNotDisturbViewState() when $default != null:
return $default(_that.isLoading,_that.isSaving,_that.periods,_that.maxPeriods,_that.errorEvent,_that.saveSuccess);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( bool isLoading, bool isSaving, List<DoNotDisturbPeriod> periods, int maxPeriods, DoNotDisturbErrorEvent? errorEvent, bool saveSuccess) $default,) {final _that = this;
switch (_that) {
case _DoNotDisturbViewState():
return $default(_that.isLoading,_that.isSaving,_that.periods,_that.maxPeriods,_that.errorEvent,_that.saveSuccess);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( bool isLoading, bool isSaving, List<DoNotDisturbPeriod> periods, int maxPeriods, DoNotDisturbErrorEvent? errorEvent, bool saveSuccess)? $default,) {final _that = this;
switch (_that) {
case _DoNotDisturbViewState() when $default != null:
return $default(_that.isLoading,_that.isSaving,_that.periods,_that.maxPeriods,_that.errorEvent,_that.saveSuccess);case _:
return null;
}
}
}
/// @nodoc
class _DoNotDisturbViewState implements DoNotDisturbViewState {
const _DoNotDisturbViewState({this.isLoading = true, this.isSaving = false, final List<DoNotDisturbPeriod> periods = const [], this.maxPeriods = 4, this.errorEvent, this.saveSuccess = false}): _periods = periods;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isSaving;
final List<DoNotDisturbPeriod> _periods;
@override@JsonKey() List<DoNotDisturbPeriod> get periods {
if (_periods is EqualUnmodifiableListView) return _periods;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_periods);
}
@override@JsonKey() final int maxPeriods;
@override final DoNotDisturbErrorEvent? errorEvent;
@override@JsonKey() final bool saveSuccess;
/// Create a copy of DoNotDisturbViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$DoNotDisturbViewStateCopyWith<_DoNotDisturbViewState> get copyWith => __$DoNotDisturbViewStateCopyWithImpl<_DoNotDisturbViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DoNotDisturbViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&const DeepCollectionEquality().equals(other._periods, _periods)&&(identical(other.maxPeriods, maxPeriods) || other.maxPeriods == maxPeriods)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,isSaving,const DeepCollectionEquality().hash(_periods),maxPeriods,errorEvent,saveSuccess);
@override
String toString() {
return 'DoNotDisturbViewState(isLoading: $isLoading, isSaving: $isSaving, periods: $periods, maxPeriods: $maxPeriods, errorEvent: $errorEvent, saveSuccess: $saveSuccess)';
}
}
/// @nodoc
abstract mixin class _$DoNotDisturbViewStateCopyWith<$Res> implements $DoNotDisturbViewStateCopyWith<$Res> {
factory _$DoNotDisturbViewStateCopyWith(_DoNotDisturbViewState value, $Res Function(_DoNotDisturbViewState) _then) = __$DoNotDisturbViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, bool isSaving, List<DoNotDisturbPeriod> periods, int maxPeriods, DoNotDisturbErrorEvent? errorEvent, bool saveSuccess
});
}
/// @nodoc
class __$DoNotDisturbViewStateCopyWithImpl<$Res>
implements _$DoNotDisturbViewStateCopyWith<$Res> {
__$DoNotDisturbViewStateCopyWithImpl(this._self, this._then);
final _DoNotDisturbViewState _self;
final $Res Function(_DoNotDisturbViewState) _then;
/// Create a copy of DoNotDisturbViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isSaving = null,Object? periods = null,Object? maxPeriods = null,Object? errorEvent = freezed,Object? saveSuccess = null,}) {
return _then(_DoNotDisturbViewState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,periods: null == periods ? _self._periods : periods // ignore: cast_nullable_to_non_nullable
as List<DoNotDisturbPeriod>,maxPeriods: null == maxPeriods ? _self.maxPeriods : maxPeriods // ignore: cast_nullable_to_non_nullable
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as DoNotDisturbErrorEvent?,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -9,11 +9,13 @@ import 'week_day_row.dart';
class DoNotDisturbPeriodCard extends ConsumerWidget {
final DoNotDisturbPeriod period;
final VoidCallback onEdit;
final VoidCallback onDelete;
const DoNotDisturbPeriodCard({
super.key,
required this.period,
required this.onEdit,
required this.onDelete,
});
@override
@@ -57,6 +59,15 @@ class DoNotDisturbPeriodCard extends ConsumerWidget {
color: primaryColor,
),
),
SizedBox(width: SizeUtils.getByScreen(small: 12, big: 10)),
GestureDetector(
onTap: onDelete,
child: Icon(
Icons.delete_outline,
size: SizeUtils.getByScreen(small: 20, big: 18),
color: Theme.of(context).colorScheme.error,
),
),
],
),
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)),

View File

@@ -0,0 +1,94 @@
import 'package:device_management/src/core/data/datasources/do_not_disturb_remote_datasource.dart';
import 'package:device_management/src/core/providers/do_not_disturb_providers.dart';
import 'package:device_management/src/features/do_not_disturb/domain/do_not_disturb_period.dart';
import 'package:device_management/src/features/do_not_disturb/presentation/providers/do_not_disturb_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 MockDoNotDisturbRemoteDatasource extends Mock
implements DoNotDisturbRemoteDatasource {}
const _period = DoNotDisturbPeriod(
start: '22:00',
end: '07:00',
week: '1111100',
);
void main() {
setUpAll(() {
registerFallbackValue(const <DoNotDisturbPeriod>[]);
});
ProviderContainer buildContainer(DoNotDisturbRemoteDatasource ds) {
return makeContainer(
overrides: [
doNotDisturbRemoteDatasourceProvider.overrideWithValue(ds),
sfTrackingProvider.overrideWithValue(
SfTrackingRepository(clients: const []),
),
],
);
}
group('DoNotDisturbController.save', () {
test('persists periods and transitions to AsyncData', () async {
final ds = MockDoNotDisturbRemoteDatasource();
when(
() => ds.updatePeriods(
identificator: any(named: 'identificator'),
periods: any(named: 'periods'),
),
).thenAnswer((_) async => const DoNotDisturbSchedule(
id: 's1',
deviceIdentificator: 'imei-1',
periods: [_period],
));
final container = buildContainer(ds);
addTearDown(container.dispose);
await container.read(doNotDisturbControllerProvider.notifier).save(
deviceIdentificator: 'imei-1',
periods: const [_period],
);
expect(
container.read(doNotDisturbControllerProvider),
isA<AsyncData<void>>(),
);
verify(
() => ds.updatePeriods(
identificator: 'imei-1',
periods: const [_period],
),
).called(1);
});
test('exposes AsyncError when datasource fails', () async {
final ds = MockDoNotDisturbRemoteDatasource();
when(
() => ds.updatePeriods(
identificator: any(named: 'identificator'),
periods: any(named: 'periods'),
),
).thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(ds);
addTearDown(container.dispose);
await container.read(doNotDisturbControllerProvider.notifier).save(
deviceIdentificator: 'imei-1',
periods: const [_period],
);
expect(
container.read(doNotDisturbControllerProvider),
isA<AsyncError<void>>(),
);
});
});
}

View File

@@ -185,7 +185,7 @@ class _ContactFormSheetWrapperState
);
}
if (!context.mounted) return;
if (!mounted) return;
final state = ref.read(blockPhoneControllerProvider);
if (state.hasError) return;
Navigator.of(context).pop();

View File

@@ -130,7 +130,7 @@ class _AddSosContactSheetState extends ConsumerState<_AddSosContactSheet> {
),
);
if (!context.mounted) return;
if (!mounted) return;
final state = ref.read(sosContactsControllerProvider);
if (state.hasError) return;
Navigator.of(context).pop();

View File

@@ -863,6 +863,8 @@
"doNotDisturbMaxPeriods": "Maximal {max} Zeiträume erlaubt",
"doNotDisturbSave": "Änderungen speichern",
"doNotDisturbSaved": "Zeiträume gespeichert",
"doNotDisturbDeletePeriod": "Zeitraum löschen",
"doNotDisturbDeletePeriodConfirm": "Möchten Sie diesen Zeitraum wirklich löschen? Die Änderung wird beim Speichern angewendet.",
"doNotDisturbConfirm": "Bestätigen",
"doNotDisturbStart": "Beginn",
"doNotDisturbEnd": "Ende",

View File

@@ -574,6 +574,8 @@
"doNotDisturbMaxPeriods": "Maximum {max} periods allowed",
"doNotDisturbSave": "Save changes",
"doNotDisturbSaved": "Periods saved successfully",
"doNotDisturbDeletePeriod": "Delete period",
"doNotDisturbDeletePeriodConfirm": "Are you sure you want to delete this period? The change is applied on save.",
"doNotDisturbConfirm": "Confirm",
"doNotDisturbStart": "Start",
"doNotDisturbEnd": "End",

View File

@@ -575,6 +575,8 @@
"doNotDisturbMaxPeriods": "Máximo {max} periodos permitidos",
"doNotDisturbSave": "Guardar cambios",
"doNotDisturbSaved": "Periodos guardados correctamente",
"doNotDisturbDeletePeriod": "Eliminar período",
"doNotDisturbDeletePeriodConfirm": "¿Seguro que quieres eliminar este período? El cambio se aplica al guardar.",
"doNotDisturbConfirm": "Confirmar",
"doNotDisturbStart": "Inicio",
"doNotDisturbEnd": "Fin",

View File

@@ -863,6 +863,8 @@
"doNotDisturbMaxPeriods": "Maximum {max} périodes autorisées",
"doNotDisturbSave": "Enregistrer",
"doNotDisturbSaved": "Périodes enregistrées",
"doNotDisturbDeletePeriod": "Supprimer la période",
"doNotDisturbDeletePeriodConfirm": "Voulez-vous supprimer cette période ? Le changement s'applique lors de l'enregistrement.",
"doNotDisturbConfirm": "Confirmer",
"doNotDisturbStart": "Début",
"doNotDisturbEnd": "Fin",

View File

@@ -863,6 +863,8 @@
"doNotDisturbMaxPeriods": "Massimo {max} periodi consentiti",
"doNotDisturbSave": "Salva modifiche",
"doNotDisturbSaved": "Periodi salvati",
"doNotDisturbDeletePeriod": "Elimina periodo",
"doNotDisturbDeletePeriodConfirm": "Vuoi eliminare questo periodo? La modifica si applica al salvataggio.",
"doNotDisturbConfirm": "Conferma",
"doNotDisturbStart": "Inizio",
"doNotDisturbEnd": "Fine",

View File

@@ -863,6 +863,8 @@
"doNotDisturbMaxPeriods": "Máximo {max} períodos permitidos",
"doNotDisturbSave": "Guardar alterações",
"doNotDisturbSaved": "Períodos guardados",
"doNotDisturbDeletePeriod": "Eliminar período",
"doNotDisturbDeletePeriodConfirm": "Tens a certeza que queres eliminar este período? A alteração aplica-se ao guardar.",
"doNotDisturbConfirm": "Confirmar",
"doNotDisturbStart": "Início",
"doNotDisturbEnd": "Fim",

View File

@@ -332,6 +332,9 @@ class I18n {
static const String doNotDisturbMaxPeriods = 'doNotDisturbMaxPeriods';
static const String doNotDisturbSave = 'doNotDisturbSave';
static const String doNotDisturbSaved = 'doNotDisturbSaved';
static const String doNotDisturbDeletePeriod = 'doNotDisturbDeletePeriod';
static const String doNotDisturbDeletePeriodConfirm =
'doNotDisturbDeletePeriodConfirm';
static const String doNotDisturbStart = 'doNotDisturbStart';
static const String doNotDisturbStatusOff = 'doNotDisturbStatusOff';
static const String doNotDisturbStatusOn = 'doNotDisturbStatusOn';