Add staff privacy & security feature and routes
This commit is contained in:
@@ -284,13 +284,24 @@ extension StaffNavigator on IModularNavigator {
|
|||||||
pushNamed(StaffPaths.faqs);
|
pushNamed(StaffPaths.faqs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the privacy and security settings page.
|
// ==========================================================================
|
||||||
|
// PRIVACY & SECURITY
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigates to the privacy and security settings page.
|
||||||
///
|
///
|
||||||
/// Manage privacy preferences and security settings.
|
/// Manage privacy preferences including:
|
||||||
void toPrivacy() {
|
/// * Location sharing settings
|
||||||
pushNamed(StaffPaths.privacy);
|
/// * View terms of service
|
||||||
|
/// * View privacy policy
|
||||||
|
void toPrivacySecurity() {
|
||||||
|
pushNamed(StaffPaths.privacySecurity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MESSAGING & COMMUNICATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
/// Pushes the messages page (placeholder).
|
/// Pushes the messages page (placeholder).
|
||||||
///
|
///
|
||||||
/// Access internal messaging system.
|
/// Access internal messaging system.
|
||||||
|
|||||||
@@ -205,8 +205,19 @@ class StaffPaths {
|
|||||||
/// FAQs - frequently asked questions.
|
/// FAQs - frequently asked questions.
|
||||||
static const String faqs = '/faqs';
|
static const String faqs = '/faqs';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// PRIVACY & SECURITY
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
/// Privacy and security settings.
|
/// Privacy and security settings.
|
||||||
static const String privacy = '/privacy';
|
///
|
||||||
|
/// Manage privacy preferences, location sharing, terms of service,
|
||||||
|
/// and privacy policy.
|
||||||
|
static const String privacySecurity = '/worker-main/privacy-security/';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MESSAGING & COMMUNICATION (Placeholders)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
/// Messages - internal messaging system (placeholder).
|
/// Messages - internal messaging system (placeholder).
|
||||||
static const String messages = '/messages';
|
static const String messages = '/messages';
|
||||||
|
|||||||
@@ -1125,6 +1125,21 @@
|
|||||||
"service_unavailable": "Service is currently unavailable."
|
"service_unavailable": "Service is currently unavailable."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"staff_privacy_security": {
|
||||||
|
"title": "Privacy & Security",
|
||||||
|
"privacy_section": "Privacy",
|
||||||
|
"legal_section": "Legal",
|
||||||
|
"location_sharing": {
|
||||||
|
"title": "Location Sharing",
|
||||||
|
"subtitle": "Share location during shifts"
|
||||||
|
},
|
||||||
|
"terms_of_service": {
|
||||||
|
"title": "Terms of Service"
|
||||||
|
},
|
||||||
|
"privacy_policy": {
|
||||||
|
"title": "Privacy Policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"hub": {
|
"hub": {
|
||||||
"created": "Hub created successfully!",
|
"created": "Hub created successfully!",
|
||||||
|
|||||||
@@ -1125,6 +1125,21 @@
|
|||||||
"service_unavailable": "El servicio no está disponible actualmente."
|
"service_unavailable": "El servicio no está disponible actualmente."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"staff_privacy_security": {
|
||||||
|
"title": "Privacidad y Seguridad",
|
||||||
|
"privacy_section": "Privacidad",
|
||||||
|
"legal_section": "Legal",
|
||||||
|
"location_sharing": {
|
||||||
|
"title": "Compartir Ubicación",
|
||||||
|
"subtitle": "Compartir ubicación durante turnos"
|
||||||
|
},
|
||||||
|
"terms_of_service": {
|
||||||
|
"title": "Términos de Servicio"
|
||||||
|
},
|
||||||
|
"privacy_policy": {
|
||||||
|
"title": "Política de Privacidad"
|
||||||
|
}
|
||||||
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"hub": {
|
"hub": {
|
||||||
"created": "¡Hub creado exitosamente!",
|
"created": "¡Hub creado exitosamente!",
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/privacy_settings_entity.dart';
|
||||||
|
import '../../domain/repositories/privacy_settings_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Data layer implementation of privacy settings repository
|
||||||
|
///
|
||||||
|
/// Handles all backend communication for privacy settings,
|
||||||
|
/// using DataConnectService for automatic auth and token refresh
|
||||||
|
class PrivacySettingsRepositoryImpl
|
||||||
|
implements PrivacySettingsRepositoryInterface {
|
||||||
|
final DataConnectService _service;
|
||||||
|
|
||||||
|
PrivacySettingsRepositoryImpl(this._service);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PrivacySettingsEntity> getPrivacySettings() async {
|
||||||
|
return _service.run<PrivacySettingsEntity>(
|
||||||
|
() async {
|
||||||
|
// TODO: Call Data Connect query to fetch privacy settings
|
||||||
|
// For now, return default settings
|
||||||
|
return PrivacySettingsEntity(
|
||||||
|
locationSharing: true,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PrivacySettingsEntity> updateLocationSharing(bool enabled) async {
|
||||||
|
return _service.run<PrivacySettingsEntity>(
|
||||||
|
() async {
|
||||||
|
// TODO: Call Data Connect mutation to update location sharing preference
|
||||||
|
// For now, return updated settings
|
||||||
|
return PrivacySettingsEntity(
|
||||||
|
locationSharing: enabled,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getTermsOfService() async {
|
||||||
|
return _service.run<String>(
|
||||||
|
() async {
|
||||||
|
// TODO: Call Data Connect query to fetch terms of service content
|
||||||
|
// For now, return placeholder
|
||||||
|
return 'Terms of Service Content';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getPrivacyPolicy() async {
|
||||||
|
return _service.run<String>(
|
||||||
|
() async {
|
||||||
|
// TODO: Call Data Connect query to fetch privacy policy content
|
||||||
|
// For now, return placeholder
|
||||||
|
return 'Privacy Policy Content';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Privacy settings entity representing user privacy preferences
|
||||||
|
class PrivacySettingsEntity extends Equatable {
|
||||||
|
/// Whether location sharing during shifts is enabled
|
||||||
|
final bool locationSharing;
|
||||||
|
|
||||||
|
/// The timestamp when these settings were last updated
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const PrivacySettingsEntity({
|
||||||
|
required this.locationSharing,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create a copy with optional field overrides
|
||||||
|
PrivacySettingsEntity copyWith({
|
||||||
|
bool? locationSharing,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return PrivacySettingsEntity(
|
||||||
|
locationSharing: locationSharing ?? this.locationSharing,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [locationSharing, updatedAt];
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import '../entities/privacy_settings_entity.dart';
|
||||||
|
|
||||||
|
/// Interface for privacy settings repository operations
|
||||||
|
abstract class PrivacySettingsRepositoryInterface {
|
||||||
|
/// Fetch the current user's privacy settings
|
||||||
|
Future<PrivacySettingsEntity> getPrivacySettings();
|
||||||
|
|
||||||
|
/// Update location sharing preference
|
||||||
|
///
|
||||||
|
/// Returns the updated privacy settings
|
||||||
|
Future<PrivacySettingsEntity> updateLocationSharing(bool enabled);
|
||||||
|
|
||||||
|
/// Fetch terms of service content
|
||||||
|
Future<String> getTermsOfService();
|
||||||
|
|
||||||
|
/// Fetch privacy policy content
|
||||||
|
Future<String> getPrivacyPolicy();
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import '../repositories/privacy_settings_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case to retrieve privacy policy
|
||||||
|
class GetPrivacyPolicyUseCase {
|
||||||
|
final PrivacySettingsRepositoryInterface _repository;
|
||||||
|
|
||||||
|
GetPrivacyPolicyUseCase(this._repository);
|
||||||
|
|
||||||
|
/// Execute the use case to get privacy policy
|
||||||
|
Future<String> call() async {
|
||||||
|
try {
|
||||||
|
return await _repository.getPrivacyPolicy();
|
||||||
|
} catch (e) {
|
||||||
|
return 'Privacy Policy is currently unavailable.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import '../entities/privacy_settings_entity.dart';
|
||||||
|
import '../repositories/privacy_settings_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case to retrieve the current user's privacy settings
|
||||||
|
class GetPrivacySettingsUseCase {
|
||||||
|
final PrivacySettingsRepositoryInterface _repository;
|
||||||
|
|
||||||
|
GetPrivacySettingsUseCase(this._repository);
|
||||||
|
|
||||||
|
/// Execute the use case to get privacy settings
|
||||||
|
Future<PrivacySettingsEntity> call() async {
|
||||||
|
try {
|
||||||
|
return await _repository.getPrivacySettings();
|
||||||
|
} catch (e) {
|
||||||
|
// Return default settings on error
|
||||||
|
return PrivacySettingsEntity(
|
||||||
|
locationSharing: true,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import '../repositories/privacy_settings_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case to retrieve terms of service
|
||||||
|
class GetTermsUseCase {
|
||||||
|
final PrivacySettingsRepositoryInterface _repository;
|
||||||
|
|
||||||
|
GetTermsUseCase(this._repository);
|
||||||
|
|
||||||
|
/// Execute the use case to get terms of service
|
||||||
|
Future<String> call() async {
|
||||||
|
try {
|
||||||
|
return await _repository.getTermsOfService();
|
||||||
|
} catch (e) {
|
||||||
|
return 'Terms of Service is currently unavailable.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import '../entities/privacy_settings_entity.dart';
|
||||||
|
import '../repositories/privacy_settings_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Parameters for updating location sharing
|
||||||
|
class UpdateLocationSharingParams extends Equatable {
|
||||||
|
/// Whether to enable or disable location sharing
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
const UpdateLocationSharingParams({required this.enabled});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [enabled];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use case to update location sharing preference
|
||||||
|
class UpdateLocationSharingUseCase {
|
||||||
|
final PrivacySettingsRepositoryInterface _repository;
|
||||||
|
|
||||||
|
UpdateLocationSharingUseCase(this._repository);
|
||||||
|
|
||||||
|
/// Execute the use case to update location sharing
|
||||||
|
Future<PrivacySettingsEntity> call(UpdateLocationSharingParams params) async {
|
||||||
|
try {
|
||||||
|
return await _repository.updateLocationSharing(params.enabled);
|
||||||
|
} catch (e) {
|
||||||
|
// Return current settings on error
|
||||||
|
return PrivacySettingsEntity(
|
||||||
|
locationSharing: params.enabled,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/privacy_settings_entity.dart';
|
||||||
|
import '../../domain/repositories/privacy_settings_repository_interface.dart';
|
||||||
|
import '../../domain/usecases/get_privacy_settings_usecase.dart';
|
||||||
|
import '../../domain/usecases/update_location_sharing_usecase.dart';
|
||||||
|
import '../../domain/usecases/get_terms_usecase.dart';
|
||||||
|
import '../../domain/usecases/get_privacy_policy_usecase.dart';
|
||||||
|
|
||||||
|
part 'privacy_security_event.dart';
|
||||||
|
part 'privacy_security_state.dart';
|
||||||
|
|
||||||
|
/// BLoC managing privacy and security settings state
|
||||||
|
class PrivacySecurityBloc
|
||||||
|
extends Bloc<PrivacySecurityEvent, PrivacySecurityState> {
|
||||||
|
final GetPrivacySettingsUseCase _getPrivacySettingsUseCase;
|
||||||
|
final UpdateLocationSharingUseCase _updateLocationSharingUseCase;
|
||||||
|
final GetTermsUseCase _getTermsUseCase;
|
||||||
|
final GetPrivacyPolicyUseCase _getPrivacyPolicyUseCase;
|
||||||
|
|
||||||
|
PrivacySecurityBloc({
|
||||||
|
required GetPrivacySettingsUseCase getPrivacySettingsUseCase,
|
||||||
|
required UpdateLocationSharingUseCase updateLocationSharingUseCase,
|
||||||
|
required GetTermsUseCase getTermsUseCase,
|
||||||
|
required GetPrivacyPolicyUseCase getPrivacyPolicyUseCase,
|
||||||
|
}) : _getPrivacySettingsUseCase = getPrivacySettingsUseCase,
|
||||||
|
_updateLocationSharingUseCase = updateLocationSharingUseCase,
|
||||||
|
_getTermsUseCase = getTermsUseCase,
|
||||||
|
_getPrivacyPolicyUseCase = getPrivacyPolicyUseCase,
|
||||||
|
super(const PrivacySecurityState()) {
|
||||||
|
on<FetchPrivacySettingsEvent>(_onFetchPrivacySettings);
|
||||||
|
on<UpdateLocationSharingEvent>(_onUpdateLocationSharing);
|
||||||
|
on<FetchTermsEvent>(_onFetchTerms);
|
||||||
|
on<FetchPrivacyPolicyEvent>(_onFetchPrivacyPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFetchPrivacySettings(
|
||||||
|
FetchPrivacySettingsEvent event,
|
||||||
|
Emitter<PrivacySecurityState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoading: true, error: null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final settings = await _getPrivacySettingsUseCase.call();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
privacySettings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
error: 'Failed to fetch privacy settings',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpdateLocationSharing(
|
||||||
|
UpdateLocationSharingEvent event,
|
||||||
|
Emitter<PrivacySecurityState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isUpdating: true, error: null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final settings = await _updateLocationSharingUseCase.call(
|
||||||
|
UpdateLocationSharingParams(enabled: event.enabled),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isUpdating: false,
|
||||||
|
privacySettings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isUpdating: false,
|
||||||
|
error: 'Failed to update location sharing',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFetchTerms(
|
||||||
|
FetchTermsEvent event,
|
||||||
|
Emitter<PrivacySecurityState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoadingTerms: true, error: null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final content = await _getTermsUseCase.call();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingTerms: false,
|
||||||
|
termsContent: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingTerms: false,
|
||||||
|
error: 'Failed to fetch terms of service',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFetchPrivacyPolicy(
|
||||||
|
FetchPrivacyPolicyEvent event,
|
||||||
|
Emitter<PrivacySecurityState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoadingPrivacyPolicy: true, error: null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final content = await _getPrivacyPolicyUseCase.call();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingPrivacyPolicy: false,
|
||||||
|
privacyPolicyContent: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingPrivacyPolicy: false,
|
||||||
|
error: 'Failed to fetch privacy policy',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
part of 'privacy_security_bloc.dart';
|
||||||
|
|
||||||
|
/// Base class for privacy security BLoC events
|
||||||
|
abstract class PrivacySecurityEvent extends Equatable {
|
||||||
|
const PrivacySecurityEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event to fetch current privacy settings
|
||||||
|
class FetchPrivacySettingsEvent extends PrivacySecurityEvent {
|
||||||
|
const FetchPrivacySettingsEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event to update location sharing preference
|
||||||
|
class UpdateLocationSharingEvent extends PrivacySecurityEvent {
|
||||||
|
/// Whether to enable or disable location sharing
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
const UpdateLocationSharingEvent({required this.enabled});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [enabled];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event to fetch terms of service
|
||||||
|
class FetchTermsEvent extends PrivacySecurityEvent {
|
||||||
|
const FetchTermsEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event to fetch privacy policy
|
||||||
|
class FetchPrivacyPolicyEvent extends PrivacySecurityEvent {
|
||||||
|
const FetchPrivacyPolicyEvent();
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
part of 'privacy_security_bloc.dart';
|
||||||
|
|
||||||
|
/// State for privacy security BLoC
|
||||||
|
class PrivacySecurityState extends Equatable {
|
||||||
|
/// Current privacy settings
|
||||||
|
final PrivacySettingsEntity? privacySettings;
|
||||||
|
|
||||||
|
/// Whether settings are currently loading
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
/// Whether settings are currently being updated
|
||||||
|
final bool isUpdating;
|
||||||
|
|
||||||
|
/// Terms of service content
|
||||||
|
final String? termsContent;
|
||||||
|
|
||||||
|
/// Whether terms are currently loading
|
||||||
|
final bool isLoadingTerms;
|
||||||
|
|
||||||
|
/// Privacy policy content
|
||||||
|
final String? privacyPolicyContent;
|
||||||
|
|
||||||
|
/// Whether privacy policy is currently loading
|
||||||
|
final bool isLoadingPrivacyPolicy;
|
||||||
|
|
||||||
|
/// Error message, if any
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const PrivacySecurityState({
|
||||||
|
this.privacySettings,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.isUpdating = false,
|
||||||
|
this.termsContent,
|
||||||
|
this.isLoadingTerms = false,
|
||||||
|
this.privacyPolicyContent,
|
||||||
|
this.isLoadingPrivacyPolicy = false,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create a copy with optional field overrides
|
||||||
|
PrivacySecurityState copyWith({
|
||||||
|
PrivacySettingsEntity? privacySettings,
|
||||||
|
bool? isLoading,
|
||||||
|
bool? isUpdating,
|
||||||
|
String? termsContent,
|
||||||
|
bool? isLoadingTerms,
|
||||||
|
String? privacyPolicyContent,
|
||||||
|
bool? isLoadingPrivacyPolicy,
|
||||||
|
String? error,
|
||||||
|
}) {
|
||||||
|
return PrivacySecurityState(
|
||||||
|
privacySettings: privacySettings ?? this.privacySettings,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
isUpdating: isUpdating ?? this.isUpdating,
|
||||||
|
termsContent: termsContent ?? this.termsContent,
|
||||||
|
isLoadingTerms: isLoadingTerms ?? this.isLoadingTerms,
|
||||||
|
privacyPolicyContent: privacyPolicyContent ?? this.privacyPolicyContent,
|
||||||
|
isLoadingPrivacyPolicy:
|
||||||
|
isLoadingPrivacyPolicy ?? this.isLoadingPrivacyPolicy,
|
||||||
|
error: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
privacySettings,
|
||||||
|
isLoading,
|
||||||
|
isUpdating,
|
||||||
|
termsContent,
|
||||||
|
isLoadingTerms,
|
||||||
|
privacyPolicyContent,
|
||||||
|
isLoadingPrivacyPolicy,
|
||||||
|
error,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
/// Extension on IModularNavigator for privacy security navigation
|
||||||
|
extension PrivacySecurityNavigator on IModularNavigator {
|
||||||
|
/// Navigate to privacy security page
|
||||||
|
Future<dynamic> toPrivacySecurityPage() {
|
||||||
|
return pushNamed('/privacy-security');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/// Navigation route paths for privacy security feature
|
||||||
|
class PrivacySecurityPaths {
|
||||||
|
/// Route to privacy security main page
|
||||||
|
static const String privacySecurity = '/privacy-security';
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
import '../blocs/privacy_security_bloc.dart';
|
||||||
|
import '../widgets/settings_action_tile_widget.dart';
|
||||||
|
import '../widgets/settings_divider_widget.dart';
|
||||||
|
import '../widgets/settings_section_header_widget.dart';
|
||||||
|
import '../widgets/settings_switch_tile_widget.dart';
|
||||||
|
|
||||||
|
/// Page displaying privacy & security settings for staff
|
||||||
|
class PrivacySecurityPage extends StatelessWidget {
|
||||||
|
const PrivacySecurityPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: UiAppBar(
|
||||||
|
title: t.staff_privacy_security.title,
|
||||||
|
showBackButton: true,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Container(color: UiColors.border, height: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: BlocProvider<PrivacySecurityBloc>.value(
|
||||||
|
value: Modular.get<PrivacySecurityBloc>()
|
||||||
|
..add(const FetchPrivacySettingsEvent()),
|
||||||
|
child: BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
|
||||||
|
builder: (BuildContext context, PrivacySecurityState state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return const UiLoadingPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space6),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// Privacy Section
|
||||||
|
SettingsSectionHeader(
|
||||||
|
title: t.staff_privacy_security.privacy_section,
|
||||||
|
icon: Icons.visibility,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SettingsSwitchTile(
|
||||||
|
title:
|
||||||
|
t.staff_privacy_security.location_sharing.title,
|
||||||
|
subtitle: t
|
||||||
|
.staff_privacy_security
|
||||||
|
.location_sharing
|
||||||
|
.subtitle,
|
||||||
|
value:
|
||||||
|
state.privacySettings?.locationSharing ?? false,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
BlocProvider.of<PrivacySecurityBloc>(context)
|
||||||
|
.add(
|
||||||
|
UpdateLocationSharingEvent(enabled: value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24.0),
|
||||||
|
|
||||||
|
// Legal Section
|
||||||
|
SettingsSectionHeader(
|
||||||
|
title: t.staff_privacy_security.legal_section,
|
||||||
|
icon: Icons.shield,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SettingsActionTile(
|
||||||
|
title:
|
||||||
|
t.staff_privacy_security.terms_of_service.title,
|
||||||
|
onTap: () => _showTermsDialog(context),
|
||||||
|
),
|
||||||
|
const SettingsDivider(),
|
||||||
|
SettingsActionTile(
|
||||||
|
title: t.staff_privacy_security.privacy_policy.title,
|
||||||
|
onTap: () => _showPrivacyPolicyDialog(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show terms of service in a modal dialog
|
||||||
|
void _showTermsDialog(BuildContext context) {
|
||||||
|
BlocProvider.of<PrivacySecurityBloc>(context)
|
||||||
|
.add(const FetchTermsEvent());
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) =>
|
||||||
|
BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
|
||||||
|
builder: (BuildContext context, PrivacySecurityState state) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
t.staff_privacy_security.terms_of_service.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Text(
|
||||||
|
state.termsContent ?? 'Loading...',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Modular.to.pop(),
|
||||||
|
child: Text(t.common.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show privacy policy in a modal dialog
|
||||||
|
void _showPrivacyPolicyDialog(BuildContext context) {
|
||||||
|
BlocProvider.of<PrivacySecurityBloc>(context)
|
||||||
|
.add(const FetchPrivacyPolicyEvent());
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) =>
|
||||||
|
BlocBuilder<PrivacySecurityBloc, PrivacySecurityState>(
|
||||||
|
builder: (BuildContext context, PrivacySecurityState state) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
t.staff_privacy_security.privacy_policy.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Text(
|
||||||
|
state.privacyPolicyContent ?? 'Loading...',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Modular.to.pop(),
|
||||||
|
child: Text(t.common.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
/// Reusable widget for action tile (tap to navigate)
|
||||||
|
class SettingsActionTile extends StatelessWidget {
|
||||||
|
/// The title of the action
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Optional subtitle describing the action
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
/// Callback when tile is tapped
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const SettingsActionTile({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.body2r.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...<Widget>[
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.muted,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
/// Divider widget for separating items within settings sections
|
||||||
|
class SettingsDivider extends StatelessWidget {
|
||||||
|
const SettingsDivider({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Divider(
|
||||||
|
height: 1,
|
||||||
|
color: UiColors.border,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
/// Reusable widget for settings section header with icon
|
||||||
|
class SettingsSectionHeader extends StatelessWidget {
|
||||||
|
/// The title of the section
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The icon to display next to the title
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const SettingsSectionHeader({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.body1r.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
/// Reusable widget for toggle tile in privacy settings
|
||||||
|
class SettingsSwitchTile extends StatelessWidget {
|
||||||
|
/// The title of the setting
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The subtitle describing the setting
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
/// Current toggle value
|
||||||
|
final bool value;
|
||||||
|
|
||||||
|
/// Callback when toggle is changed
|
||||||
|
final ValueChanged<bool> onChanged;
|
||||||
|
|
||||||
|
const SettingsSwitchTile({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.body2r.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
activeColor: UiColors.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export 'src/domain/entities/privacy_settings_entity.dart';
|
||||||
|
export 'src/domain/repositories/privacy_settings_repository_interface.dart';
|
||||||
|
export 'src/domain/usecases/get_privacy_settings_usecase.dart';
|
||||||
|
export 'src/domain/usecases/update_location_sharing_usecase.dart';
|
||||||
|
export 'src/domain/usecases/get_terms_usecase.dart';
|
||||||
|
export 'src/domain/usecases/get_privacy_policy_usecase.dart';
|
||||||
|
export 'src/data/repositories_impl/privacy_settings_repository_impl.dart';
|
||||||
|
export 'src/presentation/blocs/privacy_security_bloc.dart';
|
||||||
|
export 'src/presentation/pages/privacy_security_page.dart';
|
||||||
|
export 'src/presentation/widgets/settings_switch_tile_widget.dart';
|
||||||
|
export 'src/presentation/widgets/settings_action_tile_widget.dart';
|
||||||
|
export 'src/presentation/widgets/settings_section_header_widget.dart';
|
||||||
|
export 'src/presentation/widgets/settings_divider_widget.dart';
|
||||||
|
export 'staff_privacy_security_module.dart';
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
|
import 'src/data/repositories_impl/privacy_settings_repository_impl.dart';
|
||||||
|
import 'src/domain/repositories/privacy_settings_repository_interface.dart';
|
||||||
|
import 'src/domain/usecases/get_privacy_policy_usecase.dart';
|
||||||
|
import 'src/domain/usecases/get_privacy_settings_usecase.dart';
|
||||||
|
import 'src/domain/usecases/get_terms_usecase.dart';
|
||||||
|
import 'src/domain/usecases/update_location_sharing_usecase.dart';
|
||||||
|
import 'src/presentation/blocs/privacy_security_bloc.dart';
|
||||||
|
import 'src/presentation/pages/privacy_security_page.dart';
|
||||||
|
|
||||||
|
/// Module for privacy security feature
|
||||||
|
///
|
||||||
|
/// Provides:
|
||||||
|
/// - Dependency injection for repositories, use cases, and BLoCs
|
||||||
|
/// - Route definitions delegated to core routing
|
||||||
|
class PrivacySecurityModule extends Module {
|
||||||
|
@override
|
||||||
|
void binds(i) {
|
||||||
|
// Repository
|
||||||
|
i.addSingleton<PrivacySettingsRepositoryInterface>(
|
||||||
|
() => PrivacySettingsRepositoryImpl(
|
||||||
|
Modular.get<DataConnectService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
i.addSingleton(
|
||||||
|
() => GetPrivacySettingsUseCase(
|
||||||
|
i<PrivacySettingsRepositoryInterface>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addSingleton(
|
||||||
|
() => UpdateLocationSharingUseCase(
|
||||||
|
i<PrivacySettingsRepositoryInterface>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addSingleton(
|
||||||
|
() => GetTermsUseCase(
|
||||||
|
i<PrivacySettingsRepositoryInterface>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.addSingleton(
|
||||||
|
() => GetPrivacyPolicyUseCase(
|
||||||
|
i<PrivacySettingsRepositoryInterface>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// BLoC
|
||||||
|
i.addSingleton(
|
||||||
|
() => PrivacySecurityBloc(
|
||||||
|
getPrivacySettingsUseCase: i(),
|
||||||
|
updateLocationSharingUseCase: i(),
|
||||||
|
getTermsUseCase: i(),
|
||||||
|
getPrivacyPolicyUseCase: i(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@override
|
||||||
|
void routes(r) {
|
||||||
|
// Route is handled by core routing (StaffPaths.privacySecurity)
|
||||||
|
r.child(
|
||||||
|
'/',
|
||||||
|
child: (context) => const PrivacySecurityPage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: staff_privacy_security
|
||||||
|
description: Privacy & Security settings feature for staff application.
|
||||||
|
version: 0.0.1
|
||||||
|
publish_to: none
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_bloc: ^8.1.0
|
||||||
|
flutter_modular: ^6.3.0
|
||||||
|
equatable: ^2.0.5
|
||||||
|
firebase_data_connect: ^0.2.2+1
|
||||||
|
url_launcher: ^6.2.0
|
||||||
|
|
||||||
|
# Architecture Packages
|
||||||
|
krow_domain:
|
||||||
|
path: ../../../../../domain
|
||||||
|
krow_data_connect:
|
||||||
|
path: ../../../../../data_connect
|
||||||
|
krow_core:
|
||||||
|
path: ../../../../../core
|
||||||
|
design_system:
|
||||||
|
path: ../../../../../design_system
|
||||||
|
core_localization:
|
||||||
|
path: ../../../../../core_localization
|
||||||
|
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
bloc_test: ^9.1.0
|
||||||
|
mocktail: ^1.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
Reference in New Issue
Block a user