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,96 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:path_provider/path_provider.dart';
class GraphQLConfig {
static HttpLink httpLink = HttpLink(
dotenv.env['BASE_URL']!,
defaultHeaders: {
'Content-Type': 'application/json',
},
);
static AuthLink authLink = AuthLink(
getToken: () async {
User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
return 'Bearer ${await user.getIdToken()}';
}
return null;
},
);
static Link link = authLink.concat(httpLink);
Future<GraphQLClient> configClient() async {
var path = await getApplicationDocumentsDirectory();
Store? store;
if (currentEnv != 'background') {
store = await HiveStore.open(path: '${path.path}/gql_cache');
}
return GraphQLClient(
link: link,
cache: GraphQLCache(
store: store,
),
defaultPolicies: DefaultPolicies(
mutate: Policies(
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
error: ErrorPolicy.all,
fetch: FetchPolicy.networkOnly,
),
query: Policies(
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
error: ErrorPolicy.all,
fetch: FetchPolicy.networkOnly,
),
));
}
}
@Singleton()
class ApiClient {
static final GraphQLConfig _graphQLConfig = GraphQLConfig();
final Future<GraphQLClient> _client = _graphQLConfig.configClient();
Future<QueryResult> query(
{required String schema, Map<String, dynamic>? body}) async {
final QueryOptions options = QueryOptions(
document: gql(schema),
variables: body ?? {},
);
return (await _client).query(options);
}
Stream<QueryResult?> queryWithCache({
required String schema,
Map<String, dynamic>? body,
}) async* {
final WatchQueryOptions options = WatchQueryOptions(
document: gql(schema),
variables: body ?? {},
fetchPolicy: FetchPolicy.cacheAndNetwork);
var result = (await _client).watchQuery(options).fetchResults();
yield result.eagerResult;
yield await result.networkResult;
}
Future<QueryResult> mutate(
{required String schema, Map<String, dynamic>? body}) async {
final MutationOptions options = MutationOptions(
document: gql(schema),
variables: body ?? {},
);
return (await _client).mutate(options);
}
void dropCache() async {
(await _client).cache.store.reset();
}
}

View File

@@ -0,0 +1,54 @@
import 'package:graphql_flutter/graphql_flutter.dart';
interface class DisplayableException {
final String message;
DisplayableException(this.message);
}
class NetworkException implements Exception, DisplayableException {
@override
final String message;
NetworkException([this.message = 'No internet connection']);
@override
String toString() {
return message;
}
}
class GraphQLException implements Exception, DisplayableException {
@override
final String message;
GraphQLException(this.message);
@override
String toString() {
return message;
}
}
class UnknownException implements Exception, DisplayableException {
@override
final String message;
UnknownException([this.message = 'Something went wrong']);
@override
String toString() {
return message;
}
}
Exception parseBackendError(dynamic error) {
if (error is OperationException) {
if (error.graphqlErrors.isNotEmpty) {
return GraphQLException(
error.graphqlErrors.firstOrNull?.message ?? 'GraphQL error occurred');
}
return NetworkException('Network error occurred');
}
return UnknownException();
}

View File

@@ -0,0 +1,90 @@
const String staffFragment = '''
fragment StaffFields on Staff {
id
first_name
last_name
middle_name
email
phone
status
address
avatar
average_rating
}
''';
const String roleKitFragment = '''
fragment RoleKit on RoleKit {
id
name
is_required
photo_required
}
''';
const String skillCategoryFragment = '''
fragment SkillCategory on Skill {
category {
name
slug
}
}
''';
const String skillFragment = '''
$skillCategoryFragment
$roleKitFragment
fragment SkillFragment on Skill {
id
name
slug
price
uniforms {
...RoleKit
}
equipments {
...RoleKit
}
...SkillCategory
}
''';
String getSkillsQuery = '''
$skillFragment
query GetSkills {
skills {
...SkillFragment
}
}
''';
const String getStaffRolesQuery = '''
$skillFragment
query GetStaffRoles {
staff_roles {
id
skill {
...SkillFragment
}
confirmed_uniforms {
id
skill_kit_id
photo
}
confirmed_equipments {
id
skill_kit_id
photo
}
level
experience
status
}
}
''';

View File

@@ -0,0 +1,4 @@
extension BoolExtension on bool {
/// Returns 0 if boolean is true and 1 otherwise.
int toInt() => this ? 0 : 1;
}

View File

