feat(breaks): Implement break functionality with Break entity and adapter
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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,10 +61,11 @@ class Shift extends Equatable {
|
||||
this.roleId,
|
||||
this.hasApplied,
|
||||
this.totalValue,
|
||||
this.breakInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
title,
|
||||
clientName,
|
||||
@@ -91,16 +94,16 @@ class Shift extends Equatable {
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user