feat(breaks): Implement break functionality with Break entity and adapter

This commit is contained in:
Achintha Isuru
2026-02-16 13:26:04 -05:00
parent 2a0b39926a
commit 9b6cad3bde
7 changed files with 180 additions and 100 deletions

View File

@@ -30,6 +30,8 @@ export 'src/entities/events/work_session.dart';
// Shifts // Shifts
export 'src/entities/shifts/shift.dart'; export 'src/entities/shifts/shift.dart';
export 'src/adapters/shifts/shift_adapter.dart'; export 'src/adapters/shifts/shift_adapter.dart';
export 'src/entities/shifts/break/break.dart';
export 'src/adapters/shifts/break/break_adapter.dart';
// Orders & Requests // Orders & Requests
export 'src/entities/orders/order_type.dart'; export 'src/entities/orders/order_type.dart';

View File

@@ -0,0 +1,39 @@
import '../../../entities/shifts/break/break.dart';
/// Adapter for Break related data.
class BreakAdapter {
/// Maps break data to a Break entity.
///
/// [isPaid] whether the break is paid.
/// [breakTime] the string representation of the break duration (e.g., 'MIN_10', 'MIN_30').
static Break fromData({
required bool isPaid,
required String? breakTime,
}) {
return Break(
isBreakPaid: isPaid,
duration: _parseDuration(breakTime),
);
}
static BreakDuration _parseDuration(String? breakTime) {
if (breakTime == null) return BreakDuration.none;
switch (breakTime.toUpperCase()) {
case 'MIN_10':
return BreakDuration.ten;
case 'MIN_15':
return BreakDuration.fifteen;
case 'MIN_20':
return BreakDuration.twenty;
case 'MIN_30':
return BreakDuration.thirty;
case 'MIN_45':
return BreakDuration.fortyFive;
case 'MIN_60':
return BreakDuration.sixty;
default:
return BreakDuration.none;
}
}
}

View File

@@ -0,0 +1,47 @@
import 'package:equatable/equatable.dart';
/// Enum representing common break durations in minutes.
enum BreakDuration {
/// No break.
none(0),
/// 10 minutes break.
ten(10),
/// 15 minutes break.
fifteen(15),
/// 20 minutes break.
twenty(20),
/// 30 minutes break.
thirty(30),
/// 45 minutes break.
fortyFive(45),
/// 60 minutes break.
sixty(60);
/// The duration in minutes.
final int minutes;
const BreakDuration(this.minutes);
}
/// Represents a break configuration for a shift.
class Break extends Equatable {
const Break({
required this.duration,
required this.isBreakPaid,
});
/// The duration of the break.
final BreakDuration duration;
/// Whether the break is paid or unpaid.
final bool isBreakPaid;
@override
List<Object?> get props => <Object?>[duration, isBreakPaid];
}

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/src/entities/shifts/break/break.dart';
class Shift extends Equatable { class Shift extends Equatable {
final String id; final String id;
@@ -29,6 +30,7 @@ class Shift extends Equatable {
final String? roleId; final String? roleId;
final bool? hasApplied; final bool? hasApplied;
final double? totalValue; final double? totalValue;
final Break? breakInfo;
const Shift({ const Shift({
required this.id, required this.id,
@@ -59,48 +61,49 @@ class Shift extends Equatable {
this.roleId, this.roleId,
this.hasApplied, this.hasApplied,
this.totalValue, this.totalValue,
this.breakInfo,
}); });
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
id, id,
title, title,
clientName, clientName,
logoUrl, logoUrl,
hourlyRate, hourlyRate,
location, location,
locationAddress, locationAddress,
date, date,
startTime, startTime,
endTime, endTime,
createdDate, createdDate,
tipsAvailable, tipsAvailable,
travelTime, travelTime,
mealProvided, mealProvided,
parkingAvailable, parkingAvailable,
gasCompensation, gasCompensation,
description, description,
instructions, instructions,
managers, managers,
latitude, latitude,
longitude, longitude,
status, status,
durationDays, durationDays,
requiredSlots, requiredSlots,
filledSlots, filledSlots,
roleId, roleId,
hasApplied, hasApplied,
totalValue, totalValue,
]; breakInfo,
];
} }
class ShiftManager extends Equatable { class ShiftManager extends Equatable {
const ShiftManager({required this.name, required this.phone, this.avatar});
final String name; final String name;
final String phone; final String phone;
final String? avatar; final String? avatar;
const ShiftManager({required this.name, required this.phone, this.avatar});
@override @override
List<Object?> get props => [name, phone, avatar]; List<Object?> get props => <Object?>[name, phone, avatar];
} }

