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,42 @@
import 'package:graphql_flutter/graphql_flutter.dart';
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/features/clock_manual/data/clock_manual_gql.dart';
@singleton
class ClockManualApiProvider {
final ApiClient _client;
ClockManualApiProvider({required ApiClient client}) : _client = client;
Future<void> trackClientClockin(String positionStaffId) async {
final QueryResult result = await _client.mutate(
schema: trackClientClockinMutation,
body: {'position_staff_id': positionStaffId},
);
if (result.hasException) {
throw parseBackendError(result.exception!);
}
}
Future<void> trackClientClockout(String positionStaffId) async {
final QueryResult result = await _client.mutate(
schema: trackClientClockoutMutation,
body: {'position_staff_id': positionStaffId},
);
if (result.hasException) {
throw parseBackendError(result.exception!);
}
}
Future<void> cancelClockin(String positionStaffId) async {
final QueryResult result = await _client.mutate(
schema: cancelClockinMutation,
body: {'position_staff_id': positionStaffId},
);
if (result.hasException) {
throw parseBackendError(result.exception!);
}
}
}

View File

@@ -0,0 +1,23 @@
const String trackClientClockinMutation = r'''
mutation track_client_clockin($position_staff_id: ID!) {
track_client_clockin(position_staff_id: $position_staff_id) {
id
}
}
''';
const String trackClientClockoutMutation = r'''
mutation track_client_clockout($position_staff_id: ID!) {
track_client_clockout(position_staff_id: $position_staff_id) {
id
}
}
''';
const String cancelClockinMutation = r'''
mutation cancel_client_clockin($position_staff_id: ID!) {
cancel_client_clockin(position_staff_id: $position_staff_id) {
id
}
}
''';

View File

@@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/features/clock_manual/data/clock_manual_api_provider.dart';
import 'package:krow/features/clock_manual/domain/clock_manual_repository.dart';
@Singleton(as: ClockManualRepository)
class ClockManualRepositoryImpl implements ClockManualRepository {
final ClockManualApiProvider apiProvider;
ClockManualRepositoryImpl({required this.apiProvider});
@override
Future<void> trackClientClockin(String positionStaffId) async {
try {
await apiProvider.trackClientClockin(positionStaffId);
} catch (exception) {
debugPrint(exception.toString());
rethrow;
}
}
@override
Future<void> trackClientClockout(String positionStaffId) async {
try {
await apiProvider.trackClientClockout(positionStaffId);
} catch (exception) {
debugPrint(exception.toString());
rethrow;
}
}
@override
Future<void> cancelClockin(String positionStaffId) async{
try {
await apiProvider.cancelClockin(positionStaffId);
} catch (exception) {
debugPrint(exception.toString());
rethrow;
}
}
}

View File

@@ -0,0 +1,121 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.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/staff/pivot.dart';
import 'package:krow/core/entity/staff_contact_entity.dart';
import 'package:krow/features/clock_manual/domain/clock_manual_repository.dart';
part 'clock_manual_event.dart';
part 'clock_manual_state.dart';
class ClockManualBloc extends Bloc<ClockManualEvent, ClockManualState> {
var staffContacts = <StaffContact>[];
ClockManualBloc({required this.staffContacts})
: super(ClockManualState(
timers: Map.fromEntries(
staffContacts.map((e) => MapEntry(e.id, ValueNotifier<int>(0)))),
ongoingStaffContacts: staffContacts
.where((element) => element.status == PivotStatus.ongoing)
.toList()
..sort((a, b) => a.startAt.compareTo(b.startAt)),
confirmedStaffContacts: staffContacts
.where((element) => element.status == PivotStatus.confirmed)
.toList()
..sort((a, b) => a.startAt.compareTo(b.startAt)),
)) {
on<ChangeSelectedTabIndexClockManual>(_onChangeSelectedTabIndexStaffManual);
on<ClockInManual>(_onClockInStaffManual);
on<SortStaffContactsClockManual>(_sortStaffContactsClockManual);
on<ClockOutManual>(_onClockOutStaff);
on<CancelClockInManual>(_onCancelClockInStaffManual);
}
void _onChangeSelectedTabIndexStaffManual(
ChangeSelectedTabIndexClockManual event, Emitter<ClockManualState> emit) {
emit(state.copyWith(selectedTabIndex: event.index));
}
void _onClockInStaffManual(ClockInManual event, emit) async {
emit(state.copyWith(inLoading: true));
try {
await getIt<ClockManualRepository>()
.trackClientClockin(event.staffContact.id);
_startCountdownTimer(
state.timers[event.staffContact.id]!, event.staffContact);
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(
errorMessage: e.message,
));
}
}
emit(state.copyWith(inLoading: false));
}
void _onClockOutStaff(ClockOutManual event, emit) async {
emit(state.copyWith(inLoading: false));
try {
await getIt<ClockManualRepository>()
.trackClientClockout(event.staffContact.id);
event.staffContact.status = PivotStatus.completed;
add(SortStaffContactsClockManual());
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(
errorMessage: e.message,
));
}
}
emit(state.copyWith(inLoading: false));
}
void _onCancelClockInStaffManual(CancelClockInManual event, emit) async {
emit(state.copyWith(inLoading: true));
try {
await getIt<ClockManualRepository>().cancelClockin(event.staffContact.id);
state.timers[event.staffContact.id]!.value = -1;
event.staffContact.status = PivotStatus.confirmed;
add(SortStaffContactsClockManual());
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(
errorMessage: e.message,
));
}
}
emit(state.copyWith(inLoading: false));
}
void _sortStaffContactsClockManual(SortStaffContactsClockManual event, emit) {
emit(state.copyWith(
ongoingStaffContacts: staffContacts
.where((element) => element.status == PivotStatus.ongoing)
.toList()
..sort((a, b) => a.startAt.compareTo(b.startAt)),
confirmedStaffContacts: staffContacts
.where((element) => element.status == PivotStatus.confirmed)
.toList()
..sort((a, b) => a.startAt.compareTo(b.startAt)),
));
}
void _startCountdownTimer(ValueNotifier<int> cancelTimer, staffContact) {
cancelTimer.value = 5;
Timer.periodic(const Duration(seconds: 1), (timer) {
if (cancelTimer.value > 0) {
cancelTimer.value -= 1;
} else if (cancelTimer.value == 0) {
staffContact.status = PivotStatus.ongoing;
add(SortStaffContactsClockManual());
timer.cancel();
} else {
timer.cancel();
}
});
}
}

