feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import 'dart:io';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher_string.dart';
@singleton
class AppUpdateService {
Future<void> checkForUpdate(BuildContext context) async {
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(seconds: 1),
));
await remoteConfig.fetchAndActivate();
final minBuildNumber = remoteConfig.getInt(Platform.isIOS?'min_build_number_client_ios':'min_build_number_client_android');
final canSkip = remoteConfig.getBool('can_skip_client');
final canIgnore = remoteConfig.getBool('can_ignore_client');
final message = remoteConfig.getString('message_client');
final packageInfo = await PackageInfo.fromPlatform();
final currentBuildNumber = int.parse(packageInfo.buildNumber);
final prefs = await SharedPreferences.getInstance();
final skippedVersion = prefs.getInt('skipped_version') ?? 0;
if (minBuildNumber > currentBuildNumber &&
minBuildNumber > skippedVersion) {
await _showUpdateDialog(context, message, canSkip, canIgnore, minBuildNumber);
}
}
_showUpdateDialog(
BuildContext context, String message, bool canSkip, bool canIgnore, int minBuildNumber) {
return showDialog(
context: context,
barrierDismissible: canIgnore,
builder: (BuildContext context) {
if (Theme.of(context).platform == TargetPlatform.iOS) {
return WillPopScope(
onWillPop: () async => canIgnore,
child: CupertinoAlertDialog(
title: const Text('Update Available'),
content: Text(
message),
actions: <Widget>[
if (canSkip)
CupertinoDialogAction(
child: const Text('Skip this version'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('skipped_version', minBuildNumber);
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
onPressed:
canIgnore ? () => Navigator.of(context).pop() : null,
child: const Text('Maybe later'),
),
CupertinoDialogAction(
child: const Text('Update'),
onPressed: () async {
var url = dotenv.env['IOS_STORE_URL'] ??
'';
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
throw 'Could not launch $url';
}
},
),
],
),
);
} else {
return WillPopScope(
onWillPop: () async => canIgnore,
child: AlertDialog(
title: const Text('Update Available'),
content: Text(
message),
actions: <Widget>[
if (canSkip)
TextButton(
child: const Text('Skip this version'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('skipped_version', minBuildNumber);
Navigator.of(context).pop();
},
),
TextButton(
onPressed:
canIgnore ? () => Navigator.of(context).pop() : null,
child: const Text('Maybe later'),
),
TextButton(
child: const Text('Update'),
onPressed: () async {
var url = dotenv.env['ANDROID_STORE_URL'] ??
'';
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else {
throw 'Could not launch $url';
}
},
),
],
),
);
}
},
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service_data_provider.dart';
enum AuthStatus {
authenticated,
adminValidation,
prepareProfile,
unauthenticated,
error
}
@injectable
class AuthService {
final AuthServiceDataProvider _dataProvider;
AuthService(this._dataProvider);
Future<AuthStatus> getAuthStatus() async {
User? user;
try {
user = FirebaseAuth.instance.currentUser;
} catch (e) {
return AuthStatus.error;
}
if (user == null) {
return AuthStatus.unauthenticated;
} else {
return AuthStatus.authenticated;
}
//todo get client? check if client is inactive - logout
// final staffStatus = await getCachedStaffStatus();
//
// if (staffStatus == StaffStatus.deactivated) {
// return AuthStatus.unauthenticated;
// } else if (staffStatus == StaffStatus.pending) {
// return AuthStatus.adminValidation;
// } else if (staffStatus == StaffStatus.registered ||
// staffStatus == StaffStatus.declined) {
// return AuthStatus.prepareProfile;
// } else {
return AuthStatus.authenticated;
// }
}
void logout() {
FirebaseAuth.instance.signOut();
getIt<ApiClient>().dropCache();
}
Future<void> deleteAccount() async {
await FirebaseAuth.instance.currentUser!.delete();
getIt<ApiClient>().dropCache();
}
}

View File

@@ -0,0 +1,20 @@
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
@injectable
class AuthServiceDataProvider {
final ApiClient _client;
AuthServiceDataProvider({required ApiClient client}) : _client = client;
// Future<void> getStaffStatus() async {
// final QueryResult result = await _client.query(schema: getStaffStatusQuery);
//
// if (result.hasException) {
// throw Exception(result.exception.toString());
// }
//
// final Map<String, dynamic> data = result.data!['me'];
// return Staff.fromJson(data).status!;
// }
}

View File

@@ -0,0 +1,8 @@
const String getStaffStatusQuery = '''
query GetMe {
me {
id
status
}
}
''';

View File

@@ -0,0 +1,120 @@
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/application/clients/api/api_exception.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/models/event/event_model.dart';
import 'package:krow/core/entity/event_entity.dart';
import 'package:krow/core/entity/position_entity.dart';
import 'package:krow/core/entity/shift_entity.dart';
import 'package:krow/core/sevices/create_event_service/create_event_service_repository.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_input_model.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_position_input_model.dart';
import 'package:krow/features/events/domain/events_repository.dart';
@lazySingleton
class CreateEventService {
EventsRepository? _eventsRepository;
set eventsRepository(EventsRepository eventsRepository) {
_eventsRepository = eventsRepository;
}
Future<String> createEventService(EventEntity entity) async {
late CreateEventInputModel createEventInputModel;
try {
createEventInputModel = createInput(entity);
} catch (e) {
debugPrint('Create event model error: $e');
throw DisplayableException('Field must be filled');
}
var id = await getIt<CreateEventServiceRepository>()
.createEvent(createEventInputModel);
_eventsRepository?.refreshEvents(EventStatus.draft);
return id;
}
Future<bool> validateEventInfo(EventEntity entity) async {
return true;
}
Future<bool> validateShiftInfo(ShiftEntity entity) async {
return true;
}
Future<bool> validateShiftPositionInfo(PositionEntity entity) async {
return true;
}
Future<void> deleteDraft(EventEntity entity) async {
if (entity.id.isEmpty) {
return;
}
await getIt<CreateEventServiceRepository>().deleteDraft(entity.id);
_eventsRepository?.refreshEvents(EventStatus.draft);
}
Future<void> publishEvent(EventEntity entity) async {
var id;
if (entity.id.isEmpty) {
id = await createEventService(entity);
} else {
await updateEvent(entity);
id = entity.id;
}
try {
if (entity.status == EventStatus.draft || entity.status == null) {
await getIt<CreateEventServiceRepository>().publishEvent(id);
await Future.delayed(const Duration(seconds: 1));
_eventsRepository?.refreshEvents(EventStatus.draft);
_eventsRepository?.refreshEvents(EventStatus.pending);
}
} catch (e) {
rethrow;
}
}
Future<void> updateEvent(EventEntity entity) async {
await getIt<CreateEventServiceRepository>()
.updateEvent(createInput(entity));
_eventsRepository?.refreshEvents(entity.status ?? EventStatus.draft);
}
CreateEventInputModel createInput(EventEntity entity) {
return CreateEventInputModel(
id: entity.id.isEmpty ? null : entity.id,
name: entity.name,
date: DateFormat('yyyy-MM-dd').format(entity.startDate ?? DateTime.now()),
hubId: entity.hub!.id,
// contractId: entity.contractNumber,
purchaseOrder: entity.poNumber,
contractType: EventContractType.purchaseOrder,
additionalInfo: entity.additionalInfo,
tags: entity.tags?.map((e) => e.id).toList(),
addons: entity.addons?.map((e) => e.id).toList(),
shifts: entity.shifts?.indexed.map((e) {
var (index, shiftState) = e;
return CreateEventShiftInputModel(
name: 'Shift #${index + 1}',
address: shiftState.fullAddress!,
contacts: shiftState.managers.map((e) => e.id).toList(),
positions: shiftState.positions.map((roleEntity) {
return CreateEventShiftPositionInputModel(
businessSkillId: roleEntity.businessSkill!.id!,
hubDepartmentId: roleEntity.department!.id,
count: roleEntity.count!,
startTime: DateFormat('HH:mm').format(roleEntity.startTime!),
endTime: DateFormat('HH:mm').format(roleEntity.endTime!),
rate: roleEntity.businessSkill!.skill!.price!,
breakDuration: roleEntity.breakDuration!,
);
}).toList());
}).toList(),
);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:krow/core/entity/event_entity.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
abstract class CreateEventServiceRepository {
Future<String> createEvent(CreateEventInputModel input);
Future<void> deleteDraft(String id);
Future<void> publishEvent(String id);
Future<void> updateEvent(CreateEventInputModel input);
}

View File

@@ -0,0 +1,67 @@
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/application/clients/api/api_exception.dart';
import 'package:krow/core/sevices/create_event_service/data/create_event_service_gql.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
@Injectable()
class CreateEventServiceApiProvider {
final ApiClient _client;
CreateEventServiceApiProvider({required ApiClient client}) : _client = client;
Future<String> createEvent(CreateEventInputModel input) async {
var result = await _client.mutate(
schema: createEventMutation,
body: {
'input': input.toJson()..remove('id'),
},
);
if (result.hasException) {
throw parseBackendError(result.exception);
}
return result.data?['client_create_event']['id'] ?? '';
}
Future<void> deleteDraft(String id) async {
var result = await _client.mutate(
schema: deleteDraftMutation,
body: {
'id': id,
},
);
if (result.hasException) {
print(result.exception);
throw parseBackendError(result.exception);
}
}
Future<void> publishEvent(String id) async {
var result = await _client.mutate(
schema: publishDraftMutation,
body: {
'id': id,
},
);
if (result.hasException) {
throw parseBackendError(result.exception);
}
}
Future<void> updateEvent(CreateEventInputModel input) async {
var result = await _client.mutate(
schema: updateEventMutation,
body: {
'input': input.toJson(),
},
);
if (result.hasException) {
throw parseBackendError(result.exception);
}
}
}

View File

@@ -0,0 +1,31 @@
const String createEventMutation = r'''
mutation createEvent ($input: CreateEventInput!) {
client_create_event(input: $input) {
id
}
}
''';
const String updateEventMutation = r'''
mutation updateEvent ($input: UpdateEventInput!) {
client_update_event(input: $input) {
id
}
}
''';
const String deleteDraftMutation = r'''
mutation deleteDraft ($id: ID!) {
delete_client_event(event_id: $id) {
id
}
}
''';
const String publishDraftMutation = r'''
mutation publicsDraft ($id: ID!) {
client_publish_event(id: $id) {
id
}
}
''';

View File

@@ -0,0 +1,34 @@
import 'package:injectable/injectable.dart';
import 'package:krow/core/entity/event_entity.dart';
import 'package:krow/core/sevices/create_event_service/create_event_service_repository.dart';
import 'package:krow/core/sevices/create_event_service/data/create_event_service_api_provider.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
@Singleton(as: CreateEventServiceRepository)
class CreateEventServiceRepositoryImpl extends CreateEventServiceRepository {
final CreateEventServiceApiProvider _apiProvider;
CreateEventServiceRepositoryImpl(
{required CreateEventServiceApiProvider apiProvider})
: _apiProvider = apiProvider;
@override
Future<String> createEvent(CreateEventInputModel input) async {
return _apiProvider.createEvent(input);
}
@override
Future<void> deleteDraft(String id) {
return _apiProvider.deleteDraft(id);
}
@override
Future<void> publishEvent(String id) {
return _apiProvider.publishEvent(id);
}
@override
Future<void> updateEvent(CreateEventInputModel input) {
return _apiProvider.updateEvent(input);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:krow/core/data/models/event/event_model.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_input_model.dart';
part 'create_event_input_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class CreateEventInputModel {
final String? id;
final String name;
final String date;
final String hubId;
// final String? contractId;
final String? purchaseOrder;
final EventContractType contractType;
final String? additionalInfo;
final List<String>? tags;
final List<String>? addons;
final List<CreateEventShiftInputModel>? shifts;
CreateEventInputModel(
{this.id,
required this.name,
required this.date,
required this.hubId,
// required this.contractId,
required this.purchaseOrder,
required this.contractType,
required this.additionalInfo,
required this.tags,
required this.addons,
required this.shifts});
factory CreateEventInputModel.fromJson(Map<String, dynamic> json) {
return _$CreateEventInputModelFromJson(json);
}
Map<String, dynamic> toJson() => _$CreateEventInputModelToJson(this);
}

View File

@@ -0,0 +1,25 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:krow/core/data/models/event/full_address_model.dart';
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_position_input_model.dart';
part 'create_event_shift_input_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class CreateEventShiftInputModel {
final String name;
final FullAddress address;
final List<String>? contacts;
final List<CreateEventShiftPositionInputModel> positions;
CreateEventShiftInputModel(
{required this.name,
required this.address,
required this.contacts,
required this.positions});
factory CreateEventShiftInputModel.fromJson(Map<String, dynamic> json) {
return _$CreateEventShiftInputModelFromJson(json);
}
Map<String, dynamic> toJson() => _$CreateEventShiftInputModelToJson(this);
}

View File

@@ -0,0 +1,32 @@
import 'package:json_annotation/json_annotation.dart';
part 'create_event_shift_position_input_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class CreateEventShiftPositionInputModel {
final String businessSkillId;
final String hubDepartmentId;
final int count;
final String startTime;
final String endTime;
final double rate;
@JsonKey(name: 'break')
final int breakDuration;
CreateEventShiftPositionInputModel(
{required this.businessSkillId,
required this.hubDepartmentId,
required this.count,
required this.startTime,
required this.endTime,
required this.rate,
required this.breakDuration});
factory CreateEventShiftPositionInputModel.fromJson(
Map<String, dynamic> json) {
return _$CreateEventShiftPositionInputModelFromJson(json);
}
Map<String, dynamic> toJson() =>
_$CreateEventShiftPositionInputModelToJson(this);
}

View File

@@ -0,0 +1,89 @@
import 'dart:developer';
import 'package:geolocator/geolocator.dart';
class GeofencingService {
Future<GeolocationStatus> requestGeolocationPermission() async {
LocationPermission permission;
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return GeolocationStatus.denied;
}
}
if (permission == LocationPermission.deniedForever){
return GeolocationStatus.prohibited;
}
if (!(await Geolocator.isLocationServiceEnabled())) {
// Location services are not enabled
return GeolocationStatus.disabled;
}
return GeolocationStatus.enabled;
}
bool _isInRange(
Position currentPosition,
double pointLat,
double pointLon,
int range,
) {
double distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
pointLat,
pointLon,
);
return distance <= range;
}
/// Checks if the user's current location is within [range] meters
/// of the given [pointLatitude] and [pointLongitude].
Future<bool> isInRangeCheck({
required double pointLatitude,
required double pointLongitude,
int range = 500,
}) async {
try {
Position position = await Geolocator.getCurrentPosition();
return _isInRange(position, pointLatitude, pointLongitude, range);
} catch (except) {
log('Error getting location: $except');
return false;
}
}
/// Constantly checks if the user's current location is within [range] meters
/// of the given [pointLatitude] and [pointLongitude].
Stream<bool> isInRangeStream({
required double pointLatitude,
required double pointLongitude,
int range = 500,
}) async* {
await for (final position in Geolocator.getPositionStream()) {
try {
yield _isInRange(position, pointLatitude, pointLongitude, range);
} catch (except) {
log('Error getting location: $except');
yield false;
}
}
}
}
/// [disabled] indicates that the user should enable geolocation on the device.
/// [denied] indicates that permission should be requested or re-requested.
/// [prohibited] indicates that permission is denied and can only be changed
/// via the Settings.
/// [enabled] - geolocation service is allowed and available for usage.
enum GeolocationStatus {
disabled,
denied,
prohibited,
enabled,
}

View File

@@ -0,0 +1,26 @@
class TimeSlotService {
static DateTime calcTime({
required DateTime? currentStartTime,
required DateTime? currentEndTime,
DateTime? startTime,
DateTime? endTime,
}) {
try {
if (endTime != null) {
return endTime.difference(currentStartTime!).inHours < 5 ||
endTime.isBefore(currentStartTime!)
? endTime.subtract(const Duration(hours: 5))
: currentStartTime;
} else if (startTime != null) {
return startTime.difference(currentEndTime!).inHours < 5 ||
startTime.isAfter(currentEndTime)
? startTime.add(const Duration(hours: 5))
: currentEndTime;
}
} catch (e) {
return DateTime.now().subtract(Duration(minutes: DateTime.now().minute % 10));
}
return DateTime.now().subtract(Duration(minutes: DateTime.now().minute % 10));
}
}