feat(device-management): add app usage schedules feature

This commit is contained in:
2026-04-26 05:12:52 +02:00
parent 6193c97802
commit 440adcdf8d
19 changed files with 1910 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import 'package:device_management/src/core/data/models/app_usage_schedule_response_dto.dart';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
abstract class AppUsageSchedulesRemoteDatasource {
Future<AppUsageScheduleResponseDto> getSchedules({
required String identificator,
});
Future<AppUsageScheduleResponseDto> upsertSchedules({
required String identificator,
required List<AppUsageSchedulePeriodEntity> periods,
});
}

View File

@@ -0,0 +1,71 @@
import 'package:device_management/src/core/data/datasources/app_usage_schedules_remote_datasource.dart';
import 'package:device_management/src/core/data/models/app_usage_schedule_response_dto.dart';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
class AppUsageSchedulesRemoteDatasourceImpl
implements AppUsageSchedulesRemoteDatasource {
AppUsageSchedulesRemoteDatasourceImpl(this._repository);
final SaveFamilyRepository _repository;
@override
Future<AppUsageScheduleResponseDto> getSchedules({
required String identificator,
}) async {
final response = await safeCall(
() => _repository.get<Map<String, dynamic>>(
'/devices/identificator/$identificator/app-usage-schedules',
),
'Error getting app usage schedules',
);
final data = response.data;
if (data == null || data.isEmpty) {
throw const ApiException(message: 'Empty response', statusCode: 404);
}
final item = data['item'] as Map<String, dynamic>?;
if (item == null) {
throw const ApiException(message: 'No item in response', statusCode: 404);
}
return AppUsageScheduleResponseDto.fromJson(item);
}
@override
Future<AppUsageScheduleResponseDto> upsertSchedules({
required String identificator,
required List<AppUsageSchedulePeriodEntity> periods,
}) async {
final response = await safeCall(
() => _repository.put<Map<String, dynamic>>(
'/devices/identificator/$identificator/app-usage-schedules',
body: <String, dynamic>{
'periods': periods
.map(
(p) => <String, dynamic>{
'periodStart': p.periodStart,
'periodEnd': p.periodEnd,
'isPeriodEnabled': p.isPeriodEnabled,
},
)
.toList(),
},
),
'Error saving app usage schedules',
);
final data = response.data;
if (data == null || data.isEmpty) {
throw const ApiException(message: 'Empty response');
}
final item = data['item'] as Map<String, dynamic>?;
if (item == null) {
throw const ApiException(message: 'No item in response');
}
return AppUsageScheduleResponseDto.fromJson(item);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_usage_schedule_response_dto.freezed.dart';
part 'app_usage_schedule_response_dto.g.dart';
@freezed
abstract class AppUsageScheduleResponseDto
with _$AppUsageScheduleResponseDto {
const factory AppUsageScheduleResponseDto({
required String id,
required String deviceIdentificator,
required List<AppUsageSchedulePeriodDto> periods,
required int createdAt,
int? updatedAt,
}) = _AppUsageScheduleResponseDto;
factory AppUsageScheduleResponseDto.fromJson(Map<String, dynamic> json) =>
_$AppUsageScheduleResponseDtoFromJson(json);
}
@freezed
abstract class AppUsageSchedulePeriodDto with _$AppUsageSchedulePeriodDto {
const factory AppUsageSchedulePeriodDto({
required String periodStart,
required String periodEnd,
required bool isPeriodEnabled,
}) = _AppUsageSchedulePeriodDto;
factory AppUsageSchedulePeriodDto.fromJson(Map<String, dynamic> json) =>
_$AppUsageSchedulePeriodDtoFromJson(json);
}
extension AppUsageScheduleResponseDtoX on AppUsageScheduleResponseDto {
List<AppUsageSchedulePeriodEntity> toEntities() {
return periods
.map(
(p) => AppUsageSchedulePeriodEntity(
periodStart: p.periodStart,
periodEnd: p.periodEnd,
isPeriodEnabled: p.isPeriodEnabled,
),
)
.toList();
}
}

View File

@@ -0,0 +1,564 @@
// 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 'app_usage_schedule_response_dto.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppUsageScheduleResponseDto {
String get id; String get deviceIdentificator; List<AppUsageSchedulePeriodDto> get periods; int get createdAt; int? get updatedAt;
/// Create a copy of AppUsageScheduleResponseDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AppUsageScheduleResponseDtoCopyWith<AppUsageScheduleResponseDto> get copyWith => _$AppUsageScheduleResponseDtoCopyWithImpl<AppUsageScheduleResponseDto>(this as AppUsageScheduleResponseDto, _$identity);
/// Serializes this AppUsageScheduleResponseDto to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppUsageScheduleResponseDto&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceIdentificator, deviceIdentificator) || other.deviceIdentificator == deviceIdentificator)&&const DeepCollectionEquality().equals(other.periods, periods)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceIdentificator,const DeepCollectionEquality().hash(periods),createdAt,updatedAt);
@override
String toString() {
return 'AppUsageScheduleResponseDto(id: $id, deviceIdentificator: $deviceIdentificator, periods: $periods, createdAt: $createdAt, updatedAt: $updatedAt)';
}
}
/// @nodoc
abstract mixin class $AppUsageScheduleResponseDtoCopyWith<$Res> {
factory $AppUsageScheduleResponseDtoCopyWith(AppUsageScheduleResponseDto value, $Res Function(AppUsageScheduleResponseDto) _then) = _$AppUsageScheduleResponseDtoCopyWithImpl;
@useResult
$Res call({
String id, String deviceIdentificator, List<AppUsageSchedulePeriodDto> periods, int createdAt, int? updatedAt
});
}
/// @nodoc
class _$AppUsageScheduleResponseDtoCopyWithImpl<$Res>
implements $AppUsageScheduleResponseDtoCopyWith<$Res> {
_$AppUsageScheduleResponseDtoCopyWithImpl(this._self, this._then);
final AppUsageScheduleResponseDto _self;
final $Res Function(AppUsageScheduleResponseDto) _then;
/// Create a copy of AppUsageScheduleResponseDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceIdentificator = null,Object? periods = null,Object? createdAt = null,Object? updatedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceIdentificator: null == deviceIdentificator ? _self.deviceIdentificator : deviceIdentificator // ignore: cast_nullable_to_non_nullable
as String,periods: null == periods ? _self.periods : periods // ignore: cast_nullable_to_non_nullable
as List<AppUsageSchedulePeriodDto>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
/// Adds pattern-matching-related methods to [AppUsageScheduleResponseDto].
extension AppUsageScheduleResponseDtoPatterns on AppUsageScheduleResponseDto {
/// 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( _AppUsageScheduleResponseDto value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AppUsageScheduleResponseDto() 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( _AppUsageScheduleResponseDto value) $default,){
final _that = this;
switch (_that) {
case _AppUsageScheduleResponseDto():
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( _AppUsageScheduleResponseDto value)? $default,){
final _that = this;
switch (_that) {
case _AppUsageScheduleResponseDto() 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( String id, String deviceIdentificator, List<AppUsageSchedulePeriodDto> periods, int createdAt, int? updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppUsageScheduleResponseDto() when $default != null:
return $default(_that.id,_that.deviceIdentificator,_that.periods,_that.createdAt,_that.updatedAt);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( String id, String deviceIdentificator, List<AppUsageSchedulePeriodDto> periods, int createdAt, int? updatedAt) $default,) {final _that = this;
switch (_that) {
case _AppUsageScheduleResponseDto():
return $default(_that.id,_that.deviceIdentificator,_that.periods,_that.createdAt,_that.updatedAt);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( String id, String deviceIdentificator, List<AppUsageSchedulePeriodDto> periods, int createdAt, int? updatedAt)? $default,) {final _that = this;
switch (_that) {
case _AppUsageScheduleResponseDto() when $default != null:
return $default(_that.id,_that.deviceIdentificator,_that.periods,_that.createdAt,_that.updatedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _AppUsageScheduleResponseDto implements AppUsageScheduleResponseDto {
const _AppUsageScheduleResponseDto({required this.id, required this.deviceIdentificator, required final List<AppUsageSchedulePeriodDto> periods, required this.createdAt, this.updatedAt}): _periods = periods;
factory _AppUsageScheduleResponseDto.fromJson(Map<String, dynamic> json) => _$AppUsageScheduleResponseDtoFromJson(json);
@override final String id;
@override final String deviceIdentificator;
final List<AppUsageSchedulePeriodDto> _periods;
@override List<AppUsageSchedulePeriodDto> get periods {
if (_periods is EqualUnmodifiableListView) return _periods;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_periods);
}
@override final int createdAt;
@override final int? updatedAt;
/// Create a copy of AppUsageScheduleResponseDto
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AppUsageScheduleResponseDtoCopyWith<_AppUsageScheduleResponseDto> get copyWith => __$AppUsageScheduleResponseDtoCopyWithImpl<_AppUsageScheduleResponseDto>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AppUsageScheduleResponseDtoToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppUsageScheduleResponseDto&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceIdentificator, deviceIdentificator) || other.deviceIdentificator == deviceIdentificator)&&const DeepCollectionEquality().equals(other._periods, _periods)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceIdentificator,const DeepCollectionEquality().hash(_periods),createdAt,updatedAt);
@override
String toString() {
return 'AppUsageScheduleResponseDto(id: $id, deviceIdentificator: $deviceIdentificator, periods: $periods, createdAt: $createdAt, updatedAt: $updatedAt)';
}
}
/// @nodoc
abstract mixin class _$AppUsageScheduleResponseDtoCopyWith<$Res> implements $AppUsageScheduleResponseDtoCopyWith<$Res> {
factory _$AppUsageScheduleResponseDtoCopyWith(_AppUsageScheduleResponseDto value, $Res Function(_AppUsageScheduleResponseDto) _then) = __$AppUsageScheduleResponseDtoCopyWithImpl;
@override @useResult
$Res call({
String id, String deviceIdentificator, List<AppUsageSchedulePeriodDto> periods, int createdAt, int? updatedAt
});
}
/// @nodoc
class __$AppUsageScheduleResponseDtoCopyWithImpl<$Res>
implements _$AppUsageScheduleResponseDtoCopyWith<$Res> {
__$AppUsageScheduleResponseDtoCopyWithImpl(this._self, this._then);
final _AppUsageScheduleResponseDto _self;
final $Res Function(_AppUsageScheduleResponseDto) _then;
/// Create a copy of AppUsageScheduleResponseDto
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceIdentificator = null,Object? periods = null,Object? createdAt = null,Object? updatedAt = freezed,}) {
return _then(_AppUsageScheduleResponseDto(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceIdentificator: null == deviceIdentificator ? _self.deviceIdentificator : deviceIdentificator // ignore: cast_nullable_to_non_nullable
as String,periods: null == periods ? _self._periods : periods // ignore: cast_nullable_to_non_nullable
as List<AppUsageSchedulePeriodDto>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
/// @nodoc
mixin _$AppUsageSchedulePeriodDto {
String get periodStart; String get periodEnd; bool get isPeriodEnabled;
/// Create a copy of AppUsageSchedulePeriodDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AppUsageSchedulePeriodDtoCopyWith<AppUsageSchedulePeriodDto> get copyWith => _$AppUsageSchedulePeriodDtoCopyWithImpl<AppUsageSchedulePeriodDto>(this as AppUsageSchedulePeriodDto, _$identity);
/// Serializes this AppUsageSchedulePeriodDto to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppUsageSchedulePeriodDto&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.isPeriodEnabled, isPeriodEnabled) || other.isPeriodEnabled == isPeriodEnabled));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,periodStart,periodEnd,isPeriodEnabled);
@override
String toString() {
return 'AppUsageSchedulePeriodDto(periodStart: $periodStart, periodEnd: $periodEnd, isPeriodEnabled: $isPeriodEnabled)';
}
}
/// @nodoc
abstract mixin class $AppUsageSchedulePeriodDtoCopyWith<$Res> {
factory $AppUsageSchedulePeriodDtoCopyWith(AppUsageSchedulePeriodDto value, $Res Function(AppUsageSchedulePeriodDto) _then) = _$AppUsageSchedulePeriodDtoCopyWithImpl;
@useResult
$Res call({
String periodStart, String periodEnd, bool isPeriodEnabled
});
}
/// @nodoc
class _$AppUsageSchedulePeriodDtoCopyWithImpl<$Res>
implements $AppUsageSchedulePeriodDtoCopyWith<$Res> {
_$AppUsageSchedulePeriodDtoCopyWithImpl(this._self, this._then);
final AppUsageSchedulePeriodDto _self;
final $Res Function(AppUsageSchedulePeriodDto) _then;
/// Create a copy of AppUsageSchedulePeriodDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? periodStart = null,Object? periodEnd = null,Object? isPeriodEnabled = null,}) {
return _then(_self.copyWith(
periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as String,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as String,isPeriodEnabled: null == isPeriodEnabled ? _self.isPeriodEnabled : isPeriodEnabled // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [AppUsageSchedulePeriodDto].
extension AppUsageSchedulePeriodDtoPatterns on AppUsageSchedulePeriodDto {
/// 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( _AppUsageSchedulePeriodDto value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodDto() 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( _AppUsageSchedulePeriodDto value) $default,){
final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodDto():
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( _AppUsageSchedulePeriodDto value)? $default,){
final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodDto() 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( String periodStart, String periodEnd, bool isPeriodEnabled)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodDto() when $default != null:
return $default(_that.periodStart,_that.periodEnd,_that.isPeriodEnabled);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( String periodStart, String periodEnd, bool isPeriodEnabled) $default,) {final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodDto():
return $default(_that.periodStart,_that.periodEnd,_that.isPeriodEnabled);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( String periodStart, String periodEnd, bool isPeriodEnabled)? $default,) {final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodDto() when $default != null:
return $default(_that.periodStart,_that.periodEnd,_that.isPeriodEnabled);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _AppUsageSchedulePeriodDto implements AppUsageSchedulePeriodDto {
const _AppUsageSchedulePeriodDto({required this.periodStart, required this.periodEnd, required this.isPeriodEnabled});
factory _AppUsageSchedulePeriodDto.fromJson(Map<String, dynamic> json) => _$AppUsageSchedulePeriodDtoFromJson(json);
@override final String periodStart;
@override final String periodEnd;
@override final bool isPeriodEnabled;
/// Create a copy of AppUsageSchedulePeriodDto
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AppUsageSchedulePeriodDtoCopyWith<_AppUsageSchedulePeriodDto> get copyWith => __$AppUsageSchedulePeriodDtoCopyWithImpl<_AppUsageSchedulePeriodDto>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AppUsageSchedulePeriodDtoToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppUsageSchedulePeriodDto&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.isPeriodEnabled, isPeriodEnabled) || other.isPeriodEnabled == isPeriodEnabled));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,periodStart,periodEnd,isPeriodEnabled);
@override
String toString() {
return 'AppUsageSchedulePeriodDto(periodStart: $periodStart, periodEnd: $periodEnd, isPeriodEnabled: $isPeriodEnabled)';
}
}
/// @nodoc
abstract mixin class _$AppUsageSchedulePeriodDtoCopyWith<$Res> implements $AppUsageSchedulePeriodDtoCopyWith<$Res> {
factory _$AppUsageSchedulePeriodDtoCopyWith(_AppUsageSchedulePeriodDto value, $Res Function(_AppUsageSchedulePeriodDto) _then) = __$AppUsageSchedulePeriodDtoCopyWithImpl;
@override @useResult
$Res call({
String periodStart, String periodEnd, bool isPeriodEnabled
});
}
/// @nodoc
class __$AppUsageSchedulePeriodDtoCopyWithImpl<$Res>
implements _$AppUsageSchedulePeriodDtoCopyWith<$Res> {
__$AppUsageSchedulePeriodDtoCopyWithImpl(this._self, this._then);
final _AppUsageSchedulePeriodDto _self;
final $Res Function(_AppUsageSchedulePeriodDto) _then;
/// Create a copy of AppUsageSchedulePeriodDto
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? periodStart = null,Object? periodEnd = null,Object? isPeriodEnabled = null,}) {
return _then(_AppUsageSchedulePeriodDto(
periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as String,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as String,isPeriodEnabled: null == isPeriodEnabled ? _self.isPeriodEnabled : isPeriodEnabled // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_usage_schedule_response_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_AppUsageScheduleResponseDto _$AppUsageScheduleResponseDtoFromJson(
Map<String, dynamic> json,
) => _AppUsageScheduleResponseDto(
id: json['id'] as String,
deviceIdentificator: json['deviceIdentificator'] as String,
periods: (json['periods'] as List<dynamic>)
.map((e) => AppUsageSchedulePeriodDto.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: (json['createdAt'] as num).toInt(),
updatedAt: (json['updatedAt'] as num?)?.toInt(),
);
Map<String, dynamic> _$AppUsageScheduleResponseDtoToJson(
_AppUsageScheduleResponseDto instance,
) => <String, dynamic>{
'id': instance.id,
'deviceIdentificator': instance.deviceIdentificator,
'periods': instance.periods,
'createdAt': instance.createdAt,
'updatedAt': instance.updatedAt,
};
_AppUsageSchedulePeriodDto _$AppUsageSchedulePeriodDtoFromJson(
Map<String, dynamic> json,
) => _AppUsageSchedulePeriodDto(
periodStart: json['periodStart'] as String,
periodEnd: json['periodEnd'] as String,
isPeriodEnabled: json['isPeriodEnabled'] as bool,
);
Map<String, dynamic> _$AppUsageSchedulePeriodDtoToJson(
_AppUsageSchedulePeriodDto instance,
) => <String, dynamic>{
'periodStart': instance.periodStart,
'periodEnd': instance.periodEnd,
'isPeriodEnabled': instance.isPeriodEnabled,
};

View File

@@ -0,0 +1,42 @@
import 'package:device_management/src/core/data/datasources/app_usage_schedules_remote_datasource.dart';
import 'package:device_management/src/core/data/models/app_usage_schedule_response_dto.dart';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:device_management/src/core/domain/repositories/app_usage_schedules_repository.dart';
import 'package:dio/dio.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
class AppUsageSchedulesRepositoryImpl implements AppUsageSchedulesRepository {
AppUsageSchedulesRepositoryImpl(this._remote);
final AppUsageSchedulesRemoteDatasource _remote;
@override
Future<List<AppUsageSchedulePeriodEntity>> getSchedules({
required String identificator,
}) async {
try {
final response = await _remote.getSchedules(
identificator: identificator,
);
return response.toEntities();
} on ApiException catch (e) {
if (e.statusCode == 404) return [];
rethrow;
} on DioException catch (e) {
if (e.response?.statusCode == 404) return [];
throw mapDioError(e, defaultMessage: 'Error getting schedules');
}
}
@override
Future<List<AppUsageSchedulePeriodEntity>> upsertSchedules({
required String identificator,
required List<AppUsageSchedulePeriodEntity> periods,
}) async {
final response = await _remote.upsertSchedules(
identificator: identificator,
periods: periods,
);
return response.toEntities();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_usage_schedule_entity.freezed.dart';
@freezed
abstract class AppUsageSchedulePeriodEntity
with _$AppUsageSchedulePeriodEntity {
const factory AppUsageSchedulePeriodEntity({
required String periodStart,
required String periodEnd,
required bool isPeriodEnabled,
}) = _AppUsageSchedulePeriodEntity;
}

View File

@@ -0,0 +1,277 @@
// 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 'app_usage_schedule_entity.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppUsageSchedulePeriodEntity {
String get periodStart; String get periodEnd; bool get isPeriodEnabled;
/// Create a copy of AppUsageSchedulePeriodEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AppUsageSchedulePeriodEntityCopyWith<AppUsageSchedulePeriodEntity> get copyWith => _$AppUsageSchedulePeriodEntityCopyWithImpl<AppUsageSchedulePeriodEntity>(this as AppUsageSchedulePeriodEntity, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppUsageSchedulePeriodEntity&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.isPeriodEnabled, isPeriodEnabled) || other.isPeriodEnabled == isPeriodEnabled));
}
@override
int get hashCode => Object.hash(runtimeType,periodStart,periodEnd,isPeriodEnabled);
@override
String toString() {
return 'AppUsageSchedulePeriodEntity(periodStart: $periodStart, periodEnd: $periodEnd, isPeriodEnabled: $isPeriodEnabled)';
}
}
/// @nodoc
abstract mixin class $AppUsageSchedulePeriodEntityCopyWith<$Res> {
factory $AppUsageSchedulePeriodEntityCopyWith(AppUsageSchedulePeriodEntity value, $Res Function(AppUsageSchedulePeriodEntity) _then) = _$AppUsageSchedulePeriodEntityCopyWithImpl;
@useResult
$Res call({
String periodStart, String periodEnd, bool isPeriodEnabled
});
}
/// @nodoc
class _$AppUsageSchedulePeriodEntityCopyWithImpl<$Res>
implements $AppUsageSchedulePeriodEntityCopyWith<$Res> {
_$AppUsageSchedulePeriodEntityCopyWithImpl(this._self, this._then);
final AppUsageSchedulePeriodEntity _self;
final $Res Function(AppUsageSchedulePeriodEntity) _then;
/// Create a copy of AppUsageSchedulePeriodEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? periodStart = null,Object? periodEnd = null,Object? isPeriodEnabled = null,}) {
return _then(_self.copyWith(
periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as String,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as String,isPeriodEnabled: null == isPeriodEnabled ? _self.isPeriodEnabled : isPeriodEnabled // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [AppUsageSchedulePeriodEntity].
extension AppUsageSchedulePeriodEntityPatterns on AppUsageSchedulePeriodEntity {
/// 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( _AppUsageSchedulePeriodEntity value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodEntity() 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( _AppUsageSchedulePeriodEntity value) $default,){
final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodEntity():
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( _AppUsageSchedulePeriodEntity value)? $default,){
final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodEntity() 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( String periodStart, String periodEnd, bool isPeriodEnabled)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodEntity() when $default != null:
return $default(_that.periodStart,_that.periodEnd,_that.isPeriodEnabled);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( String periodStart, String periodEnd, bool isPeriodEnabled) $default,) {final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodEntity():
return $default(_that.periodStart,_that.periodEnd,_that.isPeriodEnabled);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( String periodStart, String periodEnd, bool isPeriodEnabled)? $default,) {final _that = this;
switch (_that) {
case _AppUsageSchedulePeriodEntity() when $default != null:
return $default(_that.periodStart,_that.periodEnd,_that.isPeriodEnabled);case _:
return null;
}
}
}
/// @nodoc
class _AppUsageSchedulePeriodEntity implements AppUsageSchedulePeriodEntity {
const _AppUsageSchedulePeriodEntity({required this.periodStart, required this.periodEnd, required this.isPeriodEnabled});
@override final String periodStart;
@override final String periodEnd;
@override final bool isPeriodEnabled;
/// Create a copy of AppUsageSchedulePeriodEntity
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AppUsageSchedulePeriodEntityCopyWith<_AppUsageSchedulePeriodEntity> get copyWith => __$AppUsageSchedulePeriodEntityCopyWithImpl<_AppUsageSchedulePeriodEntity>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppUsageSchedulePeriodEntity&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.isPeriodEnabled, isPeriodEnabled) || other.isPeriodEnabled == isPeriodEnabled));
}
@override
int get hashCode => Object.hash(runtimeType,periodStart,periodEnd,isPeriodEnabled);
@override
String toString() {
return 'AppUsageSchedulePeriodEntity(periodStart: $periodStart, periodEnd: $periodEnd, isPeriodEnabled: $isPeriodEnabled)';
}
}
/// @nodoc
abstract mixin class _$AppUsageSchedulePeriodEntityCopyWith<$Res> implements $AppUsageSchedulePeriodEntityCopyWith<$Res> {
factory _$AppUsageSchedulePeriodEntityCopyWith(_AppUsageSchedulePeriodEntity value, $Res Function(_AppUsageSchedulePeriodEntity) _then) = __$AppUsageSchedulePeriodEntityCopyWithImpl;
@override @useResult
$Res call({
String periodStart, String periodEnd, bool isPeriodEnabled
});
}
/// @nodoc
class __$AppUsageSchedulePeriodEntityCopyWithImpl<$Res>
implements _$AppUsageSchedulePeriodEntityCopyWith<$Res> {
__$AppUsageSchedulePeriodEntityCopyWithImpl(this._self, this._then);
final _AppUsageSchedulePeriodEntity _self;
final $Res Function(_AppUsageSchedulePeriodEntity) _then;
/// Create a copy of AppUsageSchedulePeriodEntity
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? periodStart = null,Object? periodEnd = null,Object? isPeriodEnabled = null,}) {
return _then(_AppUsageSchedulePeriodEntity(
periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as String,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as String,isPeriodEnabled: null == isPeriodEnabled ? _self.isPeriodEnabled : isPeriodEnabled // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,12 @@
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
abstract class AppUsageSchedulesRepository {
Future<List<AppUsageSchedulePeriodEntity>> getSchedules({
required String identificator,
});
Future<List<AppUsageSchedulePeriodEntity>> upsertSchedules({
required String identificator,
required List<AppUsageSchedulePeriodEntity> periods,
});
}

View File

@@ -0,0 +1,12 @@
import 'package:device_management/src/core/data/datasources/app_usage_schedules_remote_datasource_impl.dart';
import 'package:device_management/src/core/data/repositories/app_usage_schedules_repository_impl.dart';
import 'package:device_management/src/core/domain/repositories/app_usage_schedules_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
final appUsageSchedulesRepositoryProvider =
Provider<AppUsageSchedulesRepository>((ref) {
final remote =
AppUsageSchedulesRemoteDatasourceImpl(getIt<SaveFamilyRepository>());
return AppUsageSchedulesRepositoryImpl(remote);
});

View File

@@ -0,0 +1,14 @@
import 'package:device_management/src/features/app_usage_schedules/presentation/app_usage_schedules_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AppUsageSchedulesBuilder {
const AppUsageSchedulesBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
return MaterialPage<void>(
key: state.pageKey,
child: const AppUsageSchedulesScreen(),
);
}
}

View File

@@ -0,0 +1,281 @@
import 'package:design_system/design_system.dart';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:device_management/src/features/app_usage_schedules/presentation/providers/app_usage_schedules_controller.dart';
import 'package:device_management/src/features/app_usage_schedules/presentation/providers/app_usage_schedules_editor_provider.dart';
import 'package:device_management/src/features/app_usage_schedules/presentation/providers/app_usage_schedules_provider.dart';
import 'package:device_management/src/features/app_usage_schedules/presentation/widgets/edit_schedule_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';
class AppUsageSchedulesScreen extends ConsumerWidget {
const AppUsageSchedulesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final device = ref.watch(selectedDeviceProvider).value;
final primaryColor = context.sfColors.legacyPrimary;
final maxPeriods =
device?.capabilities?.appUsageSchedules?.maxPeriods ?? 3;
ref.listen(appUsageSchedulesControllerProvider, (prev, next) async {
if (prev == null || !prev.isLoading || next.isLoading) return;
if (next.hasError) {
await showErrorDialog(context, I18n.errorAppUsageSchedules);
return;
}
await showSuccessDialog(context, I18n.appUsageSchedulesSaved);
});
if (device == null) {
return LegacyPageLayout(
title: context.translate(I18n.appUsageSchedulesTitle),
body: const LegacyLoadingIndicator(),
);
}
final schedulesAsync = ref.watch(
appUsageSchedulesProvider(device.identificator),
);
final pendingPeriods = ref.watch(appUsageSchedulesEditorProvider);
final isSaving = ref.watch(
appUsageSchedulesControllerProvider.select((s) => s.isLoading),
);
return schedulesAsync.when(
loading: () => LegacyPageLayout(
title: context.translate(I18n.appUsageSchedulesTitle),
body: const LegacyLoadingIndicator(),
),
error: (_, __) => LegacyPageLayout(
title: context.translate(I18n.appUsageSchedulesTitle),
body: Center(child: Text(context.translate(I18n.errorGeneric))),
),
data: (fetchedPeriods) {
final periods = pendingPeriods ?? fetchedPeriods;
final canAdd = periods.length < maxPeriods;
return LegacyPageLayout(
title: context.translate(I18n.appUsageSchedulesTitle),
body: SingleChildScrollView(
padding: SizeUtils.getByScreen(
small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10),
big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < periods.length; i++) ...[
_PeriodTile(
index: i,
period: periods[i],
onTap: () async {
final result = await showEditSchedulePeriodSheet(
context,
initial: periods[i],
);
if (result == null) return;
final editor = ref.read(
appUsageSchedulesEditorProvider.notifier,
);
if (pendingPeriods == null) {
editor.init(fetchedPeriods);
}
editor.replace(i, result);
},
onToggle: (enabled) {
final editor = ref.read(
appUsageSchedulesEditorProvider.notifier,
);
if (pendingPeriods == null) {
editor.init(fetchedPeriods);
}
editor.replace(
i,
periods[i].copyWith(isPeriodEnabled: enabled),
);
},
),
if (i < periods.length - 1)
SizedBox(
height: SizeUtils.getByScreen(small: 8, big: 6),
),
],
if (canAdd) ...[
SizedBox(
height: SizeUtils.getByScreen(small: 8, big: 6),
),
_AddPeriodTile(
onTap: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
final result = await showEditSchedulePeriodSheet(context);
if (result == null) return;
final editor = ref.read(
appUsageSchedulesEditorProvider.notifier,
);
if (pendingPeriods == null) {
editor.init(fetchedPeriods);
}
editor.add(result);
},
),
],
SizedBox(
height: SizeUtils.getByScreen(small: 24, big: 20),
),
Text(
context.translate(I18n.appUsageSchedulesHint),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 14),
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
footer: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: isSaving
? const Center(child: CircularProgressIndicator())
: PrimaryButton(
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
await ref
.read(
appUsageSchedulesControllerProvider.notifier,
)
.save(
identificator: device.identificator,
periods: periods,
);
},
text: context.translate(I18n.save),
color: primaryColor,
),
),
);
},
);
}
}
class _PeriodTile extends StatelessWidget {
final int index;
final AppUsageSchedulePeriodEntity period;
final VoidCallback onTap;
final ValueChanged<bool> onToggle;
const _PeriodTile({
required this.index,
required this.period,
required this.onTap,
required this.onToggle,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(14),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 12, big: 10),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${context.translate(I18n.appUsageSchedulesPeriodLabel)} ${index + 1}',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'${period.periodStart} - ${period.periodEnd}',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 14),
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
Switch.adaptive(
value: period.isPeriodEnabled,
onChanged: onToggle,
activeTrackColor: context.sfColors.legacyPrimary,
),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
],
),
),
),
);
}
}
class _AddPeriodTile extends StatelessWidget {
final VoidCallback onTap;
const _AddPeriodTile({required this.onTap});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(14),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 14, big: 12),
),
child: Row(
children: [
Expanded(
child: Text(
context.translate(I18n.appUsageSchedulesAddPeriod),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
fontWeight: FontWeight.w500,
),
),
),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:async';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:device_management/src/core/providers/app_usage_schedules_repository_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_usage_schedules_controller.g.dart';
@Riverpod(keepAlive: true)
class AppUsageSchedulesController extends _$AppUsageSchedulesController {
@override
FutureOr<void> build() {}
Future<void> save({
required String identificator,
required List<AppUsageSchedulePeriodEntity> periods,
}) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(appUsageSchedulesRepositoryProvider).upsertSchedules(
identificator: identificator,
periods: periods,
);
});
}
}