View File

@@ -141,6 +141,10 @@ class ShiftsRepositoryImpl
requiredSlots: app.shiftRole.count, requiredSlots: app.shiftRole.count,
filledSlots: app.shiftRole.assigned ?? 0, filledSlots: app.shiftRole.assigned ?? 0,
hasApplied: true, hasApplied: true,
breakInfo: BreakAdapter.fromData(
isPaid: app.shiftRole.isBreakPaid ?? false,
breakTime: app.shiftRole.breakType?.stringValue,
),
), ),
); );
} }
@@ -208,6 +212,10 @@ class ShiftsRepositoryImpl
requiredSlots: app.shiftRole.count, requiredSlots: app.shiftRole.count,
filledSlots: app.shiftRole.assigned ?? 0, filledSlots: app.shiftRole.assigned ?? 0,
hasApplied: true, hasApplied: true,
breakInfo: BreakAdapter.fromData(
isPaid: app.shiftRole.isBreakPaid ?? false,
breakTime: app.shiftRole.breakType?.stringValue,
),
), ),
); );
} }
@@ -277,6 +285,10 @@ class ShiftsRepositoryImpl
durationDays: sr.shift.durationDays, durationDays: sr.shift.durationDays,
requiredSlots: sr.count, requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0, filledSlots: sr.assigned ?? 0,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
),
), ),
); );
} }
@@ -350,6 +362,10 @@ class ShiftsRepositoryImpl
filledSlots: sr.assigned ?? 0, filledSlots: sr.assigned ?? 0,
hasApplied: hasApplied, hasApplied: hasApplied,
totalValue: sr.totalValue, totalValue: sr.totalValue,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
),
); );
} }
@@ -360,6 +376,7 @@ class ShiftsRepositoryImpl
int? required; int? required;
int? filled; int? filled;
Break? breakInfo;
try { try {
final rolesRes = await executeProtected(() => final rolesRes = await executeProtected(() =>
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute());
@@ -370,6 +387,12 @@ class ShiftsRepositoryImpl
required = (required ?? 0) + r.count; required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0); filled = (filled ?? 0) + (r.assigned ?? 0);
} }
// Use the first role's break info as a representative
final firstRole = rolesRes.data.shiftRoles.first;
breakInfo = BreakAdapter.fromData(
isPaid: firstRole.isBreakPaid ?? false,
breakTime: firstRole.breakType?.stringValue,
);
} }
} catch (_) {} } catch (_) {}
@@ -394,6 +417,7 @@ class ShiftsRepositoryImpl
durationDays: s.durationDays, durationDays: s.durationDays,
requiredSlots: required, requiredSlots: required,
filledSlots: filled, filledSlots: filled,
breakInfo: breakInfo,
); );
} }

View File