View File

@@ -0,0 +1,30 @@
part of 'clock_manual_bloc.dart';
@immutable
sealed class ClockManualEvent {}
class ChangeSelectedTabIndexClockManual extends ClockManualEvent {
final int index;
ChangeSelectedTabIndexClockManual(this.index);
}
class ClockInManual extends ClockManualEvent {
final StaffContact staffContact;
ClockInManual(this.staffContact);
}
class SortStaffContactsClockManual extends ClockManualEvent {}
class ClockOutManual extends ClockManualEvent {
final StaffContact staffContact;
ClockOutManual(this.staffContact);
}
class CancelClockInManual extends ClockManualEvent {
final StaffContact staffContact;
CancelClockInManual(this.staffContact);
}

View File

@@ -0,0 +1,39 @@
part of 'clock_manual_bloc.dart';
@immutable
class ClockManualState {
final List<StaffContact> confirmedStaffContacts;
final List<StaffContact> ongoingStaffContacts;
final int selectedTabIndex;
final bool inLoading;
final String? errorMessage;
final Map<String, ValueNotifier<int>> timers ;
const ClockManualState(
{required this.confirmedStaffContacts,
required this.ongoingStaffContacts,
this.inLoading = false,
this.errorMessage,
this.timers = const {},
this.selectedTabIndex = 0});
copyWith({
List<StaffContact>? confirmedStaffContacts,
List<StaffContact>? ongoingStaffContacts,
bool? inLoading,
String? errorMessage,
int? selectedTabIndex,
Map<String, ValueNotifier<int>>? timers,
}) {
return ClockManualState(
timers: timers ?? this.timers,
confirmedStaffContacts:
confirmedStaffContacts ?? this.confirmedStaffContacts,
ongoingStaffContacts: ongoingStaffContacts ?? this.ongoingStaffContacts,
inLoading: inLoading ?? this.inLoading,
errorMessage: errorMessage ?? this.errorMessage,
selectedTabIndex: selectedTabIndex ?? this.selectedTabIndex,
);
}
}

View File

@@ -0,0 +1,7 @@
abstract class ClockManualRepository {
Future<void> trackClientClockin(String positionStaffId);
Future<void> trackClientClockout(String positionStaffId);
Future<void> cancelClockin(String positionStaffId);
}

View File