@@ -0,0 +1,32 @@
import 'package:intl/intl.dart';
extension DateTimeExtension on DateTime {
String toHourMinuteString() {
return '${hour.toString().padLeft(2, '0')}:'
'${minute.toString().padLeft(2, '0')}';
}
String getDayDateId() => '$year$month$day';
String getWeekdayId() => switch (weekday) {
DateTime.monday => 'monday',
DateTime.tuesday => 'tuesday',
DateTime.wednesday => 'wednesday',
DateTime.thursday => 'thursday',
DateTime.friday => 'friday',
DateTime.saturday => 'saturday',
_ => 'sunday',
};
String toDayMonthYearString() => DateFormat('dd.MM.yyyy').format(this);
DateTime tillDay() {
return DateTime(year, month, day);
}
DateTime toTenthsMinute() {
if (minute % 10 == 0) return this;
return DateTime(year, month, day, hour, minute - minute % 10);
}
}

View File

@@ -0,0 +1,17 @@
extension IntExtensions on int {
String toOrdinal() {
if (this >= 11 && this <= 13) {
return '${this}th';
}
switch (this % 10) {
case 1:
return '${this}st';
case 2:
return '${this}nd';
case 3:
return '${this}rd';
default:
return '${this}th';
}
}
}

View File

@@ -0,0 +1,27 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
class Logger {
static void error(e, {String? message}) {
log(
e.toString(),
error: message ?? e,
level: 2000,
stackTrace: StackTrace.current,
name: 'ERROR',
);
}
static debug(dynamic msg, {String? message}) {
if (kDebugMode) {
log(
msg.toString(),
name: message ?? msg.runtimeType.toString(),
);
}
}
}
const error = Logger.error;
const debug = Logger.debug;

View File

@@ -0,0 +1,19 @@
import 'package:map_launcher/map_launcher.dart';
class MapUtils {
MapUtils._();
static Future<void> openMapByLatLon(double latitude, double longitude) async {
if (await MapLauncher.isMapAvailable(MapType.google) ?? false) {
await MapLauncher.showDirections(
mapType: MapType.google,
destination: Coords(latitude, longitude),
);
} else {
final availableMaps = await MapLauncher.installedMaps;
await availableMaps.first.showDirections(
destination: Coords(latitude, longitude),
);
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1).toLowerCase();
}
}
extension StringToDropdownItem on String {
KwDropDownItem<String>? toDropDownItem() {
if (isEmpty) return null;
return KwDropDownItem(data: this, title: this);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/services.dart';
class DateTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue,) {
if (oldValue.text.length >= newValue.text.length) {
return newValue;
}
String filteredText = newValue.text.replaceAll(RegExp(r'[^0-9.]'), '');
var dateText = _addSeparators(filteredText, '.');
return newValue.copyWith(
text: dateText, selection: updateCursorPosition(dateText));
}
String _addSeparators(String value, String separator) {
value = value.replaceAll('.', '');
var newString = '';
for (int i = 0; i < value.length; i++) {
newString += value[i];
if (i == 1) {
newString += separator;
}
if (i == 3) {
newString += separator;
}
}
return newString;
}
TextSelection updateCursorPosition(String text) {
return TextSelection.fromPosition(TextPosition(offset: text.length));
}
}

View File

@@ -0,0 +1,23 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:intl/intl.dart';
class CertificateDateValidator {
CertificateDateValidator._();
static String? validate(String value) {
try {
final date = DateFormat('MM.dd.yyyy').parse(value);
var splited = value.split('.');
if (int.parse(splited.first) > 12 || int.parse(splited[1]) > 31) {
return 'invalid_date_format'.tr();
}
if (date.isBefore(DateTime.now())) {
return 'date_cannot_be_past'.tr();
}
} catch (e) {
return null;
}
return null;
}
}

View File

@@ -0,0 +1,17 @@
import 'package:easy_localization/easy_localization.dart';
abstract class EmailValidator {
static String? validate(String? email, {bool isRequired = true}) {
// Regular expression for validating emails
final RegExp phoneRegExp = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (isRequired && (email == null || email.isEmpty)) {
return 'email_is_required'.tr();
} else if (email != null && !phoneRegExp.hasMatch(email)) {
return 'invalid_email'.tr();
}
return null;
}
}

View File