@@ -132,7 +132,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
return BlocProvider<ShiftDetailsBloc>( return BlocProvider<ShiftDetailsBloc>(
create: (_) => Modular.get<ShiftDetailsBloc>() create: (_) => Modular.get<ShiftDetailsBloc>()
..add( ..add(
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift?.roleId), LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId),
), ),
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>( child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
listener: (context, state) { listener: (context, state) {
@@ -148,7 +148,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
); );
Modular.to.toShifts(selectedDate: state.shiftDate); Modular.to.toShifts(selectedDate: state.shiftDate);
} else if (state is ShiftDetailsError) { } else if (state is ShiftDetailsError) {
if (_isApplying || widget.shift == null) { if (_isApplying) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: translateErrorKey(state.message), message: translateErrorKey(state.message),
@@ -240,7 +240,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
const Divider(height: 1, thickness: 0.5), const Divider(height: 1, thickness: 0.5),
// Date Section // Date & Time Section
Padding( Padding(
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
child: Column( child: Column(
@@ -248,8 +248,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
children: [ children: [
Text( Text(
i18n.shift_date, i18n.shift_date,
style: UiTypography style: UiTypography.titleUppercase4b
.titleUppercase4b
.textSecondary, .textSecondary,
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
@@ -268,6 +267,24 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
], ],
), ),
const SizedBox(height: UiConstants.space4),
Row(
children: [
Expanded(
child: _buildTimeBox(
"CLOCK IN TIME",
displayShift.startTime,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildTimeBox(
"CLOCK OUT TIME",
displayShift.endTime,
),
),
],
),
], ],
), ),
), ),
@@ -308,30 +325,6 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
const Divider(height: 1, thickness: 0.5), const Divider(height: 1, thickness: 0.5),
// Time Section (New)
Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Row(
children: [
Expanded(
child: _buildTimeBox(
"CLOCK IN TIME",
displayShift.startTime,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildTimeBox(
"CLOCK OUT TIME",
displayShift.endTime,
),
),
],
),
),
const Divider(height: 1, thickness: 0.5),
// Location Section (New with Map) // Location Section (New with Map)
Padding( Padding(
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
@@ -344,7 +337,6 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
.titleUppercase4b .titleUppercase4b
.textSecondary, .textSecondary,
), ),
const SizedBox(height: UiConstants.space3),
Row( Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
@@ -366,12 +358,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
).showSnackBar( ).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
displayShift! displayShift.locationAddress
.locationAddress
.isNotEmpty .isNotEmpty
? displayShift! ? displayShift.locationAddress
.locationAddress : displayShift.location,
: displayShift!.location,
), ),
duration: const Duration( duration: const Duration(
seconds: 3, seconds: 3,
@@ -509,36 +499,6 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
); );
} }
void _declineShift(BuildContext context, String id) {
final i18n = Translations.of(
context,
).staff_shifts.shift_details.decline_dialog;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(i18n.title),
content: Text(i18n.message),
actions: [
TextButton(
onPressed: () => Modular.to.pop(),
child: Text(Translations.of(context).common.cancel),
),
TextButton(
onPressed: () {
BlocProvider.of<ShiftDetailsBloc>(
context,
).add(DeclineShiftDetailsEvent(id));
},
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
child: Text(
Translations.of(context).staff_shifts.shift_details.decline,
),
),
],
),
);
}
void _showApplyingDialog(BuildContext context, Shift shift) { void _showApplyingDialog(BuildContext context, Shift shift) {
if (_actionDialogOpen) return; if (_actionDialogOpen) return;
_actionDialogOpen = true; _actionDialogOpen = true;

View File

@@ -52,6 +52,8 @@ query listApplications @auth(level: USER) {
startTime startTime
endTime endTime
hours hours
breakType
isBreakPaid
totalValue totalValue
role { role {
id id
@@ -341,6 +343,8 @@ query getApplicationsByStaffId(
startTime startTime
endTime endTime
hours hours
breakType
isBreakPaid
totalValue totalValue
role { role {
id id
@@ -352,7 +356,6 @@ query getApplicationsByStaffId(
} }
} }
query vaidateDayStaffApplication( query vaidateDayStaffApplication(
$staffId: UUID! $staffId: UUID!
$offset: Int $offset: Int
@@ -692,6 +695,8 @@ query listCompletedApplicationsByStaffId(
startTime startTime
endTime endTime
hours hours
breakType
isBreakPaid
totalValue totalValue
role { role {