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
export 'src/entities/shifts/shift.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
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:krow_domain/src/entities/shifts/break/break.dart';
class Shift extends Equatable {
final String id;
@@ -29,6 +30,7 @@ class Shift extends Equatable {
final String? roleId;
final bool? hasApplied;
final double? totalValue;
final Break? breakInfo;
const Shift({
required this.id,
@@ -59,48 +61,49 @@ class Shift extends Equatable {
this.roleId,
this.hasApplied,
this.totalValue,
this.breakInfo,
});
@override
List<Object?> get props => [
id,
title,
clientName,
logoUrl,
hourlyRate,
location,
locationAddress,
date,
startTime,
endTime,
createdDate,
tipsAvailable,
travelTime,
mealProvided,
parkingAvailable,
gasCompensation,
description,
instructions,
managers,
latitude,
longitude,
status,
durationDays,
requiredSlots,
filledSlots,
roleId,
hasApplied,
totalValue,
];
List<Object?> get props => <Object?>[
id,
title,
clientName,
logoUrl,
hourlyRate,
location,
locationAddress,
date,
startTime,
endTime,
createdDate,
tipsAvailable,
travelTime,
mealProvided,
parkingAvailable,
gasCompensation,
description,
instructions,
managers,
latitude,
longitude,
status,
durationDays,
requiredSlots,
filledSlots,
roleId,
hasApplied,
totalValue,
breakInfo,
];
}
class ShiftManager extends Equatable {
const ShiftManager({required this.name, required this.phone, this.avatar});
final String name;
final String phone;
final String? avatar;
const ShiftManager({required this.name, required this.phone, this.avatar});
@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,
filledSlots: app.shiftRole.assigned ?? 0,
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,
filledSlots: app.shiftRole.assigned ?? 0,
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,
requiredSlots: sr.count,
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,
hasApplied: hasApplied,
totalValue: sr.totalValue,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
),
);
}
@@ -360,6 +376,7 @@ class ShiftsRepositoryImpl
int? required;
int? filled;
Break? breakInfo;
try {
final rolesRes = await executeProtected(() =>
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute());
@@ -370,6 +387,12 @@ class ShiftsRepositoryImpl
required = (required ?? 0) + r.count;
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 (_) {}
@@ -394,6 +417,7 @@ class ShiftsRepositoryImpl
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
breakInfo: breakInfo,
);
}

View File

@@ -132,7 +132,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
return BlocProvider<ShiftDetailsBloc>(
create: (_) => Modular.get<ShiftDetailsBloc>()
..add(
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift?.roleId),
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId),
),
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
listener: (context, state) {
@@ -148,7 +148,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
);
Modular.to.toShifts(selectedDate: state.shiftDate);
} else if (state is ShiftDetailsError) {
if (_isApplying || widget.shift == null) {
if (_isApplying) {
UiSnackbar.show(
context,
message: translateErrorKey(state.message),
@@ -240,7 +240,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
const Divider(height: 1, thickness: 0.5),
// Date Section
// Date & Time Section
Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
@@ -248,8 +248,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
children: [
Text(
i18n.shift_date,
style: UiTypography
.titleUppercase4b
style: UiTypography.titleUppercase4b
.textSecondary,
),
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),
// 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)
Padding(
padding: const EdgeInsets.all(UiConstants.space5),
@@ -344,7 +337,6 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
.titleUppercase4b
.textSecondary,
),
const SizedBox(height: UiConstants.space3),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
@@ -366,12 +358,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
).showSnackBar(
SnackBar(
content: Text(
displayShift!
.locationAddress
displayShift.locationAddress
.isNotEmpty
? displayShift!
.locationAddress
: displayShift!.location,
? displayShift.locationAddress
: displayShift.location,
),
duration: const Duration(
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) {
if (_actionDialogOpen) return;
_actionDialogOpen = true;

View File

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