@@ -0,0 +1,15 @@
import 'package:easy_localization/easy_localization.dart';
abstract class PhoneValidator {
static String? validate(String? phoneNumber, {bool isRequired = true}) {
// Regular expression for validating phone numbers
final RegExp phoneRegExp = RegExp(r'^\+?[1-9]\d{1,14}$');
if (isRequired && (phoneNumber==null || phoneNumber.isEmpty)) {
return 'phone_number_is_required'.tr();
} else if (phoneNumber !=null && !phoneRegExp.hasMatch(phoneNumber)) {
return 'invalid_phone_number'.tr();
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
import 'package:easy_localization/easy_localization.dart';
class SkillExpValidator {
SkillExpValidator._();
static String? validate(String value) {
if (value.isEmpty) {
return 'experience_is_required'.tr();
}
var parsed = int.tryParse(value);
if (value.isNotEmpty && parsed == null) {
return 'experience_must_be_a_number'.tr();
}
if (value.isNotEmpty && parsed! > 20 || parsed! < 1) {
return 'experience_must_be_between_1_and_20'.tr();
}
return null;
}
}

View File

@@ -0,0 +1,14 @@
abstract class SocialNumberValidator {
static String? validate(String? socialNumber, {bool isRequired = true}) {
// Regular expression for validating US social number
final ssnRegex =
RegExp(r'^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$');
if (isRequired && (socialNumber == null || socialNumber.isEmpty)) {
return 'Social number is required';
} else if (socialNumber != null && !ssnRegex.hasMatch(socialNumber)) {
return 'Invalid social number';
}
return null;
}
}

View File

@@ -0,0 +1,14 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injectable.config.dart';
final getIt = GetIt.instance;
late final String currentEnv;
@InjectableInit()
void configureDependencies(String env) {
currentEnv = env;
GetIt.instance.init(environment: env);
}

View File

@@ -0,0 +1,70 @@
import 'package:auto_route/auto_route.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
class SplashRedirectGuard extends AutoRouteGuard {
final AuthService authService;
SplashRedirectGuard(this.authService);
@override
Future<void> onNavigation(
NavigationResolver resolver,
StackRouter router,
) async {
final status = await authService.getAuthStatus();
switch (status) {
case AuthStatus.authenticated:
router.replace(const HomeRoute());
break;
case AuthStatus.adminValidation:
router.replace(const WaitingValidationRoute());
break;
case AuthStatus.prepareProfile:
//TODO(Heorhii): Some additional adjustment from backend are required before enabling this screen
router.replace(const CheckListFlowRoute());
break;
case AuthStatus.unauthenticated:
router.replace(const AuthFlowRoute());
break;
case AuthStatus.error:
//todo(Artem):show error screen
router.replace(const AuthFlowRoute());
}
resolver.next(false);
}
}
class SingUpRedirectGuard extends AutoRouteGuard {
final AuthService authService;
SingUpRedirectGuard(this.authService);
@override
Future<void> onNavigation(
NavigationResolver resolver, StackRouter router) async {
final status = await authService.getAuthStatus();
switch (status) {
case AuthStatus.authenticated:
router.replace(const HomeRoute());
break;
case AuthStatus.adminValidation:
router.replace(const WaitingValidationRoute());
break;
case AuthStatus.prepareProfile:
resolver.next(true);
return;
case AuthStatus.unauthenticated:
router.replace(const AuthFlowRoute());
break;
case AuthStatus.error:
//todo(Artem):show error screen
router.replace(const AuthFlowRoute());
}
resolver.next(false);
}
}

View File

@@ -0,0 +1,183 @@
import 'package:auto_route/auto_route.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/application/routing/guards.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
RouteType get defaultRouteType => const RouteType.material();
@override
List<AutoRoute> get routes => [
AdaptiveRoute(
path: '/',
page: SplashRoute.page,
initial: true,
guards: [SplashRedirectGuard(getIt<AuthService>())],
),
authFlow,
signUnFlow,
homeBottomBarFlow,
phoneReLoginFlow,
checkListFlow,
AutoRoute(page: WaitingValidationRoute.page),
];
get homeBottomBarFlow => AdaptiveRoute(
path: '/home',
page: HomeRoute.page,
children: [earningsFlow, shiftsFlow, profileFlow]);
get authFlow => AdaptiveRoute(
path: '/auth',
page: AuthFlowRoute.page,
children: [
AutoRoute(path: 'welcome', page: WelcomeRoute.page, initial: true),
AutoRoute(path: 'phone', page: PhoneVerificationRoute.page),
AutoRoute(path: 'code', page: CodeVerificationRoute.page),
],
);
get phoneReLoginFlow => AdaptiveRoute(
path: '/phoneReLogin',
page: PhoneReLoginFlowRoute.page,
children: [
AutoRoute(page: CodeVerificationRoute.page, initial: true),
],
);
get signUnFlow => AdaptiveRoute(
path: '/signup',
page: SignupFlowRoute.page,
guards: [SingUpRedirectGuard(getIt<AuthService>())],
children: [
AutoRoute(page: PersonalInfoRoute.page, initial: true),
AutoRoute(page: EmailVerificationRoute.page),
AutoRoute(page: EmergencyContactsRoute.page),
AutoRoute(page: MobilityRoute.page),
AutoRoute(page: InclusiveRoute.page),
AutoRoute(page: AddressRoute.page),
AutoRoute(page: WorkingAreaRoute.page),
AutoRoute(page: RoleRoute.page),
],
);
get checkListFlow => AdaptiveRoute(
path: '/check_list',
page: CheckListFlowRoute.page,
children: [
AutoRoute(page: CheckListRoute.page, initial: true),
AutoRoute(page: PersonalInfoRoute.page),
AutoRoute(page: EmailVerificationRoute.page),
AutoRoute(page: EmergencyContactsRoute.page),
AutoRoute(page: AddressRoute.page),
AutoRoute(page: WorkingAreaRoute.page),
AutoRoute(page: RoleRoute.page),
AutoRoute(page: CertificatesRoute.page),
AutoRoute(page: ScheduleRoute.page),
bankAccFlow,
wagesFormFlow,
roleKitFlow
],
);
get profileFlow => AdaptiveRoute(
path: 'profile',
page: ProfileMainFlowRoute.page,
children: [
AutoRoute(path: 'menu', page: ProfileMainRoute.page, initial: true),
roleKitFlow,
AutoRoute(path: 'certificates', page: CertificatesRoute.page),
AutoRoute(path: 'schedule', page: ScheduleRoute.page),
AutoRoute(path: 'working_area', page: WorkingAreaRoute.page),
AutoRoute(path: 'live_photo', page: LivePhotoRoute.page),
profileSettingsFlow,
wagesFormFlow,
bankAccFlow,
AutoRoute(path: 'benefits', page: BenefitsRoute.page),
AutoRoute(path: 'support', page: SupportRoute.page),
AutoRoute(path: 'faq', page: FaqRoute.page),
],
);
get bankAccFlow =>
AutoRoute(path: 'bank', page: BankAccountFlowRoute.page, children: [
AutoRoute(path: 'info', page: BankAccountRoute.page, initial: true),
AutoRoute(path: 'edit', page: BankAccountEditRoute.page),
]);
get wagesFormFlow => AutoRoute(
path: 'wages_form',
page: WagesFormsFlowRoute.page,
children: [
AutoRoute(path: 'list', page: FormsListRoute.page, initial: true),
AutoRoute(path: 'offerLatter', page: OfferLetterFormRoute.page),
AutoRoute(path: 'edd', page: EddFormRoute.page),
AutoRoute(path: 'i-9', page: INineFormRoute.page),
AutoRoute(path: 'w-4', page: WFourFormRoute.page),
AutoRoute(path: 'sign', page: FormSignRoute.page),
AutoRoute(path: 'signedForm', page: SignedFormRoute.page),
AutoRoute(path: 'preview', page: FormPreviewRoute.page),
],
);
get profileSettingsFlow => AutoRoute(
path: 'profile_settings',
page: ProfileSettingsFlowRoute.page,
children: [
AutoRoute(
path: 'menu',
page: ProfileSettingsMenuRoute.page,
initial: true,
),
AutoRoute(path: 'role', page: RoleRoute.page),
AutoRoute(path: 'personal', page: PersonalInfoRoute.page),
AutoRoute(
page: EmailVerificationRoute.page,
path: 'emailVerification',
),
AutoRoute(path: 'address', page: AddressRoute.page),
AutoRoute(path: 'emergency', page: EmergencyContactsRoute.page),
AutoRoute(path: 'mobility', page: MobilityRoute.page),
AutoRoute(path: 'inclusive', page: InclusiveRoute.page),
],
);
get roleKitFlow => AutoRoute(
path: 'role_kit',
page: RoleKitFlowRoute.page,
children: [
AutoRoute(path: 'list', page: RolesKitListRoute.page, initial: true),
AutoRoute(path: 'details', page: RoleKitRoute.page),
],
);
get earningsFlow => AdaptiveRoute(
path: 'earnings',
page: EarningsFlowRoute.page,
children: [
AutoRoute(path: 'list', page: EarningsRoute.page, initial: true),
AutoRoute(path: 'history', page: EarningsHistoryRoute.page),
],
);
get shiftsFlow => AdaptiveRoute(
path: 'shifts',
initial: true,
page: ShiftsFlowRoute.page,
children: [
AdaptiveRoute(
path: 'list',
page: ShiftsListMainRoute.page,
initial: true,
),
AdaptiveRoute(path: 'details', page: ShiftDetailsRoute.page),
AdaptiveRoute(path: 'scanner', page: QrScannerRoute.page)
],
);
@override
List<AutoRouteGuard> get guards => [];
}