View File

@@ -0,0 +1,57 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_usage_schedules_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AppUsageSchedulesController)
const appUsageSchedulesControllerProvider =
AppUsageSchedulesControllerProvider._();
final class AppUsageSchedulesControllerProvider
extends $AsyncNotifierProvider<AppUsageSchedulesController, void> {
const AppUsageSchedulesControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appUsageSchedulesControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appUsageSchedulesControllerHash();
@$internal
@override
AppUsageSchedulesController create() => AppUsageSchedulesController();
}
String _$appUsageSchedulesControllerHash() =>
r'c46ea8d94d602b29c5b76774e0bb1d0e5d4479ce';
abstract class _$AppUsageSchedulesController 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,33 @@
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_usage_schedules_editor_provider.g.dart';
@Riverpod(keepAlive: true)
class AppUsageSchedulesEditor extends _$AppUsageSchedulesEditor {
@override
List<AppUsageSchedulePeriodEntity>? build() => null;
void init(List<AppUsageSchedulePeriodEntity> periods) {
state = List.of(periods);
}
void add(AppUsageSchedulePeriodEntity period) {
state = [...?state, period];
}
void replace(int index, AppUsageSchedulePeriodEntity period) {
if (state == null || index >= state!.length) return;
final updated = [...state!];
updated[index] = period;
state = updated;
}
void remove(int index) {
if (state == null || index >= state!.length) return;
final updated = [...state!]..removeAt(index);
state = updated;
}
void clear() => state = null;
}