@@ -0,0 +1,180 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/application/common/str_extensions.dart';
import 'package:krow/core/data/models/staff/pivot.dart';
import 'package:krow/core/entity/staff_contact_entity.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/clock_manual/domain/bloc/clock_manual_bloc.dart';
class AssignedStaffManualItemWidget extends StatelessWidget {
final StaffContact staffContact;
final ValueNotifier<int> timer;
const AssignedStaffManualItemWidget(
{super.key, required this.staffContact, required this.timer});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: timer,
builder: (BuildContext context, int value, Widget? child) {
return Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 8),
decoration: KwBoxDecorations.white8,
child: Column(
children: [
_staffInfo(),
const Gap(12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(staffContact.status == PivotStatus.ongoing?'End Time': 'Start Time',
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray)),
Text(
DateFormat('hh:mm a').format(
(staffContact.status == PivotStatus.ongoing
? staffContact.parentPosition?.endTime
: staffContact.parentPosition?.startTime) ??
DateTime.now()),
style: AppTextStyles.bodySmallMed,
),
],
),
const Gap(12),
if (staffContact.status == PivotStatus.confirmed && value <= 0)
_buildClockIn(context),
if (staffContact.status == PivotStatus.ongoing)
_buildClockOut(context),
if (value > 0) _buildCancel(context),
if (kDebugMode) _buildCancel(context),
],
),
);
},
);
}
KwButton _buildCancel(BuildContext context) {
return KwButton.outlinedPrimary(
onPressed: () {
BlocProvider.of<ClockManualBloc>(context)
.add(CancelClockInManual(staffContact));
},
label: 'Click again to cancel (${timer.value}) ',
height: 36)
.copyWith(
textColors: AppColors.blackGray,
color: AppColors.grayWhite,
isFilledOutlined: true,
borderColor: AppColors.grayTintStroke,
pressedColor: AppColors.grayWhite);
}
KwButton _buildClockIn(BuildContext context) {
return KwButton.outlinedPrimary(
onPressed: () {
BlocProvider.of<ClockManualBloc>(context)
.add(ClockInManual(staffContact));
},
label: 'Clock In',
height: 36)
.copyWith(
textColors: AppColors.statusSuccess,
color: AppColors.tintGreen,
isFilledOutlined: true,
borderColor: AppColors.tintDarkGreen,
pressedColor: AppColors.tintDarkGreen);
}
KwButton _buildClockOut(BuildContext context) {
return KwButton.outlinedPrimary(
onPressed: () {
BlocProvider.of<ClockManualBloc>(context)
.add(ClockOutManual(staffContact));
},
label: 'Clock Out',
height: 36)
.copyWith(
textColors: AppColors.statusError,
color: AppColors.tintRed,
isFilledOutlined: true,
borderColor: AppColors.tintDarkRed,
pressedColor: AppColors.tintDarkRed);
}
Row _staffInfo() {
return Row(
children: [
if ((staffContact.photoUrl ?? '').isNotEmpty)
CircleAvatar(
radius: 18,
backgroundImage: NetworkImage(staffContact.photoUrl ?? ''),
),
const Gap(12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${staffContact.firstName} ${staffContact.lastName}',
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyMediumMed,
),
const Gap(4),
Text(
staffContact.phoneNumber ?? '',
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray),
),
],
),
),
const Gap(8),
SizedBox(
height: 40,
child: Align(
alignment: Alignment.topCenter,
child: Container(
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
border: Border.all(
color: staffContact.status.getStatusBorderColor(),
),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
width: 6,
height: 6,
padding: const EdgeInsets.all(0),
decoration: BoxDecoration(
color: staffContact.status.getStatusTextColor(),
borderRadius: BorderRadius.circular(3),
),
),
const Gap(2),
Text(
staffContact.status.formattedName.capitalize(),
style: AppTextStyles.bodyTinyMed.copyWith(
color: staffContact.status.getStatusTextColor()),
)
],
),
),
),
)
],
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/entity/staff_contact_entity.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
import 'package:krow/features/clock_manual/domain/bloc/clock_manual_bloc.dart';
import 'package:krow/features/clock_manual/presentation/clock_manual_list_item.dart';
@RoutePage()
class ClockManualScreen extends StatelessWidget implements AutoRouteWrapper {
final List<StaffContact> staffContacts;
const ClockManualScreen({
super.key,
required this.staffContacts,
});
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider(
create: (context) => ClockManualBloc(staffContacts: staffContacts),
child: this);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<ClockManualBloc, ClockManualState>(
listenWhen: (previous, current) =>
previous.errorMessage != current.errorMessage,
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.errorMessage ?? ''),
));
}
},
builder: (context, state) {
var items = state.selectedTabIndex == 0
? state.confirmedStaffContacts
: state.ongoingStaffContacts;
return Scaffold(
appBar: KwAppBar(
titleText: 'Assigned Staff',
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: KwOptionSelector(
selectedIndex: state.selectedTabIndex,
onChanged: (index) {
BlocProvider.of<ClockManualBloc>(context)
.add(ChangeSelectedTabIndexClockManual(index));
},
height: 26,
selectorHeight: 4,
textStyle: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
selectedTextStyle: AppTextStyles.bodyMediumMed,
itemAlign: Alignment.topCenter,
items: const [
'Clock In',
'Clock Out',
]),
),
ListView.builder(
primary: false,
shrinkWrap: true,
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, index) {
return AssignedStaffManualItemWidget(
staffContact: items[index],
timer: state.timers[items[index].id]!,
);
},
),
],
),
),
);
},
);
}
}