View File

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

View File

@@ -0,0 +1,23 @@
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:device_management/src/core/providers/app_usage_schedules_repository_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_usage_schedules_provider.g.dart';
@riverpod
Future<List<AppUsageSchedulePeriodEntity>> appUsageSchedules(
Ref ref,
String identificator,
) async {
final all = await ref
.read(appUsageSchedulesRepositoryProvider)
.getSchedules(identificator: identificator);
return all
.where(
(p) =>
p.isPeriodEnabled ||
p.periodStart != '00:00' ||
p.periodEnd != '00:00',
)
.toList();
}

View File

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

View File

@@ -0,0 +1,211 @@
import 'package:design_system/design_system.dart';
import 'package:device_management/src/core/domain/entities/app_usage_schedule_entity.dart';
import 'package:flutter/material.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
Future<AppUsageSchedulePeriodEntity?> showEditSchedulePeriodSheet(
BuildContext context, {
AppUsageSchedulePeriodEntity? initial,
}) {
return showModalBottomSheet<AppUsageSchedulePeriodEntity>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _EditSchedulePeriodSheet(initial: initial),
);
}
class _EditSchedulePeriodSheet extends StatefulWidget {
final AppUsageSchedulePeriodEntity? initial;
const _EditSchedulePeriodSheet({this.initial});
@override
State<_EditSchedulePeriodSheet> createState() =>
_EditSchedulePeriodSheetState();
}
class _EditSchedulePeriodSheetState extends State<_EditSchedulePeriodSheet> {
late TimeOfDay _start;
late TimeOfDay _end;
late bool _enabled;
@override
void initState() {
super.initState();
_start = _parse(widget.initial?.periodStart ?? '08:00');
_end = _parse(widget.initial?.periodEnd ?? '10:00');
_enabled = widget.initial?.isPeriodEnabled ?? true;
}
TimeOfDay _parse(String hhmm) {
final parts = hhmm.split(':');
return TimeOfDay(
hour: int.tryParse(parts[0]) ?? 0,
minute: int.tryParse(parts[1]) ?? 0,
);
}
String _format(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
Future<void> _pickTime({required bool isStart}) async {
final picked = await showTimePicker(
context: context,
initialTime: isStart ? _start : _end,
);
if (picked == null) return;
setState(() {
if (isStart) {
_start = picked;
} else {
_end = picked;
}
});
}
void _submit() {
Navigator.of(context).pop(
AppUsageSchedulePeriodEntity(
periodStart: _format(_start),
periodEnd: _format(_end),
isPeriodEnabled: _enabled,
),
);
}
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
padding: EdgeInsets.fromLTRB(
24,
16,
24,
24 + bottomInset + MediaQuery.of(context).padding.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 4,
width: 48,
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(999),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)),
Row(
children: [
Expanded(
child: _TimePickerButton(
label: context.translate(
I18n.appUsageSchedulesPeriodStart,
),
value: _format(_start),
onTap: () => _pickTime(isStart: true),
color: primaryColor,
),
),
SizedBox(width: SizeUtils.getByScreen(small: 16, big: 14)),
Expanded(
child: _TimePickerButton(
label: context.translate(
I18n.appUsageSchedulesPeriodEnd,
),
value: _format(_end),
onTap: () => _pickTime(isStart: false),
color: primaryColor,
),
),
],
),
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translate(I18n.enabled),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
),
),
Switch.adaptive(
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
activeTrackColor: primaryColor,
),
],
),
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
PrimaryButton(
onPressed: _submit,
text: context.translate(I18n.save),
color: primaryColor,
),
],
),
);
}
}
class _TimePickerButton extends StatelessWidget {
final String label;
final String value;
final VoidCallback onTap;
final Color color;
const _TimePickerButton({
required this.label,
required this.value,
required this.onTap,
required this.color,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 14, big: 12),
vertical: SizeUtils.getByScreen(small: 14, big: 12),
),
decoration: BoxDecoration(
border: Border.all(color: color.withValues(alpha: 0.3)),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
),
);
}
}