feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/skill.dart';
|
||||
|
||||
part 'business_skill.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class BusinessSkill {
|
||||
Skill? skill;
|
||||
|
||||
BusinessSkill({this.skill});
|
||||
|
||||
factory BusinessSkill.fromJson(Map<String, dynamic> json) {
|
||||
return _$BusinessSkillFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$BusinessSkillToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'cancellation_reason.g.dart';
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
enum CancellationReason {
|
||||
sickLeave,
|
||||
vacation,
|
||||
other,
|
||||
health,
|
||||
transportation,
|
||||
personal,
|
||||
scheduleConflict,
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class CancellationReasonModel {
|
||||
final String type;
|
||||
final CancellationReason? reason;
|
||||
final String? details;
|
||||
|
||||
CancellationReasonModel({
|
||||
required this.type,
|
||||
required this.reason,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
factory CancellationReasonModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$CancellationReasonModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$CancellationReasonModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/features/shifts/data/models/event_tag.dart';
|
||||
|
||||
part 'event.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Event {
|
||||
String? id;
|
||||
Business? business;
|
||||
String? name;
|
||||
String? date;
|
||||
String? additionalInfo;
|
||||
List<Addon>? addons;
|
||||
List<EventTag>? tags;
|
||||
|
||||
|
||||
Event({
|
||||
this.business,
|
||||
this.name,
|
||||
this.date,
|
||||
this.additionalInfo,
|
||||
this.addons,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
factory Event.fromJson(Map<String, dynamic> json) {
|
||||
return _$EventFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$EventToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Business {
|
||||
String? name;
|
||||
String? avatar;
|
||||
|
||||
Business({this.name, this.avatar});
|
||||
|
||||
factory Business.fromJson(Map<String, dynamic> json) {
|
||||
return _$BusinessFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$BusinessToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Addon {
|
||||
String? name;
|
||||
|
||||
Addon({this.name});
|
||||
|
||||
factory Addon.fromJson(Map<String, dynamic> json) {
|
||||
return Addon(
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/staff/full_address_model.dart';
|
||||
import 'package:krow/features/shifts/data/models/event.dart';
|
||||
import 'package:krow/features/shifts/data/models/shift_contact.dart';
|
||||
|
||||
part 'event_shift.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Shift {
|
||||
String? id;
|
||||
String? name;
|
||||
String? address;
|
||||
FullAddress? fullAddress;
|
||||
List<Contact>? contacts;
|
||||
Event? event;
|
||||
|
||||
Shift({
|
||||
this.id,
|
||||
this.name,
|
||||
this.address,
|
||||
this.event,
|
||||
this.contacts,
|
||||
this.fullAddress,
|
||||
});
|
||||
|
||||
factory Shift.fromJson(Map<String, dynamic> json) {
|
||||
return _$ShiftFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$ShiftToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'event_tag.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class EventTag{
|
||||
final String id;
|
||||
final String name;
|
||||
final String? slug;
|
||||
|
||||
EventTag({required this.name, required this.id, required this.slug});
|
||||
|
||||
factory EventTag.fromJson(Map<String, dynamic> json) {
|
||||
return _$EventTagFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$EventTagToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/features/shifts/data/models/business_skill.dart';
|
||||
import 'package:krow/features/shifts/data/models/event_shift.dart';
|
||||
|
||||
part 'position.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Position {
|
||||
String? id;
|
||||
String? startTime;
|
||||
String? endTime;
|
||||
Shift? shift;
|
||||
BusinessSkill? businessSkill;
|
||||
double? rate;
|
||||
@JsonKey(name: 'break')
|
||||
int? breakMinutes;
|
||||
|
||||
Position({
|
||||
this.id,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.shift,
|
||||
this.businessSkill,
|
||||
});
|
||||
|
||||
factory Position.fromJson(Map<String, dynamic> json) =>
|
||||
_$PositionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PositionToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'shift_contact.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Contact {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String? avatar;
|
||||
final String lastName;
|
||||
final String title;
|
||||
final AuthInfo authInfo;
|
||||
|
||||
Contact({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.title,
|
||||
required this.avatar,
|
||||
required this.authInfo,
|
||||
});
|
||||
|
||||
factory Contact.fromJson(Map<String, dynamic> json) {
|
||||
return _$ContactFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$ContactToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class AuthInfo {
|
||||
final String email;
|
||||
final String phone;
|
||||
|
||||
AuthInfo({
|
||||
required this.email,
|
||||
required this.phone,
|
||||
});
|
||||
|
||||
factory AuthInfo.fromJson(Map<String, dynamic> json) {
|
||||
return _$AuthInfoFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$AuthInfoToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/features/shifts/data/models/cancellation_reason.dart';
|
||||
import 'package:krow/features/shifts/data/models/position.dart';
|
||||
|
||||
part 'staff_shift.g.dart';
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
enum EventShiftRoleStaffStatus {
|
||||
assigned,
|
||||
confirmed,
|
||||
ongoing,
|
||||
completed,
|
||||
canceledByStaff,
|
||||
canceledByBusiness,
|
||||
canceledByAdmin,
|
||||
requestedReplace,
|
||||
declineByStaff,
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class StaffShift {
|
||||
String id;
|
||||
DateTime? statusUpdatedAt;
|
||||
EventShiftRoleStaffStatus status;
|
||||
Position? position;
|
||||
DateTime? startAt;
|
||||
DateTime? endAt;
|
||||
DateTime? clockIn;
|
||||
DateTime? clockOut;
|
||||
DateTime? breakIn;
|
||||
DateTime? breakOut;
|
||||
List<CancellationReasonModel>? cancelReason;
|
||||
StaffRating? rating;
|
||||
|
||||
|
||||
// Staff? staff;
|
||||
|
||||
StaffShift({
|
||||
required this.id,
|
||||
required this.status,
|
||||
this.statusUpdatedAt,
|
||||
this.position,
|
||||
this.startAt,
|
||||
this.endAt,
|
||||
this.clockIn,
|
||||
this.clockOut,
|
||||
this.breakIn,
|
||||
this.breakOut,
|
||||
this.rating,
|
||||
// this.staff
|
||||
});
|
||||
|
||||
factory StaffShift.fromJson(Map<String, dynamic> json) {
|
||||
return _$StaffShiftFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$StaffShiftToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class StaffRating{
|
||||
final String id;
|
||||
final double rating;
|
||||
|
||||
factory StaffRating.fromJson(Map<String, dynamic> json) {
|
||||
return _$StaffRatingFromJson(json);
|
||||
}
|
||||
|
||||
StaffRating({required this.id, required this.rating});
|
||||
|
||||
Map<String, dynamic> toJson() => _$StaffRatingToJson(this);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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/core/data/models/pagination_wrapper/pagination_wrapper.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/data/shifts_gql.dart';
|
||||
|
||||
@Injectable()
|
||||
class ShiftsApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
ShiftsApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<PaginationWrapper> fetchShifts(String status, {String? after}) async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: getShiftsQuery,
|
||||
body: {'status': status, 'first': 100, 'after': after},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
return PaginationWrapper.fromJson(
|
||||
result.data!['staff_shifts'],
|
||||
(json) => StaffShift.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PaginationWrapper> getMissBreakFinishedShift() async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: staffNoBreakShifts,
|
||||
body: {'first': 100},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
return PaginationWrapper.fromJson(
|
||||
result.data!['staff_no_break_shifts'],
|
||||
(json) => StaffShift.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> confirmShift(String id) async {
|
||||
final QueryResult result =
|
||||
await _client.mutate(schema: acceptShiftMutation, body: {'id': id});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clockInShift(String id) async {
|
||||
final QueryResult result =
|
||||
await _client.mutate(schema: trackStaffClockin, body: {'id': id});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> completeShift(String id,
|
||||
{String? breakIn, String? breakOut, bool isPast = false}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: isPast ? trackStaffBreak : completeShiftMutation,
|
||||
body: {'id': id, 'break_in': breakIn, 'break_out': breakOut});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> completeShiftNoBreak(String id,
|
||||
{String? reason, String? additionalReason}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: submitNoBreakStaffShiftMutation,
|
||||
body: {'id': id, 'reason': reason, 'details': additionalReason});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> declineShift(String id,
|
||||
{String? reason, String? additionalReason}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: declineStaffShiftMutation,
|
||||
body: {
|
||||
'position_id': id,
|
||||
'reason': reason,
|
||||
'details': additionalReason
|
||||
});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelShift(String id,
|
||||
{String? reason, String? additionalReason}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: cancelStaffShiftMutation,
|
||||
body: {
|
||||
'position_id': id,
|
||||
'reason': reason,
|
||||
'details': additionalReason
|
||||
});
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<StaffShift> getShiftById(String id) async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: getShiftPositionQuery,
|
||||
body: {'id': id},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
if (result.data == null || result.data!['staff_shift'] == null) {
|
||||
throw Exception('No data found');
|
||||
}
|
||||
return StaffShift.fromJson(result.data!['staff_shift']);
|
||||
}
|
||||
}
|
||||
207
mobile-apps/staff-app/lib/features/shifts/data/shifts_gql.dart
Normal file
207
mobile-apps/staff-app/lib/features/shifts/data/shifts_gql.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
import 'package:krow/core/application/clients/api/gql.dart';
|
||||
|
||||
const String _shiftFields = '''
|
||||
id
|
||||
status
|
||||
status_updated_at
|
||||
start_at
|
||||
end_at
|
||||
clock_in
|
||||
clock_out
|
||||
break_in
|
||||
break_out
|
||||
...position
|
||||
cancel_reason {
|
||||
type
|
||||
reason
|
||||
details
|
||||
}
|
||||
rating {
|
||||
id
|
||||
rating
|
||||
}
|
||||
''';
|
||||
|
||||
const String getShiftsQuery = '''
|
||||
$_positionFragment
|
||||
|
||||
query GetShifts (\$status: EventShiftPositionStaffStatusInput!, \$first: Int!, \$after: String) {
|
||||
staff_shifts(status: \$status, first: \$first, after: \$after) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
$_shiftFields
|
||||
}
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
''';
|
||||
|
||||
const String staffNoBreakShifts = '''
|
||||
$_positionFragment
|
||||
|
||||
query staffNoBreakShifts (\$first: Int!) {
|
||||
staff_no_break_shifts(first: \$first) {
|
||||
pageInfo{
|
||||
}
|
||||
edges {
|
||||
|
||||
node {
|
||||
$_shiftFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
''';
|
||||
|
||||
const String getShiftPositionQuery = '''
|
||||
$_positionFragment
|
||||
query GetShiftPosition (\$id: ID!) {
|
||||
staff_shift(id: \$id) {
|
||||
$_shiftFields
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String acceptShiftMutation = '''
|
||||
$_positionFragment
|
||||
|
||||
mutation AcceptShift(\$id: ID!) {
|
||||
accept_shift(position_id: \$id) {
|
||||
$_shiftFields
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String trackStaffClockin = '''
|
||||
mutation TrackStaffClockin(\$id: ID!) {
|
||||
track_staff_clockin(position_staff_id: \$id) {
|
||||
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String completeShiftMutation = '''
|
||||
mutation CompleteShift(\$id: ID!, \$break_in: DateTime, \$break_out: DateTime) {
|
||||
track_staff_clockout(position_staff_id: \$id, break_in: \$break_in, break_out: \$break_out) {
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String trackStaffBreak = '''
|
||||
mutation trackStaffBreak(\$id: ID!, \$break_in: DateTime!, \$break_out: DateTime!) {
|
||||
track_staff_break(position_staff_id: \$id, break_in: \$break_in, break_out: \$break_out) {
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String submitNoBreakStaffShiftMutation = '''
|
||||
mutation SubmitNoBreakStaffShift(\$id: ID!, \$reason: NoBreakShiftPenaltyLogReason, \$details: String) {
|
||||
submit_no_break_staff_shift(position_staff_id: \$id, reason: \$reason, details: \$details) {
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String cancelStaffShiftMutation = '''
|
||||
|
||||
mutation cancelStaffShiftMutation(\$position_id: ID!, \$reason: CancelShiftPenaltyLogReason, \$details: String) {
|
||||
cancel_staff_shift(position_staff_id: \$position_id, reason: \$reason, details: \$details) {
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String declineStaffShiftMutation = '''
|
||||
mutation DeclineStaffShift(\$position_id: ID!,\$reason: DeclineShiftPenaltyLogReason, \$details: String) {
|
||||
decline_shift(position_id: \$position_id, reason: \$reason, details: \$details) {
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const _positionFragment = '''
|
||||
$skillFragment
|
||||
|
||||
fragment position on EventShiftPositionStaff {
|
||||
position {
|
||||
id
|
||||
start_time
|
||||
end_time
|
||||
rate
|
||||
break
|
||||
...shift
|
||||
...business_skill
|
||||
}
|
||||
}
|
||||
|
||||
fragment shift on EventShiftPosition {
|
||||
shift {
|
||||
id
|
||||
name
|
||||
address
|
||||
...FullAddress
|
||||
...contacts
|
||||
event {
|
||||
id
|
||||
date
|
||||
name
|
||||
...business
|
||||
additional_info
|
||||
tags{
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
addons{
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment business_skill on EventShiftPosition {
|
||||
business_skill {
|
||||
skill {
|
||||
...SkillFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment business on Event {
|
||||
business {
|
||||
name
|
||||
avatar
|
||||
}
|
||||
}
|
||||
|
||||
fragment contacts on EventShift {
|
||||
contacts {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
avatar
|
||||
title
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment FullAddress on EventShift {
|
||||
full_address {
|
||||
street_number
|
||||
zip_code
|
||||
latitude
|
||||
longitude
|
||||
formatted_address
|
||||
street
|
||||
region
|
||||
city
|
||||
country
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/data/shifts_api_provider.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/domain/shifts_repository.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
|
||||
|
||||
@Singleton(as: ShiftsRepository)
|
||||
class ShiftsRepositoryImpl extends ShiftsRepository {
|
||||
final ShiftsApiProvider _apiProvider;
|
||||
StreamController<EventShiftRoleStaffStatus>? _statusController;
|
||||
|
||||
ShiftsRepositoryImpl({required ShiftsApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Stream<EventShiftRoleStaffStatus> get statusStream {
|
||||
_statusController ??=
|
||||
StreamController<EventShiftRoleStaffStatus>.broadcast();
|
||||
return _statusController!.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ShiftEntity>> getShifts(
|
||||
{String? lastItemId, required ShiftStatusFilterType statusFilter}) async {
|
||||
var paginationWrapper = await _apiProvider
|
||||
.fetchShifts(statusFilterToGqlString(statusFilter), after: lastItemId);
|
||||
return paginationWrapper.edges.map((e) {
|
||||
return ShiftEntity.fromStaffShift(
|
||||
e.node,
|
||||
cursor: (paginationWrapper.pageInfo?.hasNextPage??false) ? e.cursor : null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
statusFilterToGqlString(ShiftStatusFilterType statusFilter) {
|
||||
return statusFilter.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> confirmShift(ShiftEntity shiftViewModel) async {
|
||||
var result = await _apiProvider.confirmShift(shiftViewModel.id);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.assigned);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.confirmed);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clockInShift(ShiftEntity shiftViewModel) async {
|
||||
// try {
|
||||
await _apiProvider.clockInShift(shiftViewModel.id);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.assigned);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.ongoing);
|
||||
// } catch (e) {
|
||||
// _statusController?.add(EventShiftRoleStaffStatus.assigned);
|
||||
// _statusController?.add(EventShiftRoleStaffStatus.ongoing);
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> completeShift(
|
||||
ShiftEntity shiftViewModel, ClockOutDetails clockOutDetails, bool isPast) async {
|
||||
if (clockOutDetails.reason != null) {
|
||||
_apiProvider.completeShiftNoBreak(
|
||||
shiftViewModel.id,
|
||||
reason: clockOutDetails.reason,
|
||||
additionalReason: clockOutDetails.additionalReason,
|
||||
);
|
||||
} else {
|
||||
var breakInTime =
|
||||
DateFormat('H:mm').parse(clockOutDetails.breakStartTime!);
|
||||
var start = DateTime(
|
||||
shiftViewModel.startDate.year,
|
||||
shiftViewModel.startDate.month,
|
||||
shiftViewModel.startDate.day,
|
||||
breakInTime.hour,
|
||||
breakInTime.minute);
|
||||
var breakOutTime =
|
||||
DateFormat('H:mm').parse(clockOutDetails.breakEndTime!);
|
||||
var end = DateTime(
|
||||
shiftViewModel.startDate.year,
|
||||
shiftViewModel.startDate.month,
|
||||
shiftViewModel.startDate.day,
|
||||
breakOutTime.hour,
|
||||
breakOutTime.minute);
|
||||
_apiProvider.completeShift(
|
||||
shiftViewModel.id,
|
||||
isPast: isPast,
|
||||
breakIn: DateFormat('yyyy-MM-dd HH:mm:ss').format(start),
|
||||
breakOut: DateFormat('yyyy-MM-dd HH:mm:ss').format(end),
|
||||
);
|
||||
}
|
||||
_statusController?.add(EventShiftRoleStaffStatus.ongoing);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.completed);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> forceClockOut(String id) async {
|
||||
await _apiProvider.completeShift(id);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.assigned);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.confirmed);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_statusController?.close();
|
||||
_statusController = null;
|
||||
}
|
||||
|
||||
@override
|
||||
declineShift(
|
||||
ShiftEntity shiftViewModel, String? reason, String? additionalReason) {
|
||||
_apiProvider.declineShift(
|
||||
shiftViewModel.id,
|
||||
reason: reason,
|
||||
additionalReason: additionalReason,
|
||||
);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.assigned);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.canceledByStaff);
|
||||
}
|
||||
|
||||
@override
|
||||
cancelShift(
|
||||
ShiftEntity shiftViewModel, String? reason, String? additionalReason) {
|
||||
_apiProvider.cancelShift(
|
||||
shiftViewModel.id,
|
||||
reason: reason,
|
||||
additionalReason: additionalReason,
|
||||
);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.assigned);
|
||||
_statusController?.add(EventShiftRoleStaffStatus.canceledByStaff);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ShiftEntity?> getShiftById(String id) async {
|
||||
return ShiftEntity.fromStaffShift(
|
||||
cursor: '', await _apiProvider.getShiftById(id));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ShiftEntity>> getMissBreakFinishedShift() async{
|
||||
var paginationWrapper = await _apiProvider
|
||||
.getMissBreakFinishedShift();
|
||||
return paginationWrapper.edges.map((e) {
|
||||
return ShiftEntity.fromStaffShift(
|
||||
e.node,
|
||||
cursor: (paginationWrapper.pageInfo?.hasNextPage??false) ? e.cursor : null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/common/date_time_extension.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
|
||||
|
||||
class CompleteDialogBloc
|
||||
extends Bloc<CompleteDialogEvent, CompleteDialogState> {
|
||||
CompleteDialogBloc() : super(const CompleteDialogState()) {
|
||||
on<InitializeCompleteDialog>((event, emit) {
|
||||
|
||||
emit(state.copyWith(
|
||||
minLimit: event.minLimit,
|
||||
startTime: event.minLimit?.toHourMinuteString() ?? DateTime.now().toHourMinuteString(),
|
||||
endTime: event.minLimit?.add(Duration(minutes: event.breakDurationInMinutes)).toHourMinuteString() ?? DateTime.now().add(Duration(minutes: event.breakDurationInMinutes)).toHourMinuteString(),
|
||||
breakDuration: Duration(minutes: event.breakDurationInMinutes),
|
||||
));
|
||||
});
|
||||
|
||||
on<SelectBreakStatus>((event, emit) {
|
||||
emit(state.copyWith(status: event.status));
|
||||
});
|
||||
|
||||
on<SelectReason>((event, emit) {
|
||||
emit(state.copyWith(selectedReason: event.reason));
|
||||
});
|
||||
|
||||
on<ChangeStartTime>((event, emit) {
|
||||
emit(state.copyWith(startTime: event.startTime.toHourMinuteString(),endTime: event.startTime.add(state.breakDuration).toHourMinuteString()));
|
||||
});
|
||||
|
||||
on<ChangeEndTime>((event, emit) {
|
||||
emit(state.copyWith(endTime: event.endTime.toHourMinuteString()));
|
||||
});
|
||||
|
||||
on<ChangeAdditionalReason>((event, emit) {
|
||||
emit(state.copyWith(additionalReason: event.additionalReason));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
|
||||
|
||||
abstract class CompleteDialogEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class InitializeCompleteDialog extends CompleteDialogEvent {
|
||||
final DateTime? minLimit;
|
||||
final int breakDurationInMinutes;
|
||||
|
||||
InitializeCompleteDialog(
|
||||
{this.minLimit, required this.breakDurationInMinutes});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [minLimit];
|
||||
}
|
||||
|
||||
class SelectBreakStatus extends CompleteDialogEvent {
|
||||
final BreakStatus status;
|
||||
|
||||
SelectBreakStatus(this.status);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status];
|
||||
}
|
||||
|
||||
class SelectReason extends CompleteDialogEvent {
|
||||
final String reason;
|
||||
|
||||
SelectReason(this.reason);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [reason];
|
||||
}
|
||||
|
||||
class ChangeStartTime extends CompleteDialogEvent {
|
||||
final DateTime startTime;
|
||||
|
||||
ChangeStartTime(this.startTime);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [startTime];
|
||||
}
|
||||
|
||||
class ChangeEndTime extends CompleteDialogEvent {
|
||||
final DateTime endTime;
|
||||
|
||||
ChangeEndTime(this.endTime);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [endTime];
|
||||
}
|
||||
|
||||
class ChangeAdditionalReason extends CompleteDialogEvent {
|
||||
final String additionalReason;
|
||||
|
||||
ChangeAdditionalReason(this.additionalReason);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [additionalReason];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
enum BreakStatus { neutral, positive, negative }
|
||||
|
||||
class CompleteDialogState extends Equatable {
|
||||
final BreakStatus status;
|
||||
final String? selectedReason;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final String additionalReason;
|
||||
final DateTime? minLimit;
|
||||
final Duration breakDuration;
|
||||
|
||||
const CompleteDialogState({
|
||||
this.status = BreakStatus.neutral,
|
||||
this.selectedReason,
|
||||
this.startTime = '00:00',
|
||||
this.endTime = '00:00',
|
||||
this.additionalReason = '',
|
||||
this.minLimit,
|
||||
this.breakDuration = const Duration(minutes: 0),
|
||||
});
|
||||
|
||||
String? get breakTimeInputError {
|
||||
if (startTime == endTime) {
|
||||
return 'Break start time and end time cannot be the same';
|
||||
}
|
||||
if (DateFormat('H:mm')
|
||||
.parse(startTime)
|
||||
.isAfter(DateFormat('H:mm').parse(endTime))) {
|
||||
return 'Break start time cannot be after break end time';
|
||||
}
|
||||
if (DateFormat('H:mm').parse(endTime).isAfter(DateTime.now())) {
|
||||
return 'Break end time cannot be in the future';
|
||||
}
|
||||
|
||||
if (minLimit != null) {
|
||||
final start = DateFormat('H:mm').parse(startTime);
|
||||
final min =
|
||||
DateFormat('H:mm').parse(DateFormat('H:mm').format(minLimit!));
|
||||
if (start.isBefore(min)) {
|
||||
return 'Break start time cannot be before the shift start time';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CompleteDialogState copyWith({
|
||||
BreakStatus? status,
|
||||
String? selectedReason,
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
String? additionalReason,
|
||||
DateTime? minLimit,
|
||||
Duration? breakDuration,
|
||||
}) {
|
||||
return CompleteDialogState(
|
||||
status: status ?? this.status,
|
||||
selectedReason: selectedReason ?? this.selectedReason,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
additionalReason: additionalReason ?? this.additionalReason,
|
||||
minLimit: minLimit ?? this.minLimit,
|
||||
breakDuration: breakDuration ?? this.breakDuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[status, selectedReason, startTime, endTime, additionalReason];
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/sevices/geofencing_serivce.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/services/force_clockout_service.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/domain/shifts_repository.dart';
|
||||
|
||||
part 'shift_details_event.dart';
|
||||
part 'shift_details_state.dart';
|
||||
|
||||
class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
|
||||
ShiftDetailsBloc()
|
||||
: super(ShiftDetailsState(shiftViewModel: ShiftEntity.empty)) {
|
||||
on<ShiftDetailsInitialEvent>(_onInitial);
|
||||
on<StaffGeofencingUpdate>(_onStaffGeofencingUpdate);
|
||||
on<ShiftUpdateTimerEvent>(_onUpdateTimer);
|
||||
on<ShiftConfirmEvent>(_onConfirm);
|
||||
on<ShiftClockInEvent>(_onClockIn);
|
||||
on<ShiftCompleteEvent>(_onComplete);
|
||||
on<ShiftDeclineEvent>(_onDecline);
|
||||
on<ShiftCancelEvent>(_onCancel);
|
||||
on<ShiftRefreshEvent>(_onRefresh);
|
||||
on<ShiftCheckGeocodingEvent>(_onCheckGeocoding);
|
||||
on<ShiftErrorWasShownEvent>(_onErrorWasShown);
|
||||
}
|
||||
|
||||
final GeofencingService _geofencingService = getIt<GeofencingService>();
|
||||
Timer? _timer;
|
||||
Timer? _refreshTimer;
|
||||
StreamSubscription<bool>? _geofencingStream;
|
||||
|
||||
Future<void> _onInitial(
|
||||
ShiftDetailsInitialEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(shiftViewModel: event.shift));
|
||||
if (event.shift.status == EventShiftRoleStaffStatus.ongoing) {
|
||||
_startOngoingTimer();
|
||||
}
|
||||
_runRefreshTimer();
|
||||
|
||||
add(const ShiftCheckGeocodingEvent());
|
||||
}
|
||||
|
||||
void _onStaffGeofencingUpdate(
|
||||
StaffGeofencingUpdate event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) {
|
||||
emit(state.copyWith(isToFar: !event.isInRange));
|
||||
}
|
||||
|
||||
void _onUpdateTimer(
|
||||
ShiftUpdateTimerEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) {
|
||||
emit(state.copyWith(shiftViewModel: state.shiftViewModel.copyWith()));
|
||||
}
|
||||
|
||||
void _onConfirm(
|
||||
ShiftConfirmEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
await getIt<ShiftsRepository>().confirmShift(state.shiftViewModel);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
shiftViewModel: state.shiftViewModel.copyWith(
|
||||
status: EventShiftRoleStaffStatus.confirmed,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(errorMessage: e.message));
|
||||
}else{
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(isLoading: false));
|
||||
|
||||
}
|
||||
|
||||
void _onClockIn(
|
||||
ShiftClockInEvent event, Emitter<ShiftDetailsState> emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
try {
|
||||
await getIt<ShiftsRepository>().clockInShift(state.shiftViewModel);
|
||||
_geofencingStream?.cancel();
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
shiftViewModel: state.shiftViewModel.copyWith(
|
||||
status: EventShiftRoleStaffStatus.ongoing,
|
||||
clockIn: DateTime.now(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(errorMessage: e.message));
|
||||
}else{
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
}
|
||||
emit(state.copyWith(isLoading: false));
|
||||
}
|
||||
_startOngoingTimer();
|
||||
|
||||
getIt<ForceClockoutService>().startTrackOngoingLocation(
|
||||
state.shiftViewModel,
|
||||
() {
|
||||
onForceUpdateUI();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onComplete(
|
||||
ShiftCompleteEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
emit(
|
||||
state.copyWith(
|
||||
shiftViewModel: state.shiftViewModel.copyWith(
|
||||
status: EventShiftRoleStaffStatus.completed,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(errorMessage: e.message));
|
||||
}else{
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(isLoading: false));
|
||||
|
||||
}
|
||||
|
||||
void _onDecline(
|
||||
ShiftDeclineEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
await getIt<ShiftsRepository>().declineShift(
|
||||
state.shiftViewModel, event.reason, event.additionalReason);
|
||||
emit(state.copyWith(
|
||||
needPop: true,
|
||||
shiftViewModel: state.shiftViewModel.copyWith(
|
||||
status: EventShiftRoleStaffStatus.canceledByStaff,
|
||||
)));
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(errorMessage: e.message));
|
||||
}else{
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(isLoading: false));
|
||||
|
||||
}
|
||||
|
||||
void _onErrorWasShown(
|
||||
ShiftErrorWasShownEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) {
|
||||
emit(state.copyWith(errorMessage: ''));
|
||||
}
|
||||
|
||||
void _onCancel(
|
||||
ShiftCancelEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
await getIt<ShiftsRepository>().cancelShift(
|
||||
state.shiftViewModel, event.reason, event.additionalReason);
|
||||
emit(state.copyWith(
|
||||
shiftViewModel: state.shiftViewModel.copyWith(
|
||||
status: EventShiftRoleStaffStatus.canceledByStaff,
|
||||
)));
|
||||
} catch (e) {
|
||||
print('!!!!!! ${e}');
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(errorMessage: e.message));
|
||||
}else{
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(isLoading: false));
|
||||
|
||||
}
|
||||
|
||||
void _onRefresh(
|
||||
ShiftRefreshEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) {
|
||||
emit(state.copyWith(
|
||||
shiftViewModel: event.shift,
|
||||
proximityState: GeofencingProximityState.none));
|
||||
}
|
||||
|
||||
void _onCheckGeocoding(
|
||||
ShiftCheckGeocodingEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(
|
||||
proximityState: GeofencingProximityState.none,
|
||||
));
|
||||
|
||||
await _checkByGeocoding(emit);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_timer?.cancel();
|
||||
_refreshTimer?.cancel();
|
||||
_geofencingStream?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _runRefreshTimer() {
|
||||
_refreshTimer = Timer.periodic(
|
||||
const Duration(seconds: 10),
|
||||
(timer) async {
|
||||
final shift = await getIt<ShiftsRepository>()
|
||||
.getShiftById(state.shiftViewModel.id);
|
||||
if (shift == null) {
|
||||
return;
|
||||
}
|
||||
add((ShiftRefreshEvent(shift)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkByGeocoding(Emitter<ShiftDetailsState> emit) async {
|
||||
if (![
|
||||
EventShiftRoleStaffStatus.assigned,
|
||||
EventShiftRoleStaffStatus.confirmed,
|
||||
EventShiftRoleStaffStatus.ongoing
|
||||
].contains(state.shiftViewModel.status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final geolocationCheck =
|
||||
await _geofencingService.requestGeolocationPermission();
|
||||
emit(
|
||||
state.copyWith(
|
||||
proximityState: switch (geolocationCheck) {
|
||||
GeolocationStatus.disabled =>
|
||||
GeofencingProximityState.locationDisabled,
|
||||
GeolocationStatus.denied => GeofencingProximityState.permissionDenied,
|
||||
GeolocationStatus.prohibited => GeofencingProximityState.goToSettings,
|
||||
GeolocationStatus.onlyInUse => GeofencingProximityState.onlyInUse,
|
||||
GeolocationStatus.enabled => null,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (geolocationCheck == GeolocationStatus.enabled) {
|
||||
_geofencingStream = _geofencingService
|
||||
.isInRangeStream(
|
||||
pointLatitude: state.shiftViewModel.locationLat,
|
||||
pointLongitude: state.shiftViewModel.locationLon,
|
||||
)
|
||||
.listen(
|
||||
(isInRange) {
|
||||
|
||||
add(StaffGeofencingUpdate(isInRange: isInRange));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _startOngoingTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(
|
||||
const Duration(seconds: 10),
|
||||
(timer) {
|
||||
if (state.shiftViewModel.status != EventShiftRoleStaffStatus.ongoing) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
add(const ShiftUpdateTimerEvent());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onForceUpdateUI() async {
|
||||
_timer?.cancel();
|
||||
if (!isClosed) {
|
||||
final shift =
|
||||
await getIt<ShiftsRepository>().getShiftById(state.shiftViewModel.id);
|
||||
if (shift == null) {
|
||||
return;
|
||||
}
|
||||
add((ShiftRefreshEvent(shift)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
part of 'shift_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class ShiftDetailsEvent {
|
||||
const ShiftDetailsEvent();
|
||||
}
|
||||
|
||||
class ShiftDetailsInitialEvent extends ShiftDetailsEvent {
|
||||
final ShiftEntity shift;
|
||||
|
||||
const ShiftDetailsInitialEvent({required this.shift});
|
||||
}
|
||||
|
||||
class StaffGeofencingUpdate extends ShiftDetailsEvent {
|
||||
const StaffGeofencingUpdate({required this.isInRange});
|
||||
|
||||
final bool isInRange;
|
||||
}
|
||||
|
||||
class ShiftUpdateTimerEvent extends ShiftDetailsEvent {
|
||||
const ShiftUpdateTimerEvent();
|
||||
}
|
||||
|
||||
class ShiftCompleteEvent extends ShiftDetailsEvent {
|
||||
const ShiftCompleteEvent();
|
||||
}
|
||||
|
||||
class ShiftClockInEvent extends ShiftDetailsEvent {
|
||||
const ShiftClockInEvent();
|
||||
}
|
||||
|
||||
class ShiftConfirmEvent extends ShiftDetailsEvent {
|
||||
const ShiftConfirmEvent();
|
||||
}
|
||||
|
||||
class ShiftDeclineEvent extends ShiftDetailsEvent {
|
||||
final String? reason;
|
||||
final String? additionalReason;
|
||||
|
||||
const ShiftDeclineEvent(this.reason, this.additionalReason);
|
||||
}
|
||||
|
||||
class ShiftCancelEvent extends ShiftDetailsEvent {
|
||||
final String? reason;
|
||||
final String? additionalReason;
|
||||
|
||||
const ShiftCancelEvent(this.reason, this.additionalReason);
|
||||
}
|
||||
|
||||
class ShiftRefreshEvent extends ShiftDetailsEvent {
|
||||
final ShiftEntity shift;
|
||||
|
||||
const ShiftRefreshEvent(this.shift);
|
||||
}
|
||||
|
||||
class ShiftCheckGeocodingEvent extends ShiftDetailsEvent {
|
||||
const ShiftCheckGeocodingEvent();
|
||||
}
|
||||
|
||||
class ShiftErrorWasShownEvent extends ShiftDetailsEvent {
|
||||
const ShiftErrorWasShownEvent();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
part of 'shift_details_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class ShiftDetailsState {
|
||||
final ShiftEntity shiftViewModel;
|
||||
final bool isToFar;
|
||||
final bool isLoading;
|
||||
final GeofencingProximityState proximityState;
|
||||
final String? errorMessage;
|
||||
final bool needPop;
|
||||
|
||||
const ShiftDetailsState({
|
||||
required this.shiftViewModel,
|
||||
this.isToFar = true,
|
||||
this.isLoading = false,
|
||||
this.proximityState = GeofencingProximityState.none,
|
||||
this.errorMessage,
|
||||
this.needPop = false,
|
||||
});
|
||||
|
||||
ShiftDetailsState copyWith({
|
||||
ShiftEntity? shiftViewModel,
|
||||
bool? isToFar,
|
||||
bool? isLoading,
|
||||
GeofencingProximityState? proximityState,
|
||||
String? errorMessage,
|
||||
bool? needPop
|
||||
}) {
|
||||
return ShiftDetailsState(
|
||||
shiftViewModel: shiftViewModel ?? this.shiftViewModel,
|
||||
isToFar: isToFar ?? this.isToFar,
|
||||
isLoading: isLoading ?? false,
|
||||
proximityState: proximityState ?? this.proximityState,
|
||||
errorMessage: errorMessage,
|
||||
needPop: needPop ?? this.needPop,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum GeofencingProximityState {
|
||||
none,
|
||||
tooFar,
|
||||
locationDisabled,
|
||||
goToSettings,
|
||||
onlyInUse,
|
||||
permissionDenied,
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_event.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_state.dart';
|
||||
import 'package:krow/features/shifts/domain/services/force_clockout_service.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/domain/shifts_repository.dart';
|
||||
|
||||
class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
||||
var indexToStatus = <int, ShiftStatusFilterType>{
|
||||
0: ShiftStatusFilterType.assigned,
|
||||
1: ShiftStatusFilterType.confirmed,
|
||||
2: ShiftStatusFilterType.ongoing,
|
||||
3: ShiftStatusFilterType.completed,
|
||||
4: ShiftStatusFilterType.canceled,
|
||||
};
|
||||
|
||||
ShiftsBloc()
|
||||
: super(const ShiftsState(tabs: {
|
||||
0: ShiftTabState(items: [], isLoading: true),
|
||||
1: ShiftTabState(items: []),
|
||||
2: ShiftTabState(items: []),
|
||||
3: ShiftTabState(items: []),
|
||||
4: ShiftTabState(items: []),
|
||||
})) {
|
||||
on<ShiftsInitialEvent>(_onInitial);
|
||||
on<ShiftsTabChangedEvent>(_onTabChanged);
|
||||
on<LoadTabShiftEvent>(_onLoadTabItems);
|
||||
on<LoadMoreShiftEvent>(_onLoadMoreTabItems);
|
||||
on<ReloadMissingBreakShift>(_onReloadMissingBreakShift);
|
||||
|
||||
getIt<ShiftsRepository>().statusStream.listen((event) {
|
||||
add(LoadTabShiftEvent(status: event.index));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onInitial(ShiftsInitialEvent event, emit) async {
|
||||
add(const LoadTabShiftEvent(status: 0));
|
||||
add(const LoadTabShiftEvent(status: 2));
|
||||
|
||||
var missedShifts =
|
||||
await getIt<ShiftsRepository>().getMissBreakFinishedShift();
|
||||
if (missedShifts.isNotEmpty) {
|
||||
emit(state.copyWith(
|
||||
missedShifts: missedShifts,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTabChanged(ShiftsTabChangedEvent event, emit) async {
|
||||
emit(state.copyWith(tabIndex: event.tabIndex));
|
||||
final currentTabState = state.tabs[event.tabIndex]!;
|
||||
if (currentTabState.items.isEmpty && !currentTabState.isLoading) {
|
||||
add(LoadTabShiftEvent(status: event.tabIndex));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadTabItems(LoadTabShiftEvent event, emit) async {
|
||||
await _fetchShifts(event.status, null, emit);
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreTabItems(LoadMoreShiftEvent event, emit) async {
|
||||
final currentTabState = state.tabs[event.status]!;
|
||||
if (!currentTabState.hasMoreItems || currentTabState.isLoading) return;
|
||||
await _fetchShifts(event.status, currentTabState.items, emit);
|
||||
}
|
||||
|
||||
_fetchShifts(int tabIndex, List<ShiftEntity>? previousItems, emit) async {
|
||||
if (previousItems != null && previousItems.lastOrNull?.cursor == null) {
|
||||
return;
|
||||
}
|
||||
final currentTabState = state.tabs[tabIndex]!;
|
||||
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: currentTabState.copyWith(isLoading: true),
|
||||
},
|
||||
));
|
||||
|
||||
try {
|
||||
var items = await getIt<ShiftsRepository>().getShifts(
|
||||
statusFilter: indexToStatus[tabIndex]!,
|
||||
lastItemId: previousItems?.lastOrNull?.cursor,
|
||||
);
|
||||
|
||||
// if(items.isNotEmpty){
|
||||
// items = List.generate(20, (i)=>items[0]);
|
||||
// }
|
||||
var allItems = (previousItems ?? [])..addAll(items);
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: currentTabState.copyWith(
|
||||
items: allItems,
|
||||
hasMoreItems: items.isNotEmpty,
|
||||
isLoading: false,
|
||||
),
|
||||
},
|
||||
));
|
||||
|
||||
if (tabIndex == 2 &&
|
||||
allItems.isNotEmpty ) {
|
||||
getIt<ForceClockoutService>()
|
||||
.startTrackOngoingLocation(allItems.first, () {});
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugPrint(e.toString());
|
||||
debugPrint(s.toString());
|
||||
emit(state.copyWith(
|
||||
tabs: {
|
||||
...state.tabs,
|
||||
tabIndex: currentTabState.copyWith(isLoading: false),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onReloadMissingBreakShift(
|
||||
ReloadMissingBreakShift event, emit) async {
|
||||
emit(state.copyWith(missedShifts: []));
|
||||
var missedShifts =
|
||||
await getIt<ShiftsRepository>().getMissBreakFinishedShift();
|
||||
emit(state.copyWith(missedShifts: missedShifts));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
getIt<ShiftsRepository>().dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
sealed class ShiftsEvent {
|
||||
const ShiftsEvent();
|
||||
}
|
||||
|
||||
class ShiftsInitialEvent extends ShiftsEvent {
|
||||
const ShiftsInitialEvent();
|
||||
}
|
||||
|
||||
class ShiftsTabChangedEvent extends ShiftsEvent {
|
||||
final int tabIndex;
|
||||
|
||||
const ShiftsTabChangedEvent({required this.tabIndex});
|
||||
}
|
||||
|
||||
class LoadTabShiftEvent extends ShiftsEvent {
|
||||
final int status;
|
||||
|
||||
const LoadTabShiftEvent({required this.status});
|
||||
}
|
||||
|
||||
class LoadMoreShiftEvent extends ShiftsEvent {
|
||||
final int status;
|
||||
|
||||
const LoadMoreShiftEvent({required this.status});
|
||||
}
|
||||
|
||||
class ReloadMissingBreakShift extends ShiftsEvent {
|
||||
|
||||
const ReloadMissingBreakShift();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftsState {
|
||||
final bool isLoading;
|
||||
final int tabIndex;
|
||||
|
||||
final Map<int, ShiftTabState> tabs;
|
||||
final List<ShiftEntity> missedShifts;
|
||||
|
||||
const ShiftsState(
|
||||
{this.isLoading = false,
|
||||
this.tabIndex = 0,
|
||||
required this.tabs,
|
||||
this.missedShifts = const []});
|
||||
|
||||
ShiftsState copyWith({
|
||||
bool? isLoading,
|
||||
int? tabIndex,
|
||||
Map<int, ShiftTabState>? tabs,
|
||||
List<ShiftEntity>? missedShifts,
|
||||
}) {
|
||||
return ShiftsState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
tabIndex: tabIndex ?? this.tabIndex,
|
||||
tabs: tabs ?? this.tabs,
|
||||
missedShifts: missedShifts ?? this.missedShifts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShiftTabState {
|
||||
final List<ShiftEntity> items;
|
||||
final bool isLoading;
|
||||
final bool hasMoreItems;
|
||||
|
||||
const ShiftTabState({
|
||||
required this.items,
|
||||
this.isLoading = false,
|
||||
this.hasMoreItems = true,
|
||||
});
|
||||
|
||||
ShiftTabState copyWith({
|
||||
List<ShiftEntity>? items,
|
||||
bool? isLoading,
|
||||
bool? hasMoreItems,
|
||||
}) {
|
||||
return ShiftTabState(
|
||||
items: items ?? this.items,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/sevices/background_service/background_task.dart';
|
||||
import 'package:krow/core/sevices/geofencing_serivce.dart';
|
||||
import 'package:krow/features/shifts/domain/shifts_repository.dart';
|
||||
|
||||
class ContinuousClockoutCheckerTask implements BackgroundTask {
|
||||
@override
|
||||
Future<void> oneTime(ServiceInstance? service) async {
|
||||
// var shift = (await getIt<ShiftsRepository>()
|
||||
// .getShifts(statusFilter: ShiftStatusFilterType.ongoing)
|
||||
// .onError((error, stackTrace) {
|
||||
// return [];
|
||||
// }))
|
||||
// .firstOrNull;
|
||||
//
|
||||
// if (shift == null) {
|
||||
// if (service is AndroidServiceInstance) {
|
||||
// service.setAsBackgroundService();
|
||||
// }
|
||||
// return;
|
||||
// } else {
|
||||
// GeofencingService geofencingService = getIt<GeofencingService>();
|
||||
// try {
|
||||
// var permission = await geofencingService.requestGeolocationPermission();
|
||||
// if (permission == GeolocationStatus.enabled) {
|
||||
// var inArea = await geofencingService.isInRangeCheck(
|
||||
// pointLatitude: shift.locationLat,
|
||||
// pointLongitude: shift.locationLon,
|
||||
// range: 500,
|
||||
// );
|
||||
// if (!inArea) {
|
||||
// await getIt<ShiftsRepository>().forceClockOut(shift.id);
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {}
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/sevices/geofencing_serivce.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/domain/shifts_repository.dart';
|
||||
|
||||
|
||||
StreamSubscription? geofencingClockOutStream;
|
||||
|
||||
@singleton
|
||||
class ForceClockoutService {
|
||||
final GeofencingService geofencingService;
|
||||
|
||||
ForceClockoutService(this.geofencingService);
|
||||
|
||||
startTrackOngoingLocation(ShiftEntity shift, VoidCallback? onClockOut) {
|
||||
// if (geofencingClockOutStream != null) {
|
||||
// geofencingClockOutStream?.cancel();
|
||||
// }
|
||||
//
|
||||
// geofencingClockOutStream = geofencingService
|
||||
// .isInRangeStream(
|
||||
// pointLatitude: shift.locationLat, pointLongitude: shift.locationLon)
|
||||
// .listen(
|
||||
// (isInRange) async {
|
||||
// if (!isInRange) {
|
||||
// await getIt<ShiftsRepository>().forceClockOut(shift.id);
|
||||
// onClockOut?.call();
|
||||
// geofencingClockOutStream?.cancel();
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/domain/shifts_repository.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
|
||||
|
||||
class ShiftCompleterService {
|
||||
Future<void> startCompleteProcess(BuildContext context, ShiftEntity shift,
|
||||
{required Null Function() onComplete, bool canSkip = true}) async {
|
||||
|
||||
var result = await ShiftCompleteDialog.showCustomDialog(
|
||||
context,
|
||||
canSkip,
|
||||
shift.eventName,
|
||||
shift.clockIn ?? DateTime.now(),
|
||||
shift.planingBreakTime ?? 30);
|
||||
|
||||
if (result != null) {
|
||||
if(!kDebugMode)
|
||||
await getIt<ShiftsRepository>()
|
||||
.completeShift(shift, result['details'], !canSkip);
|
||||
if (result['result'] == true) {
|
||||
await KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.medalStar,
|
||||
state: KwDialogState.positive,
|
||||
title: 'Congratulations, Shift Completed!',
|
||||
message:
|
||||
'Your break has been logged and added to your timeline. Keep up the good work!',
|
||||
primaryButtonLabel: 'Back to Shift');
|
||||
} else {
|
||||
await KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'Your Selection is under review',
|
||||
message:
|
||||
'Labor Code § 512 requires California employers to give unpaid lunch breaks to non-exempt employees. Lunch breaks must be uninterrupted. Employers cannot require employees to do any work while on their lunch breaks. They also cannot discourage employees from taking one. However, the employer and employee can agree to waive the meal break if the worker’s shift is less than 6 hours.',
|
||||
child: const Text(
|
||||
'Once resolved you will be notify.\nNo further Action',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
primaryButtonLabel: 'Continue');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:krow/features/shifts/data/models/cancellation_reason.dart';
|
||||
import 'package:krow/features/shifts/data/models/event.dart';
|
||||
import 'package:krow/features/shifts/data/models/event_tag.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
|
||||
@immutable
|
||||
class ShiftEntity {
|
||||
final String id;
|
||||
final String imageUrl;
|
||||
final String skillName;
|
||||
final String businessName;
|
||||
final DateTime assignedDate;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final DateTime? clockIn;
|
||||
final DateTime? clockOut;
|
||||
final String locationName;
|
||||
final double locationLat;
|
||||
final double locationLon;
|
||||
final EventShiftRoleStaffStatus status;
|
||||
final String? rate;
|
||||
final List<EventTag>? tags;
|
||||
final StaffRating? rating;
|
||||
final int? planingBreakTime;
|
||||
final int? totalBreakTime;
|
||||
final int? paymentStatus;
|
||||
final int? canceledReason;
|
||||
final List<Addon>? additionalData;
|
||||
final List<ShiftManager> managers;
|
||||
final String? additionalInfo;
|
||||
final String? cursor;
|
||||
final CancellationReason? cancellationReason;
|
||||
final String eventId;
|
||||
final String eventName;
|
||||
|
||||
const ShiftEntity({
|
||||
required this.id,
|
||||
required this.skillName,
|
||||
required this.businessName,
|
||||
required this.locationName,
|
||||
required this.locationLat,
|
||||
required this.locationLon,
|
||||
required this.status,
|
||||
required this.rate,
|
||||
required this.imageUrl,
|
||||
required this.assignedDate,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.clockIn,
|
||||
required this.clockOut,
|
||||
required this.tags,
|
||||
required this.planingBreakTime,
|
||||
required this.totalBreakTime,
|
||||
required this.paymentStatus,
|
||||
required this.canceledReason,
|
||||
required this.additionalData,
|
||||
required this.managers,
|
||||
required this.rating,
|
||||
required this.additionalInfo,
|
||||
required this.eventId,
|
||||
required this.eventName,
|
||||
this.cancellationReason,
|
||||
this.cursor,
|
||||
});
|
||||
|
||||
ShiftEntity copyWith({
|
||||
String? id,
|
||||
String? imageUrl,
|
||||
String? skillName,
|
||||
String? businessName,
|
||||
DateTime? assignedDate,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
DateTime? clockIn,
|
||||
DateTime? clockOut,
|
||||
String? locationName,
|
||||
double? locationLat,
|
||||
double? locationLon,
|
||||
EventShiftRoleStaffStatus? status,
|
||||
String? rate,
|
||||
List<EventTag>? tags,
|
||||
StaffRating? rating,
|
||||
int? planingBreakTime,
|
||||
int? totalBreakTime,
|
||||
int? paymentStatus,
|
||||
int? canceledReason,
|
||||
List<Addon>? additionalData,
|
||||
List<ShiftManager>? managers,
|
||||
String? additionalInfo,
|
||||
String? eventId,
|
||||
String? eventName,
|
||||
String? cursor,
|
||||
}) =>
|
||||
ShiftEntity(
|
||||
id: id ?? this.id,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
skillName: skillName ?? this.skillName,
|
||||
businessName: businessName ?? this.businessName,
|
||||
locationName: locationName ?? this.locationName,
|
||||
locationLat: locationLat ?? this.locationLat,
|
||||
locationLon: locationLon ?? this.locationLon,
|
||||
status: status ?? this.status,
|
||||
rate: rate ?? this.rate,
|
||||
assignedDate: assignedDate ?? this.assignedDate,
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
clockIn: clockIn ?? this.clockIn,
|
||||
clockOut: clockOut ?? this.clockOut,
|
||||
tags: tags ?? this.tags,
|
||||
planingBreakTime: planingBreakTime ?? this.planingBreakTime,
|
||||
totalBreakTime: totalBreakTime ?? this.totalBreakTime,
|
||||
paymentStatus: paymentStatus ?? this.paymentStatus,
|
||||
canceledReason: canceledReason ?? this.canceledReason,
|
||||
additionalData: additionalData ?? this.additionalData,
|
||||
managers: managers ?? this.managers,
|
||||
rating: rating ?? this.rating,
|
||||
additionalInfo: additionalInfo ?? this.additionalInfo,
|
||||
cancellationReason: cancellationReason,
|
||||
eventId: eventId ?? this.eventId,
|
||||
eventName: eventName ?? this.eventName,
|
||||
cursor: cursor ?? this.cursor,
|
||||
);
|
||||
|
||||
static ShiftEntity empty = ShiftEntity(
|
||||
id: '',
|
||||
skillName: '',
|
||||
businessName: '',
|
||||
locationName: '',
|
||||
locationLat: .0,
|
||||
locationLon: .0,
|
||||
status: EventShiftRoleStaffStatus.assigned,
|
||||
rate: '',
|
||||
imageUrl: '',
|
||||
assignedDate: DateTime.now(),
|
||||
clockIn: null,
|
||||
clockOut: null,
|
||||
startDate: DateTime.now(),
|
||||
endDate: DateTime.now(),
|
||||
tags: [],
|
||||
planingBreakTime: 0,
|
||||
totalBreakTime: 0,
|
||||
paymentStatus: 0,
|
||||
canceledReason: 0,
|
||||
rating: StaffRating(id: '0', rating: 0),
|
||||
additionalData: [],
|
||||
managers: [],
|
||||
eventId: '',
|
||||
eventName: '',
|
||||
additionalInfo: null,
|
||||
);
|
||||
|
||||
static ShiftEntity fromStaffShift(
|
||||
StaffShift shift, {
|
||||
required String? cursor,
|
||||
}) {
|
||||
return ShiftEntity(
|
||||
id: shift.id,
|
||||
eventId: shift.position?.shift?.event?.id ?? '',
|
||||
eventName: shift.position?.shift?.event?.name ?? '',
|
||||
cursor: cursor,
|
||||
skillName: shift.position?.businessSkill?.skill?.name ?? '',
|
||||
businessName: shift.position?.shift?.event?.business?.name ?? '',
|
||||
locationName: shift.position?.shift?.fullAddress?.formattedAddress ?? '',
|
||||
locationLat: shift.position?.shift?.fullAddress?.latitude ?? 0,
|
||||
locationLon: shift.position?.shift?.fullAddress?.longitude ?? 0,
|
||||
status: shift.status,
|
||||
rate: shift.position?.rate?.toStringAsFixed(2) ?? '0',
|
||||
imageUrl: shift.position?.shift?.event?.business?.avatar ?? '',
|
||||
additionalInfo: shift.position?.shift?.event?.additionalInfo,
|
||||
assignedDate: shift.statusUpdatedAt ?? DateTime.now(),
|
||||
clockIn: shift.clockIn,
|
||||
clockOut: shift.clockOut,
|
||||
startDate: shift.startAt ?? DateTime.now(),
|
||||
endDate: shift.endAt ?? DateTime.now(),
|
||||
tags: shift.position?.shift?.event?.tags,
|
||||
planingBreakTime: shift.position?.breakMinutes ?? 0,
|
||||
totalBreakTime: shift.breakOut
|
||||
?.difference(shift.breakIn ?? DateTime.now())
|
||||
.inMinutes ??
|
||||
0,
|
||||
paymentStatus: 0,
|
||||
canceledReason: 0,
|
||||
rating: shift.rating,
|
||||
cancellationReason: shift.cancelReason?.firstOrNull?.reason,
|
||||
additionalData: shift.position?.shift?.event?.addons ?? [],
|
||||
managers: [
|
||||
for (final contact in shift.position?.shift?.contacts ?? [])
|
||||
ShiftManager(
|
||||
id: contact.id,
|
||||
name: '${contact.firstName} ${contact.lastName}',
|
||||
imageUrl: contact.avatar ?? '',
|
||||
phoneNumber: contact.authInfo.phone,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ShiftManager {
|
||||
final String id;
|
||||
final String name;
|
||||
final String imageUrl;
|
||||
final String phoneNumber;
|
||||
|
||||
const ShiftManager({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.imageUrl,
|
||||
required this.phoneNumber,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
|
||||
|
||||
enum ShiftStatusFilterType { assigned, confirmed, ongoing, completed, canceled }
|
||||
|
||||
abstract class ShiftsRepository {
|
||||
Stream<EventShiftRoleStaffStatus> get statusStream;
|
||||
|
||||
Future<List<ShiftEntity>> getShifts(
|
||||
{String? lastItemId, required ShiftStatusFilterType statusFilter});
|
||||
|
||||
Future<void> confirmShift(ShiftEntity shiftViewModel);
|
||||
|
||||
Future<void> clockInShift(ShiftEntity shiftViewModel);
|
||||
|
||||
Future<void> completeShift(
|
||||
ShiftEntity shiftViewModel, ClockOutDetails clockOutDetails,bool isPast);
|
||||
|
||||
Future<void> forceClockOut(String id);
|
||||
|
||||
void dispose();
|
||||
|
||||
declineShift(
|
||||
ShiftEntity shiftViewModel, String? reason, String? additionalReason);
|
||||
|
||||
cancelShift(
|
||||
ShiftEntity shiftViewModel, String? reason, String? additionalReason);
|
||||
|
||||
Future<ShiftEntity?> getShiftById(String id);
|
||||
|
||||
Future<List<ShiftEntity>> getMissBreakFinishedShift();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.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/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/cancel_dialog/cancel_reason_dropdown.dart';
|
||||
|
||||
class CancelDialogView extends StatefulWidget {
|
||||
const CancelDialogView({super.key});
|
||||
|
||||
@override
|
||||
State<CancelDialogView> createState() => _CancelDialogViewState();
|
||||
}
|
||||
|
||||
class _CancelDialogViewState extends State<CancelDialogView> {
|
||||
String selectedReason = '';
|
||||
final _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'cancel_shift'.tr(),
|
||||
style: AppTextStyles.headingH1.copyWith(height: 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'please_select_reason'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
CancelReasonDropdown(
|
||||
selectedReason: selectedReason,
|
||||
onReasonSelected: (String reason) {
|
||||
setState(() {
|
||||
selectedReason = reason;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
controller: _textEditingController,
|
||||
minHeight: 144,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
radius: 12,
|
||||
title: 'additional_reasons'.tr(),
|
||||
hintText: 'enter_main_text'.tr(),
|
||||
),
|
||||
const Gap(24),
|
||||
_buttonGroup(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buttonGroup(
|
||||
BuildContext context,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
disabled: selectedReason.isEmpty,
|
||||
label: 'submit_reason'.tr(),
|
||||
onPressed: () {
|
||||
context.maybePop({
|
||||
'reason': selectedReason,
|
||||
'additionalReason': _textEditingController.text,
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'cancel'.tr(),
|
||||
onPressed: () {
|
||||
context.maybePop();
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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_popup_menu.dart';
|
||||
|
||||
const reasons = {
|
||||
'sick_leave': 'sick_leave',
|
||||
'vacation ': 'vacation',
|
||||
'other': 'other_specify',
|
||||
};
|
||||
|
||||
class CancelReasonDropdown extends StatelessWidget {
|
||||
final String? selectedReason;
|
||||
final Function(String reason) onReasonSelected;
|
||||
|
||||
const CancelReasonDropdown(
|
||||
{super.key,
|
||||
required this.selectedReason,
|
||||
required this.onReasonSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: buildReasonInput(context, selectedReason),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildReasonInput(BuildContext context, String? selectedReason) {
|
||||
return [
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'reason'.tr(),
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
KwPopupMenu(
|
||||
horizontalPadding: 40,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened, selectedReason);
|
||||
},
|
||||
menuItems: [
|
||||
...reasons.entries
|
||||
.map((e) => _buildMenuItem(context, e, selectedReason ?? ''))
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Container _buildMenuButton(bool isOpened, String? selectedReason) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isOpened ? AppColors.bgColorDark : AppColors.grayTintStroke,
|
||||
width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
reasons[selectedReason]?.tr() ?? 'select_reason_from_list'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selectedReason == null
|
||||
? AppColors.blackGray
|
||||
: AppColors.blackBlack),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwPopupMenuItem _buildMenuItem(BuildContext context,
|
||||
MapEntry<String, String> entry, String selectedReason) {
|
||||
return KwPopupMenuItem(
|
||||
title: entry.value.tr(),
|
||||
onTap: () {
|
||||
onReasonSelected(entry.key);
|
||||
},
|
||||
icon: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason != entry.key ? null : AppColors.bgColorDark,
|
||||
shape: BoxShape.circle,
|
||||
border: selectedReason == entry.key
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: selectedReason == entry.key
|
||||
? Center(
|
||||
child: Assets.images.icons.check.svg(
|
||||
height: 10,
|
||||
width: 10,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite, BlendMode.srcIn),
|
||||
))
|
||||
: null,
|
||||
),
|
||||
textStyle: AppTextStyles.bodySmallMed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/cancel_dialog/cancel_dialog_view.dart';
|
||||
|
||||
class ShiftCancelDialog extends StatelessWidget {
|
||||
const ShiftCancelDialog({super.key});
|
||||
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(
|
||||
BuildContext context) async {
|
||||
return await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => const CancelDialogView(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CancelDialogView();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/widgets/complete_dialog_view.dart';
|
||||
|
||||
//TODO(Artem: create widgets instead helper methods. Add initial break time values.Move reasons to state. Create Time slots validator.)
|
||||
class ShiftCompleteDialog {
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(BuildContext context,
|
||||
bool canSkip, String eventName, DateTime minLimit, int breakDurationInMinutes) async {
|
||||
return showDialog<Map<String, dynamic>>(
|
||||
barrierDismissible: canSkip,
|
||||
context: context,
|
||||
builder: (context) => BlocProvider(
|
||||
create: (_) => CompleteDialogBloc()
|
||||
..add(
|
||||
InitializeCompleteDialog(
|
||||
minLimit: minLimit,
|
||||
breakDurationInMinutes:breakDurationInMinutes , // Default break duration
|
||||
),
|
||||
),
|
||||
child: CompleteDialogView(
|
||||
canSkip: canSkip,
|
||||
eventName: eventName,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClockOutDetails {
|
||||
final String? breakStartTime;
|
||||
final String? breakEndTime;
|
||||
final String? reason;
|
||||
final String? additionalReason;
|
||||
|
||||
ClockOutDetails._(
|
||||
{this.breakStartTime,
|
||||
this.breakEndTime,
|
||||
this.reason,
|
||||
this.additionalReason});
|
||||
|
||||
factory ClockOutDetails.positive(String breakStartTime, String breakEndTime) {
|
||||
return ClockOutDetails._(
|
||||
breakStartTime: breakStartTime,
|
||||
breakEndTime: breakEndTime,
|
||||
);
|
||||
}
|
||||
|
||||
factory ClockOutDetails.negative(String reason, String additionalReason) {
|
||||
return ClockOutDetails._(
|
||||
reason: reason,
|
||||
additionalReason: additionalReason,
|
||||
);
|
||||
}
|
||||
|
||||
factory ClockOutDetails.empty() {
|
||||
return ClockOutDetails._();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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/date_time_extension.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/kw_time_slot.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
|
||||
|
||||
class BreakTimePicker extends StatelessWidget {
|
||||
final CompleteDialogState state;
|
||||
|
||||
const BreakTimePicker({super.key, required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
label: 'Break Start Time',
|
||||
initialValue: DateFormat('H:mm').parse(state.startTime),
|
||||
onChange: (v) => context.read<CompleteDialogBloc>().add(
|
||||
ChangeStartTime(v),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: KwTimeSlotInput(
|
||||
editable: false,
|
||||
label: 'Break End Time',
|
||||
initialValue: DateFormat('H:mm').parse(state.endTime),
|
||||
onChange: (v) {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (state.breakTimeInputError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 16),
|
||||
child: Text(
|
||||
state.breakTimeInputError!,
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.statusError),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.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/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/widgets/break_time_picker.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/widgets/complete_reason_input.dart';
|
||||
|
||||
class CompleteDialogView extends StatelessWidget {
|
||||
CompleteDialogView({super.key, this.canSkip = true, required this.eventName});
|
||||
|
||||
final _textEditingController = TextEditingController();
|
||||
final bool canSkip;
|
||||
final String eventName;
|
||||
|
||||
String _title(BreakStatus state) => state == BreakStatus.negative
|
||||
? 'help_us_understand'.tr()
|
||||
: 'did_you_take_a_break'.tr();
|
||||
|
||||
String _message(BreakStatus state) => state == BreakStatus.negative
|
||||
? 'taking_breaks_essential'.tr()
|
||||
: 'taking_regular_breaks'.tr(namedArgs: {'eventName': eventName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CompleteDialogBloc, CompleteDialogState>(
|
||||
builder: (context, state) {
|
||||
return Center(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_title(state.status),
|
||||
style: AppTextStyles.headingH1.copyWith(height: 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
_message(state.status),
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
if (state.status == BreakStatus.positive)
|
||||
BreakTimePicker(state: state),
|
||||
if (state.status == BreakStatus.negative)
|
||||
CompleteReasonInput(selectedReason: state.selectedReason),
|
||||
if (state.status == BreakStatus.negative) ...[
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
controller: _textEditingController,
|
||||
minHeight: 144,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
radius: 12,
|
||||
title: 'additional_reasons'.tr(),
|
||||
hintText: 'enter_main_text'.tr(),
|
||||
),
|
||||
],
|
||||
const Gap(24),
|
||||
_buttonGroup(context, state),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buttonGroup(BuildContext context, CompleteDialogState state) {
|
||||
var cancelButton = canSkip
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: KwButton.outlinedPrimary(
|
||||
label: 'cancel'.tr(),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
if (state.status == BreakStatus.neutral) ...[
|
||||
KwButton.primary(
|
||||
label: 'yes_i_took_a_break'.tr(),
|
||||
onPressed: () => context
|
||||
.read<CompleteDialogBloc>()
|
||||
.add(SelectBreakStatus(BreakStatus.positive)),
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'no_i_didnt_take_a_break'.tr(),
|
||||
onPressed: () => context
|
||||
.read<CompleteDialogBloc>()
|
||||
.add(SelectBreakStatus(BreakStatus.negative)),
|
||||
),
|
||||
],
|
||||
if (state.status == BreakStatus.positive) ...[
|
||||
KwButton.primary(
|
||||
disabled: state.breakTimeInputError != null,
|
||||
label: 'submit_break_time'.tr(),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, <String, dynamic>{
|
||||
'result': true,
|
||||
'details': ClockOutDetails.positive(
|
||||
state.startTime,
|
||||
state.endTime,
|
||||
)
|
||||
});
|
||||
},
|
||||
),
|
||||
cancelButton,
|
||||
],
|
||||
if (state.status == BreakStatus.negative) ...[
|
||||
KwButton.primary(
|
||||
disabled: state.selectedReason == null,
|
||||
label: 'submit_reason'.tr(),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, {
|
||||
'result': false,
|
||||
'details': ClockOutDetails.negative(
|
||||
state.selectedReason!, _textEditingController.text)
|
||||
});
|
||||
},
|
||||
),
|
||||
cancelButton,
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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_popup_menu.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
|
||||
|
||||
const reasons = {
|
||||
'unpredictable_workflows': 'unpredictable_workflows',
|
||||
'poor_time_management': 'poor_time_management',
|
||||
'lack_of_coverage_or_short_staff': 'lack_of_coverage_or_short_staff',
|
||||
'no_break_area': 'no_break_area',
|
||||
'other': 'other',
|
||||
};
|
||||
|
||||
class CompleteReasonInput extends StatelessWidget {
|
||||
final String? selectedReason;
|
||||
|
||||
const CompleteReasonInput({super.key, required this.selectedReason});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: buildReasonInput(context, selectedReason),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildReasonInput(BuildContext context, String? selectedReason) {
|
||||
return [
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'reason'.tr(),
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
KwPopupMenu(
|
||||
horizontalPadding: 40,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened, selectedReason);
|
||||
},
|
||||
menuItems: [
|
||||
...reasons.entries
|
||||
.map((e) => _buildMenuItem(context, e, selectedReason ?? ''))
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Container _buildMenuButton(bool isOpened, String? selectedReason) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isOpened ? AppColors.bgColorDark : AppColors.grayTintStroke,
|
||||
width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
reasons[selectedReason]?.tr() ?? 'select_reason_from_list'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selectedReason == null
|
||||
? AppColors.blackGray
|
||||
: AppColors.blackBlack),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwPopupMenuItem _buildMenuItem(BuildContext context,
|
||||
MapEntry<String, String> entry, String selectedReason) {
|
||||
return KwPopupMenuItem(
|
||||
title: entry.value.tr(),
|
||||
onTap: () {
|
||||
context.read<CompleteDialogBloc>().add(SelectReason(entry.key));
|
||||
},
|
||||
icon: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason != entry.key ? null : AppColors.bgColorDark,
|
||||
shape: BoxShape.circle,
|
||||
border: selectedReason == entry.key
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: selectedReason == entry.key
|
||||
? Center(
|
||||
child: Assets.images.icons.check.svg(
|
||||
height: 10,
|
||||
width: 10,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite, BlendMode.srcIn),
|
||||
))
|
||||
: null,
|
||||
),
|
||||
textStyle: AppTextStyles.bodySmallMed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.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/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/decline_dialog/decline_reason_dropdown.dart';
|
||||
|
||||
class DeclineDialogView extends StatefulWidget {
|
||||
const DeclineDialogView({super.key});
|
||||
|
||||
@override
|
||||
State<DeclineDialogView> createState() => _DeclineDialogViewState();
|
||||
}
|
||||
|
||||
class _DeclineDialogViewState extends State<DeclineDialogView> {
|
||||
String selectedReason = '';
|
||||
final _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'decline_alert'.tr(),
|
||||
style: AppTextStyles.headingH1.copyWith(height: 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'mention_reason_declining'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
DeclineReasonDropdown(
|
||||
selectedReason: selectedReason,
|
||||
onReasonSelected: (String reason) {
|
||||
setState(() {
|
||||
selectedReason = reason;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
controller: _textEditingController,
|
||||
minHeight: 144,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
radius: 12,
|
||||
title: 'additional_reasons'.tr(),
|
||||
hintText: 'enter_main_text'.tr(),
|
||||
),
|
||||
const Gap(24),
|
||||
_buttonGroup(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buttonGroup(
|
||||
BuildContext context,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
disabled: selectedReason.isEmpty,
|
||||
label: 'agree_and_close',
|
||||
onPressed: () {
|
||||
context.maybePop({
|
||||
'reason': selectedReason,
|
||||
'additionalReason': _textEditingController.text,
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'contact_admin',
|
||||
onPressed: () {
|
||||
//todo contact admin
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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_popup_menu.dart';
|
||||
|
||||
const reasons = {
|
||||
'health': 'health',
|
||||
'transportation': 'transportation',
|
||||
'personal': 'personal',
|
||||
'schedule_conflict': 'schedule_conflict',
|
||||
'other': 'other_specify',
|
||||
};
|
||||
|
||||
class DeclineReasonDropdown extends StatelessWidget {
|
||||
final String? selectedReason;
|
||||
final Function(String reason) onReasonSelected;
|
||||
|
||||
const DeclineReasonDropdown(
|
||||
{super.key,
|
||||
required this.selectedReason,
|
||||
required this.onReasonSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: buildReasonInput(context, selectedReason),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildReasonInput(BuildContext context, String? selectedReason) {
|
||||
return [
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'valid_reasons'.tr(),
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
KwPopupMenu(
|
||||
horizontalPadding: 40,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened, selectedReason);
|
||||
},
|
||||
menuItems: [
|
||||
...reasons.entries
|
||||
.map((e) => _buildMenuItem(context, e, selectedReason ?? ''))
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Container _buildMenuButton(bool isOpened, String? selectedReason) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isOpened ? AppColors.bgColorDark : AppColors.grayTintStroke,
|
||||
width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
reasons[selectedReason]?.tr() ?? 'select_reason_from_list'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selectedReason == null
|
||||
? AppColors.blackGray
|
||||
: AppColors.blackBlack),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwPopupMenuItem _buildMenuItem(BuildContext context,
|
||||
MapEntry<String, String> entry, String selectedReason) {
|
||||
return KwPopupMenuItem(
|
||||
title: entry.value.tr(),
|
||||
onTap: () {
|
||||
onReasonSelected(entry.key);
|
||||
},
|
||||
icon: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason != entry.key ? null : AppColors.bgColorDark,
|
||||
shape: BoxShape.circle,
|
||||
border: selectedReason == entry.key
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: selectedReason == entry.key
|
||||
? Center(
|
||||
child: Assets.images.icons.check.svg(
|
||||
height: 10,
|
||||
width: 10,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite, BlendMode.srcIn),
|
||||
))
|
||||
: null,
|
||||
),
|
||||
textStyle: AppTextStyles.bodySmallMed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/decline_dialog/decline_dialog_view.dart';
|
||||
|
||||
class ShiftDeclineDialog extends StatelessWidget {
|
||||
const ShiftDeclineDialog({super.key});
|
||||
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(
|
||||
BuildContext context) async {
|
||||
return await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => const ShiftDeclineDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DeclineDialogView();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
|
||||
|
||||
class GeocodingDialogs {
|
||||
static bool dialogAlreadyOpen = false;
|
||||
|
||||
static void showGeocodingErrorDialog(
|
||||
ShiftDetailsState state, BuildContext context) async{
|
||||
if (dialogAlreadyOpen) {
|
||||
return;
|
||||
}
|
||||
dialogAlreadyOpen = true;
|
||||
var future;
|
||||
switch (state.proximityState) {
|
||||
case GeofencingProximityState.tooFar:
|
||||
future = KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.warning,
|
||||
title: "You're too far",
|
||||
message: 'Please move closer to the designated location.',
|
||||
primaryButtonLabel: 'OK',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
dialogContext.router.maybePop();
|
||||
dialogAlreadyOpen = false;
|
||||
|
||||
},
|
||||
);
|
||||
break;
|
||||
case GeofencingProximityState.locationDisabled:
|
||||
future = KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'Location Disabled',
|
||||
message: 'Please enable location services to continue.',
|
||||
primaryButtonLabel: 'Go to Settings',
|
||||
onPrimaryButtonPressed: (dialogContext) async {
|
||||
dialogContext.router.maybePop();
|
||||
dialogAlreadyOpen = false;
|
||||
await Geolocator.openLocationSettings();
|
||||
|
||||
},
|
||||
);
|
||||
break;
|
||||
case GeofencingProximityState.goToSettings:
|
||||
future = KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.info,
|
||||
title: 'Permission Required',
|
||||
message: 'You need to allow location access in settings.',
|
||||
primaryButtonLabel: 'Open Settings',
|
||||
onPrimaryButtonPressed: (dialogContext) async {
|
||||
dialogContext.maybePop();
|
||||
dialogAlreadyOpen = false;
|
||||
|
||||
await Geolocator.openLocationSettings();
|
||||
|
||||
},
|
||||
);
|
||||
break;
|
||||
case GeofencingProximityState.onlyInUse:
|
||||
future = KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.info,
|
||||
title: 'Track "All the time" required',
|
||||
message:
|
||||
'To ensure accurate time tracking, we need access to your location. Time tracking will automatically stop if you move more than 500 meters away from your assigned work location. '
|
||||
'Please grant “Allow all the time” access to your location. '
|
||||
'Go to Settings → Permissions → Location and select “Allow all the time”.',
|
||||
primaryButtonLabel: 'Open Settings',
|
||||
onPrimaryButtonPressed: (dialogContext) async {
|
||||
dialogContext.maybePop();
|
||||
dialogAlreadyOpen = false;
|
||||
await Geolocator.openLocationSettings();
|
||||
},
|
||||
);
|
||||
break;
|
||||
case GeofencingProximityState.permissionDenied:
|
||||
future = KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'Permission Denied',
|
||||
message:
|
||||
'You have denied location access. Please allow it manually.',
|
||||
primaryButtonLabel: 'OK',
|
||||
onPrimaryButtonPressed: (dialogContext) async {
|
||||
dialogContext.maybePop();
|
||||
dialogAlreadyOpen = false;
|
||||
await Geolocator.openAppSettings();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
dialogAlreadyOpen = false;
|
||||
break;
|
||||
}
|
||||
await future;
|
||||
dialogAlreadyOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/geocoding_dialogs.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_buttons_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_clock_time_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_planing_duration_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_start_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_info_time_row/shift_info_total_duration_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_key_responsibilities_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_location_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_manage_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_payment_step_card_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_rating_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_timer_widgets/shift_timer_card_widget.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ShiftDetailsScreen extends StatefulWidget implements AutoRouteWrapper {
|
||||
final ShiftEntity shift;
|
||||
|
||||
const ShiftDetailsScreen({super.key, required this.shift});
|
||||
|
||||
@override
|
||||
State<ShiftDetailsScreen> createState() => _ShiftDetailsScreenState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider<ShiftDetailsBloc>(
|
||||
create: (context) =>
|
||||
ShiftDetailsBloc()..add(ShiftDetailsInitialEvent(shift: shift)),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShiftDetailsScreenState extends State<ShiftDetailsScreen> with WidgetsBindingObserver{
|
||||
final OverlayPortalController _controller = OverlayPortalController();
|
||||
var expanded = false;
|
||||
|
||||
String _getTitle(EventShiftRoleStaffStatus status) {
|
||||
switch (status) {
|
||||
//do not use enum name. Its need for future localization;
|
||||
case EventShiftRoleStaffStatus.assigned:
|
||||
return 'assigned';
|
||||
case EventShiftRoleStaffStatus.confirmed:
|
||||
return 'confirmed';
|
||||
case EventShiftRoleStaffStatus.ongoing:
|
||||
return 'active';
|
||||
case EventShiftRoleStaffStatus.completed:
|
||||
return 'completed';
|
||||
case EventShiftRoleStaffStatus.declineByStaff:
|
||||
return 'declined';
|
||||
case EventShiftRoleStaffStatus.canceledByStaff:
|
||||
case EventShiftRoleStaffStatus.canceledByBusiness:
|
||||
case EventShiftRoleStaffStatus.canceledByAdmin:
|
||||
case EventShiftRoleStaffStatus.requestedReplace:
|
||||
return 'canceled';
|
||||
}
|
||||
}
|
||||
|
||||
bool _showButtons(EventShiftRoleStaffStatus status) =>
|
||||
status == EventShiftRoleStaffStatus.assigned || status == EventShiftRoleStaffStatus.confirmed;
|
||||
|
||||
bool showTimer(EventShiftRoleStaffStatus status) =>
|
||||
status == EventShiftRoleStaffStatus.confirmed ||
|
||||
status == EventShiftRoleStaffStatus.ongoing;
|
||||
|
||||
void _listenHandler(BuildContext context, ShiftDetailsState state) {
|
||||
|
||||
if (state.needPop) {
|
||||
context.router.maybePop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isLoading) {
|
||||
_controller.show();
|
||||
} else {
|
||||
_controller.hide();
|
||||
}
|
||||
|
||||
if(state.errorMessage!=null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage!),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.proximityState == GeofencingProximityState.none) return;
|
||||
GeocodingDialogs.showGeocodingErrorDialog(state, context);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
expanded = widget.shift.status != EventShiftRoleStaffStatus.completed;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(const ShiftCheckGeocodingEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ShiftDetailsBloc, ShiftDetailsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.shiftViewModel != current.shiftViewModel,
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isLoading != current.isLoading ||
|
||||
previous.proximityState != current.proximityState || current.errorMessage!=null,
|
||||
listener: _listenHandler,
|
||||
builder: (context, state) {
|
||||
var viewModel = state.shiftViewModel;
|
||||
var status = viewModel.status;
|
||||
|
||||
return KwLoadingOverlay(
|
||||
controller: _controller,
|
||||
child: Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: '${_getTitle(status).tr()} ${'Shift'.tr()}',
|
||||
showNotification: true,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
children: [
|
||||
ShiftItemWidget(
|
||||
bottomPadding: 0,
|
||||
viewModel,
|
||||
isDetailsMode: true,
|
||||
),
|
||||
if (showTimer(viewModel.status))
|
||||
const ShiftTimerCardWidget(),
|
||||
_buildShiftTimeInfo(viewModel),
|
||||
if (status == EventShiftRoleStaffStatus.completed)
|
||||
ShiftPaymentStepCardWidget(viewModel: viewModel),
|
||||
if (status == EventShiftRoleStaffStatus.completed &&
|
||||
viewModel.rating != null)
|
||||
ShiftRatingWidget(viewModel: viewModel),
|
||||
if (status != EventShiftRoleStaffStatus.ongoing)
|
||||
ShiftLocationWidget(viewModel: viewModel),
|
||||
ShiftManageWidget(managers: viewModel.managers),
|
||||
if (viewModel.additionalInfo != null)
|
||||
ShiftKeyResponsibilitiesWidget(
|
||||
text: viewModel.additionalInfo!,
|
||||
expandable:
|
||||
status == EventShiftRoleStaffStatus.completed,
|
||||
isExpanded: expanded,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
expanded = !expanded;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(height: MediaQuery.sizeOf(context).height / 3),
|
||||
],
|
||||
),
|
||||
if (_showButtons(viewModel.status))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ShiftButtonsWidget(
|
||||
viewModel.status,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShiftTimeInfo(ShiftEntity shiftViewModel) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
shiftViewModel.status == EventShiftRoleStaffStatus.completed
|
||||
? ShiftInfoClockTimeWidget(viewModel: shiftViewModel)
|
||||
: ShiftInfoStartWidget(viewModel: shiftViewModel),
|
||||
const Gap(8),
|
||||
shiftViewModel.status == EventShiftRoleStaffStatus.completed
|
||||
? ShiftInfoTotalDurationWidget(viewModel: shiftViewModel)
|
||||
: ShiftInfoPlaningDurationWidget(viewModel: shiftViewModel)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_tabs.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_event.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_state.dart';
|
||||
import 'package:krow/features/shifts/domain/services/shift_completer_service.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_widget.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ShiftsListMainScreen extends StatefulWidget {
|
||||
const ShiftsListMainScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ShiftsListMainScreen> createState() => _ShiftsListMainScreenState();
|
||||
}
|
||||
|
||||
class _ShiftsListMainScreenState extends State<ShiftsListMainScreen> {
|
||||
late AppLifecycleListener _appLifecycleListener;
|
||||
var dialogOpened = false;
|
||||
final List<String> tabs = [
|
||||
'assigned',
|
||||
'confirmed',
|
||||
'active',
|
||||
'completed',
|
||||
'canceled'
|
||||
];
|
||||
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
BlocProvider.of<ShiftsBloc>(context).add(
|
||||
LoadTabShiftEvent(
|
||||
status: BlocProvider.of<ShiftsBloc>(context).state.tabIndex),
|
||||
);
|
||||
BlocProvider.of<ShiftsBloc>(context).add(
|
||||
const ReloadMissingBreakShift(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_appLifecycleListener = AppLifecycleListener(onStateChange: (state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
BlocProvider.of<ShiftsBloc>(context).add(
|
||||
const ReloadMissingBreakShift(),
|
||||
);
|
||||
}
|
||||
});
|
||||
_scrollController = ScrollController();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_appLifecycleListener.dispose();
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.atEdge) {
|
||||
if (_scrollController.position.pixels != 0) {
|
||||
BlocProvider.of<ShiftsBloc>(context).add(
|
||||
LoadMoreShiftEvent(
|
||||
status: BlocProvider.of<ShiftsBloc>(context).state.tabIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ShiftsBloc, ShiftsState>(
|
||||
listenWhen: (oldState, state) {
|
||||
return state.missedShifts.isNotEmpty &&
|
||||
(oldState.missedShifts.isEmpty ||
|
||||
oldState.missedShifts.first.id != state.missedShifts.first.id);
|
||||
},
|
||||
listener: (context, state) async {
|
||||
if (state.missedShifts.isNotEmpty) {
|
||||
if(dialogOpened) return;
|
||||
dialogOpened = true;
|
||||
await ShiftCompleterService().startCompleteProcess(
|
||||
context,
|
||||
canSkip: false,
|
||||
state.missedShifts.first,
|
||||
onComplete: () {},
|
||||
);
|
||||
dialogOpened = false;
|
||||
|
||||
BlocProvider.of<ShiftsBloc>(context).add(
|
||||
const ReloadMissingBreakShift(),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
List<ShiftEntity> items = state.tabs[state.tabIndex]!.items;
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'your_shifts'.tr(),
|
||||
showNotification: true,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
onRefresh: () async {
|
||||
BlocProvider.of<ShiftsBloc>(context)
|
||||
.add(const ReloadMissingBreakShift());
|
||||
BlocProvider.of<ShiftsBloc>(context)
|
||||
.add(LoadTabShiftEvent(status: state.tabIndex));
|
||||
},
|
||||
controller: _scrollController,
|
||||
upperWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
KwTabBar(
|
||||
key: const Key('shifts_tab_bar'),
|
||||
tabs: tabs.map((e) => e.tr()).toList(),
|
||||
onTap: (index) {
|
||||
BlocProvider.of<ShiftsBloc>(context)
|
||||
.add(ShiftsTabChangedEvent(tabIndex: index));
|
||||
}),
|
||||
const Gap(16),
|
||||
if (state.tabs[state.tabIndex]!.isLoading &&
|
||||
state.tabs[state.tabIndex]!.items.isEmpty)
|
||||
..._buildListLoading(),
|
||||
if (!state.tabs[state.tabIndex]!.isLoading && items.isEmpty)
|
||||
..._emptyListWidget(),
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
BlocProvider.of<ShiftsBloc>(context)
|
||||
.add(LoadTabShiftEvent(status: state.tabIndex));
|
||||
},
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ShiftItemWidget(
|
||||
items[index],
|
||||
bottomPadding: 12,
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
ShiftDetailsRoute(shift: items[index]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
lowerWidget: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildListLoading() {
|
||||
return [
|
||||
const Gap(116),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _emptyListWidget() {
|
||||
return [
|
||||
const Gap(100),
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Center(child: Assets.images.icons.xCircle.svg()),
|
||||
),
|
||||
const Gap(24),
|
||||
Text(
|
||||
'you_currently_have_no_shifts'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.headingH2,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_event.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ShiftsFlowScreen extends StatelessWidget {
|
||||
const ShiftsFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(providers: [
|
||||
BlocProvider<ShiftsBloc>(
|
||||
create: (context) => ShiftsBloc()..add(const ShiftsInitialEvent()),
|
||||
),
|
||||
], child: const AutoRouter());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/cancel_dialog/shift_cancel_dialog.dart';
|
||||
import 'package:krow/features/shifts/presentation/dialogs/decline_dialog/shift_decline_dialog.dart';
|
||||
|
||||
class ShiftButtonsWidget extends StatelessWidget {
|
||||
final EventShiftRoleStaffStatus status;
|
||||
|
||||
const ShiftButtonsWidget(this.status, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: const Alignment(0, -0.5),
|
||||
colors: [
|
||||
AppColors.grayWhite,
|
||||
AppColors.grayWhite.withAlpha(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: status == EventShiftRoleStaffStatus.assigned
|
||||
? [
|
||||
KwButton.primary(
|
||||
label: 'accept_shift'.tr(),
|
||||
onPressed: () {
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||
const ShiftConfirmEvent(),
|
||||
);
|
||||
},
|
||||
fit: KwButtonFit.expanded,
|
||||
),
|
||||
const Gap(12),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'decline_shift'.tr(),
|
||||
onPressed: () async {
|
||||
var result =
|
||||
await ShiftDeclineDialog.showCustomDialog(context);
|
||||
if (result != null && context.mounted) {
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||
ShiftDeclineEvent(
|
||||
result['reason'], result['additionalReason']),
|
||||
);
|
||||
}
|
||||
}).copyWith(color: AppColors.statusError)
|
||||
]
|
||||
: [
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'cancel_shift'.tr(),
|
||||
onPressed: () async {
|
||||
var result =
|
||||
await ShiftCancelDialog.showCustomDialog(context);
|
||||
if (result != null && context.mounted) {
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||
ShiftCancelEvent(
|
||||
result['reason'], result['additionalReason']),
|
||||
);
|
||||
}
|
||||
}).copyWith(color: AppColors.statusError)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.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/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftInfoClockTimeWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftInfoClockTimeWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var clockIn = DateFormat('h:mma','en')
|
||||
.format(viewModel.clockIn ?? DateTime.now())
|
||||
.toLowerCase();
|
||||
var clockOut = DateFormat('h:mma','en')
|
||||
.format(viewModel.clockOut ?? DateTime.now())
|
||||
.toLowerCase();
|
||||
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${'clock_in_1'.tr()} - ${'clock_out_1'.tr()}'.toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText)),
|
||||
const Gap(16),
|
||||
Text(clockIn, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('clock_in_1'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(12),
|
||||
Text(clockOut, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('clock_out_1'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.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/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftInfoPlaningDurationWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftInfoPlaningDurationWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var formattedDuration =
|
||||
_format(viewModel.endDate.difference(viewModel.startDate).inMinutes);
|
||||
var formattedBreak = _format(viewModel.planingBreakTime!);
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('duration'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText)),
|
||||
const Gap(16),
|
||||
Text(formattedDuration, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('shift_duration'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(12),
|
||||
Text(formattedBreak, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('break_duration'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _format(int? duration) {
|
||||
if (duration == null) return '0 hours';
|
||||
var hours = duration ~/ 60;
|
||||
var minutes = duration % 60;
|
||||
var hoursStr = 'hours_1'.tr(namedArgs: {'hours':hours.toString(),'plural':hours == 1 ? '' : 's'});
|
||||
return minutes == 0 ? hoursStr : 'hours_minutes'.tr(namedArgs: {'hours':hours.toString(),'minutes':minutes.toString()});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.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/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftInfoStartWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftInfoStartWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var dateTime = viewModel.startDate;
|
||||
|
||||
var date = DateFormat('MMMM d', context.locale.languageCode).format(dateTime);
|
||||
var time = DateFormat('h:mma', 'en').format(dateTime).toLowerCase();
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('start'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText)),
|
||||
const Gap(16),
|
||||
Text(date, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('date'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(12),
|
||||
Text(time, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('time'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.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/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftInfoTotalDurationWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftInfoTotalDurationWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var formattedDuration;
|
||||
if (viewModel.clockOut == null || viewModel.clockIn == null) {
|
||||
formattedDuration = '00:00:00';
|
||||
}
|
||||
formattedDuration = _format(viewModel.clockOut
|
||||
?.difference(viewModel.clockIn ?? DateTime.now())
|
||||
.inSeconds ??
|
||||
0);
|
||||
var formattedBreak = _format(viewModel.totalBreakTime!);
|
||||
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('total_time_breaks'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText)),
|
||||
const Gap(16),
|
||||
Text(formattedBreak, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('break_hours'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(12),
|
||||
Text(formattedDuration, style: AppTextStyles.headingH3),
|
||||
const Gap(6),
|
||||
Text('total_hours'.tr(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _format(int duration) {
|
||||
var hours = (duration ~/ 3600).toString().padLeft(2, '0');
|
||||
var minutes = ((duration % 3600) ~/ 60).toString().padLeft(2, '0');
|
||||
var seconds = (duration % 60).toString().padLeft(2, '0');
|
||||
return '$hours:$minutes:$seconds';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.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/features/shifts/data/models/cancellation_reason.dart';
|
||||
|
||||
class ShiftItemCanceledWidget extends StatelessWidget {
|
||||
final CancellationReason? reason;
|
||||
final bool canceledByUser;
|
||||
|
||||
const ShiftItemCanceledWidget(
|
||||
{super.key, required this.reason, required this.canceledByUser});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
decoration:
|
||||
KwBoxDecorations.primaryLight8.copyWith(color: AppColors.tintBlue),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'your_shift_canceled'.tr(),
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'please_review_reason'.tr(),
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.primaryBlue)),
|
||||
const Gap(8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'canceled_by'.tr(),
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.tintDarkBlue),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
canceledByUser ? 'user'.tr() : 'admin'.tr(),
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: KwBoxDecorations.primaryLight6,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${'reason'.tr()}:',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(reasonToString(reason).tr(),
|
||||
style: AppTextStyles.bodyMediumMed),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String reasonToString(CancellationReason? reason) {
|
||||
switch (reason) {
|
||||
case CancellationReason.sickLeave:
|
||||
return 'sick_leave';
|
||||
case CancellationReason.vacation:
|
||||
return 'vacation';
|
||||
case CancellationReason.other:
|
||||
return 'Other';
|
||||
case CancellationReason.health:
|
||||
return 'health';
|
||||
case CancellationReason.transportation:
|
||||
return 'transportation';
|
||||
case CancellationReason.personal:
|
||||
return 'personal';
|
||||
case CancellationReason.scheduleConflict:
|
||||
return 'schedule_conflict';
|
||||
case null:
|
||||
return 'other';
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftAdditionalDetails extends StatefulWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftAdditionalDetails({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
State<ShiftAdditionalDetails> createState() => _ShiftAdditionalDetailsState();
|
||||
}
|
||||
|
||||
class _ShiftAdditionalDetailsState extends State<ShiftAdditionalDetails> {
|
||||
bool expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.viewModel.additionalData?.isEmpty ?? true) {
|
||||
return const Gap(12);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Gap(12),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
height: 1,
|
||||
color: AppColors.grayTintStroke,
|
||||
),
|
||||
const Gap(12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
expanded = !expanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
height: 16,
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('additional_details'.tr(),
|
||||
style: AppTextStyles.captionReg),
|
||||
const Spacer(),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
turns: expanded ? -0.5 : 0,
|
||||
child: Assets.images.icons.caretDown.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackBlack, BlendMode.srcIn))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.viewModel.additionalData?.isNotEmpty ?? false) ...[
|
||||
const Gap(12),
|
||||
_buildExpandedDetails(),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildExpandedDetails() {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
height: expanded ? widget.viewModel.additionalData!.length * 28 + 12 : 0,
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.graySecondaryFrame,
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...widget.viewModel.additionalData?.map(
|
||||
(e) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(e.name ?? '',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Spacer(),
|
||||
Text('yes'.tr(), style: AppTextStyles.bodySmallReg),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
[],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_status_label_widget.dart';
|
||||
|
||||
class ShiftItemHeaderWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftItemHeaderWidget(this.viewModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
viewModel.imageUrl.isNotEmpty
|
||||
? ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: viewModel.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
height: 48,
|
||||
width: 48,
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
ShiftStatusLabelWidget(viewModel),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
viewModel.skillName,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)),
|
||||
const Gap(24),
|
||||
Text(
|
||||
'\$${viewModel.rate}/h',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
viewModel.eventName,
|
||||
style:
|
||||
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
Timer? shiftListTimer;
|
||||
|
||||
class ShiftOngoingCounterWidget extends StatefulWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftOngoingCounterWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
State<ShiftOngoingCounterWidget> createState() =>
|
||||
_ShiftOngoingCounterWidgetState();
|
||||
}
|
||||
|
||||
class _ShiftOngoingCounterWidgetState extends State<ShiftOngoingCounterWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
shiftListTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var duration =
|
||||
DateTime.now().difference(widget.viewModel.clockIn ?? DateTime.now());
|
||||
var hours = duration.inHours.remainder(24).abs().toString().padLeft(2, '0');
|
||||
var minutes =
|
||||
duration.inMinutes.remainder(60).abs().toString().padLeft(2, '0');
|
||||
var seconds =
|
||||
duration.inSeconds.remainder(60).abs().toString().padLeft(2, '0');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
margin: const EdgeInsets.only(top: 24, left: 24, right: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tintGreen,
|
||||
border: Border.all(color: AppColors.tintDarkGreen),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildTimeText('hours'.tr(), hours),
|
||||
_divider(),
|
||||
_buildTimeText('minutes'.tr(), minutes),
|
||||
_divider(),
|
||||
_buildTimeText('seconds'.tr(), seconds),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() {
|
||||
return SizedBox(
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 24,
|
||||
width: 1,
|
||||
color: AppColors.tintDarkGreen,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeText(String label, String time) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(time,
|
||||
style: AppTextStyles.headingH3
|
||||
.copyWith(color: AppColors.statusSuccess)),
|
||||
const Gap(6),
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.statusSuccess),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftPlaceAndTimeWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftPlaceAndTimeWidget(this.viewModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final timeFormat = DateFormat('MMMM d, h:mma', context.locale.languageCode);
|
||||
return Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 28,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: MediaQuery.of(context).size.width - 32),
|
||||
child: IntrinsicWidth(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Assets.images.icons.location.svg(width: 16, height: 16),
|
||||
const Gap(4),
|
||||
Text(
|
||||
viewModel.locationName,
|
||||
style: AppTextStyles.bodySmallReg,
|
||||
),
|
||||
const Spacer(),
|
||||
const Gap(12),
|
||||
Assets.images.icons.calendar.svg(width: 16, height: 16),
|
||||
const Gap(4),
|
||||
Text(
|
||||
timeFormat.format(viewModel.startDate),
|
||||
style: AppTextStyles.bodySmallReg,
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 28,
|
||||
width: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
AppColors.grayPrimaryFrame.withAlpha(50),
|
||||
AppColors.grayPrimaryFrame.withAlpha(255),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftStatusLabelWidget extends StatefulWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftStatusLabelWidget(this.viewModel, {super.key});
|
||||
|
||||
@override
|
||||
State<ShiftStatusLabelWidget> createState() => _ShiftStatusLabelWidgetState();
|
||||
}
|
||||
|
||||
class _ShiftStatusLabelWidgetState extends State<ShiftStatusLabelWidget> {
|
||||
Color getColor() {
|
||||
switch (widget.viewModel.status) {
|
||||
case EventShiftRoleStaffStatus.assigned:
|
||||
return AppColors.primaryBlue;
|
||||
case EventShiftRoleStaffStatus.confirmed:
|
||||
return AppColors.statusWarning;
|
||||
case EventShiftRoleStaffStatus.ongoing:
|
||||
return AppColors.statusSuccess;
|
||||
case EventShiftRoleStaffStatus.completed:
|
||||
return AppColors.bgColorDark;
|
||||
case EventShiftRoleStaffStatus.canceledByAdmin:
|
||||
case EventShiftRoleStaffStatus.canceledByBusiness:
|
||||
case EventShiftRoleStaffStatus.canceledByStaff:
|
||||
case EventShiftRoleStaffStatus.requestedReplace:
|
||||
case EventShiftRoleStaffStatus.declineByStaff:
|
||||
return AppColors.statusError;
|
||||
}
|
||||
}
|
||||
|
||||
String getText() {
|
||||
switch (widget.viewModel.status) {
|
||||
case EventShiftRoleStaffStatus.assigned:
|
||||
return _getAssignedAgo();
|
||||
case EventShiftRoleStaffStatus.confirmed:
|
||||
return _getStartIn();
|
||||
case EventShiftRoleStaffStatus.ongoing:
|
||||
return 'ongoing'.tr();
|
||||
case EventShiftRoleStaffStatus.completed:
|
||||
return 'completed'.tr();
|
||||
case EventShiftRoleStaffStatus.declineByStaff:
|
||||
return 'declined'.tr();
|
||||
case EventShiftRoleStaffStatus.canceledByAdmin:
|
||||
case EventShiftRoleStaffStatus.canceledByBusiness:
|
||||
case EventShiftRoleStaffStatus.canceledByStaff:
|
||||
case EventShiftRoleStaffStatus.requestedReplace:
|
||||
return 'canceled'.tr();
|
||||
}
|
||||
}
|
||||
|
||||
String _getStartIn() {
|
||||
var duration = widget.viewModel.startDate.difference(DateTime.now());
|
||||
var startIn = '';
|
||||
if (duration.inMinutes < 0) {
|
||||
return 'started'.tr();
|
||||
}
|
||||
if (duration.inDays > 0) {
|
||||
startIn = '${duration.inDays}d ${duration.inHours.remainder(24)}h';
|
||||
} else if (duration.inHours.abs() > 0) {
|
||||
startIn = '${duration.inHours}h ${duration.inMinutes.remainder(60)}min';
|
||||
} else {
|
||||
startIn = '${duration.inMinutes}min';
|
||||
}
|
||||
return 'starts_in'.tr(namedArgs: {'time': startIn});
|
||||
}
|
||||
|
||||
String _getAssignedAgo() {
|
||||
var duration = DateTime.now().difference(widget.viewModel.assignedDate);
|
||||
var timeAgo = '';
|
||||
if (duration.inDays > 0) {
|
||||
timeAgo = '${duration.inDays}d ago';
|
||||
} else if (duration.inHours > 0) {
|
||||
timeAgo = '${duration.inHours}h ago';
|
||||
} else {
|
||||
timeAgo = '${duration.inMinutes}m ago';
|
||||
}
|
||||
return 'assigned_ago'.tr(namedArgs: {'time': timeAgo});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 24,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: getColor(),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
getText(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite, height: 0.7),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/data/models/event_tag.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftTagsWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
final bool scrollable;
|
||||
|
||||
const ShiftTagsWidget(this.viewModel, {this.scrollable = false, super.key});
|
||||
|
||||
final textColors = const {
|
||||
'1': AppColors.statusWarningBody,
|
||||
'2': AppColors.statusError,
|
||||
'3': AppColors.statusSuccess,
|
||||
};
|
||||
final backGroundsColors = const {
|
||||
'1': AppColors.tintYellow,
|
||||
'2': AppColors.tintRed,
|
||||
'3': AppColors.tintGreen,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (viewModel.tags == null || viewModel.tags!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
const Gap(12),
|
||||
if (scrollable) _buildScrollableList(),
|
||||
if (!scrollable) _buildChips(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScrollableList() {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||
itemCount: viewModel.tags?.length ?? 0,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildTag(viewModel.tags![index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChips() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||
child: Wrap(
|
||||
runSpacing: 4,
|
||||
children: viewModel.tags!.map((e) => _buildTag(e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(EventTag shiftTage) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: backGroundsColors[shiftTage.id]??AppColors.tintGreen,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 28,
|
||||
width: 28,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.grayWhite,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.eye.svg(
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(
|
||||
textColors[shiftTage.id] ?? AppColors.statusWarningBody,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
shiftTage.name,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: textColors[shiftTage.id]??AppColors.statusSuccess),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/widgets/shift_payment_step_widget.dart';
|
||||
import 'package:krow/core/presentation/widgets/shift_total_time_spend_widget.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_canceled_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_additional_details_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_item_header_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_ongoing_counter_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_place_and_time_widget.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_item_components/shift_tags_widget.dart';
|
||||
|
||||
class ShiftItemWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
final bool isDetailsMode;
|
||||
final VoidCallback? onPressed;
|
||||
final double bottomPadding;
|
||||
|
||||
const ShiftItemWidget(this.viewModel,
|
||||
{this.bottomPadding = 0,
|
||||
this.onPressed,
|
||||
this.isDetailsMode = false,
|
||||
super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var showPlaceAndTime = !isDetailsMode &&
|
||||
(viewModel.status == EventShiftRoleStaffStatus.assigned ||
|
||||
viewModel.status == EventShiftRoleStaffStatus.confirmed);
|
||||
|
||||
var showTags =
|
||||
isDetailsMode || viewModel.status == EventShiftRoleStaffStatus.assigned;
|
||||
|
||||
var showOngoingCounter =
|
||||
!isDetailsMode && viewModel.status == EventShiftRoleStaffStatus.ongoing;
|
||||
|
||||
var showTotalTimeSpend = !isDetailsMode &&
|
||||
viewModel.status == EventShiftRoleStaffStatus.completed;
|
||||
|
||||
var showAdditionalDetails =
|
||||
isDetailsMode || viewModel.status == EventShiftRoleStaffStatus.ongoing;
|
||||
|
||||
var canceled =
|
||||
viewModel.status == EventShiftRoleStaffStatus.canceledByStaff ||
|
||||
viewModel.status == EventShiftRoleStaffStatus.canceledByBusiness ||
|
||||
viewModel.status == EventShiftRoleStaffStatus.canceledByAdmin;
|
||||
|
||||
|
||||
return GestureDetector(
|
||||
onTap: canceled?null:onPressed,
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
margin: EdgeInsets.only(bottom: bottomPadding, left: 16, right: 16),
|
||||
padding:
|
||||
EdgeInsets.only(top: 24, bottom: showAdditionalDetails ? 0 : 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShiftItemHeaderWidget(viewModel),
|
||||
if (showPlaceAndTime) ShiftPlaceAndTimeWidget(viewModel),
|
||||
if (showTags)
|
||||
ShiftTagsWidget(viewModel, scrollable: !isDetailsMode),
|
||||
if (showOngoingCounter)
|
||||
ShiftOngoingCounterWidget(viewModel: viewModel),
|
||||
if (showTotalTimeSpend)
|
||||
ShiftTotalTimeSpendWidget(
|
||||
startTime: viewModel.clockIn?? DateTime.now(),
|
||||
endTime: viewModel.clockOut?? viewModel.clockIn??DateTime.now(),
|
||||
totalBreakTime: viewModel.totalBreakTime ?? 0,
|
||||
),
|
||||
if (showTotalTimeSpend)
|
||||
const ShiftPaymentStepWidget(
|
||||
currentIndex: 1,
|
||||
),
|
||||
if (showAdditionalDetails)
|
||||
ShiftAdditionalDetails(viewModel: viewModel),
|
||||
if (canceled)
|
||||
ShiftItemCanceledWidget(
|
||||
reason: viewModel.cancellationReason,
|
||||
canceledByUser: viewModel.status ==
|
||||
EventShiftRoleStaffStatus.canceledByStaff),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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';
|
||||
|
||||
class ShiftKeyResponsibilitiesWidget extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
final bool isExpanded;
|
||||
final bool expandable;
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ShiftKeyResponsibilitiesWidget(
|
||||
{super.key,
|
||||
required this.text,
|
||||
required this.expandable,
|
||||
required this.isExpanded,
|
||||
required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
height: isExpanded ? null : 40,
|
||||
margin: const EdgeInsets.only(top: 8, left: 16, right: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
expandable ? expandableTitle() : fixedTitle(),
|
||||
const Gap(16),
|
||||
Text(
|
||||
text,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget fixedTitle() {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('${'key_responsibilities'.tr()}:'.toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget expandableTitle() {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('additional_information'.tr().toUpperCase(),
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
],
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
turns: isExpanded ? -0.5 : 0,
|
||||
child: Assets.images.icons.caretDown.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackBlack, BlendMode.srcIn))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:krow/core/application/common/map_utils.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftLocationWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftLocationWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8, left: 16, right: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'location'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
viewModel.locationName,
|
||||
style: AppTextStyles.headingH3.copyWith(height: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'get_direction'.tr(),
|
||||
leftIcon: Assets.images.icons.routing,
|
||||
onPressed: () {
|
||||
MapUtils.openMapByLatLon(
|
||||
viewModel.locationLat,
|
||||
viewModel.locationLon,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (viewModel.status != EventShiftRoleStaffStatus.completed)
|
||||
_buildMap(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMap() {
|
||||
if (viewModel.locationLat == 0 && viewModel.locationLon == 0) {
|
||||
return Container(
|
||||
height: 166,
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
height: 166,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: GoogleMap(
|
||||
rotateGesturesEnabled: false,
|
||||
compassEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
scrollGesturesEnabled: false,
|
||||
zoomGesturesEnabled: false,
|
||||
tiltGesturesEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('1'),
|
||||
position: LatLng(viewModel.locationLat, viewModel.locationLon),
|
||||
),
|
||||
},
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(viewModel.locationLat, viewModel.locationLon),
|
||||
zoom: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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/shifts/domain/shift_entity.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:whatsapp_unilink/whatsapp_unilink.dart';
|
||||
|
||||
class ShiftManageWidget extends StatelessWidget {
|
||||
final List<ShiftManager> managers;
|
||||
|
||||
const ShiftManageWidget({super.key, required this.managers});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (managers.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'manage_contact_details'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText),
|
||||
),
|
||||
const Gap(16),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: managers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildManager(managers[index]);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManager(ShiftManager manager) {
|
||||
return Row(
|
||||
children: [
|
||||
if(manager.imageUrl.isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 24.0,
|
||||
backgroundColor: AppColors.grayWhite,
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
manager.imageUrl), // Replace with your image asset
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
manager.name,
|
||||
style: AppTextStyles.bodyMediumSmb,
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
manager.phoneNumber,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Row(
|
||||
children: [
|
||||
KwButton.outlinedPrimary(
|
||||
rightIcon: Assets.images.icons.whatsapp,
|
||||
fit: KwButtonFit.circular,
|
||||
onPressed: () {
|
||||
var link = WhatsAppUnilink(
|
||||
phoneNumber: manager.phoneNumber,
|
||||
text: '',
|
||||
);
|
||||
launchUrlString(link.toString());
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'call'.tr(),
|
||||
rightIcon: Assets.images.icons.call,
|
||||
onPressed: () {
|
||||
launchUrlString('tel:${manager.phoneNumber}');
|
||||
},
|
||||
).copyWith(
|
||||
color: AppColors.tintDarkGreen,
|
||||
textColors: AppColors.statusSuccess),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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/shift_payment_step_widget.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftPaymentStepCardWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftPaymentStepCardWidget({super.key, required this.viewModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'payment_status'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText),
|
||||
),
|
||||
const ShiftPaymentStepWidget(currentIndex: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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/features/shifts/domain/shift_entity.dart';
|
||||
|
||||
class ShiftRatingWidget extends StatelessWidget {
|
||||
final ShiftEntity viewModel;
|
||||
|
||||
const ShiftRatingWidget({super.key, required this.viewModel});
|
||||
|
||||
final double maxRating = 5.0; // Maximum rating, e.g., 5
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'clients_rate'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText),
|
||||
),
|
||||
const Gap(4),
|
||||
Center(
|
||||
child: Text(viewModel.rating?.rating.toStringAsFixed(1) ?? '0.0',
|
||||
style: AppTextStyles.headingH0),
|
||||
),
|
||||
const Gap(4),
|
||||
_buildRating(viewModel.rating?.rating ?? 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildRating(double rating) {
|
||||
List<Widget> stars = [];
|
||||
for (int i = 1; i <= maxRating; i++) {
|
||||
if (i <= rating) {
|
||||
stars.add(Assets.images.icons.ratingStar.star.svg());
|
||||
} else if (i - rating < 1) {
|
||||
stars.add(Assets.images.icons.ratingStar.starHalf.svg());
|
||||
} else {
|
||||
stars.add(Assets.images.icons.ratingStar.starEmpty.svg());
|
||||
}
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [const Gap(28), ...stars, const Gap(28)],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/map_utils.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
import 'package:krow/features/shifts/domain/blocs/shift_deteils_bloc/shift_details_bloc.dart';
|
||||
import 'package:krow/features/shifts/domain/services/shift_completer_service.dart';
|
||||
import 'package:krow/features/shifts/domain/shift_entity.dart';
|
||||
import 'package:krow/features/shifts/presentation/widgets/shift_timer_widgets/shift_timer_widget.dart';
|
||||
|
||||
class ShiftTimerCardWidget extends StatelessWidget {
|
||||
const ShiftTimerCardWidget({super.key});
|
||||
|
||||
bool _hasBorder(EventShiftRoleStaffStatus status) =>
|
||||
status == EventShiftRoleStaffStatus.ongoing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ShiftDetailsBloc, ShiftDetailsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isToFar != current.isToFar ||
|
||||
previous.shiftViewModel != current.shiftViewModel,
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(top: 8, left: 16, right: 16),
|
||||
decoration: KwBoxDecorations.primaryLight12.copyWith(
|
||||
border: _hasBorder(state.shiftViewModel.status)
|
||||
? Border.all(color: AppColors.tintDarkGreen, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'timer'.tr().toUpperCase(),
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.blackCaptionText),
|
||||
),
|
||||
const Gap(16),
|
||||
ShiftTimerWidget(
|
||||
shiftStatus: state.shiftViewModel.status,
|
||||
clockIn: state.shiftViewModel.clockIn,
|
||||
clockOut: state.shiftViewModel.clockOut,
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
state.isToFar,
|
||||
state.shiftViewModel,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_buildButton(
|
||||
context,
|
||||
bool isToFar,
|
||||
ShiftEntity viewModel,
|
||||
) {
|
||||
final status = viewModel.status;
|
||||
return Column(
|
||||
children: [
|
||||
if (isToFar && status == EventShiftRoleStaffStatus.confirmed ||
|
||||
status == EventShiftRoleStaffStatus.ongoing)
|
||||
_toFarMessage(status),
|
||||
const Gap(16),
|
||||
KwButton.primary(
|
||||
disabled: (status == EventShiftRoleStaffStatus.confirmed ||
|
||||
status == EventShiftRoleStaffStatus.ongoing) &&
|
||||
isToFar != false,
|
||||
label: status == EventShiftRoleStaffStatus.confirmed
|
||||
? 'clock_in'.tr()
|
||||
: 'clock_out'.tr(),
|
||||
onPressed: () async {
|
||||
if (status == EventShiftRoleStaffStatus.confirmed) {
|
||||
_onClockIn(context, viewModel.eventId);
|
||||
} else {
|
||||
await _onClocOut(context, viewModel.eventId, viewModel);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (isToFar && status == EventShiftRoleStaffStatus.confirmed) ...[
|
||||
const Gap(16),
|
||||
KwButton.outlinedPrimary(
|
||||
leftIcon: Assets.images.icons.routing,
|
||||
label: 'get_direction'.tr(),
|
||||
onPressed: () {
|
||||
MapUtils.openMapByLatLon(
|
||||
viewModel.locationLat,
|
||||
viewModel.locationLon,
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toFarMessage(status) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.graySecondaryFrame,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 28,
|
||||
width: 28,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Assets.images.icons.alertCircle.svg(
|
||||
height: 12,
|
||||
width: 12,
|
||||
colorFilter:
|
||||
const ColorFilter.mode(AppColors.blackBlack, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
status == EventShiftRoleStaffStatus.ongoing
|
||||
? 'reach_location_to_clock_out'.tr()
|
||||
: 'reach_location_to_clock_in'.tr(),
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onClocOut(
|
||||
BuildContext context, String eventId, ShiftEntity shift) async {
|
||||
if (!kDebugMode) {
|
||||
var qrResult = await context.router.push(const QrScannerRoute());
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (qrResult == null || qrResult != eventId) {
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'oops_something_wrong'.tr(),
|
||||
message: 'qr_code_error'.tr(),
|
||||
primaryButtonLabel: 'retry_scanning'.tr(),
|
||||
secondaryButtonLabel: 'cancel'.tr(),
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
_onClocOut(context, eventId, shift);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
ShiftCompleterService().startCompleteProcess(context, shift,
|
||||
onComplete: () {
|
||||
|
||||
BlocProvider.of<ShiftDetailsBloc>(context)
|
||||
.add(const ShiftCompleteEvent());
|
||||
});
|
||||
}
|
||||
|
||||
void _onClockIn(BuildContext context, String eventId) async {
|
||||
if (kDebugMode) {
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(const ShiftClockInEvent());
|
||||
return;
|
||||
}
|
||||
var result = await context.router.push(const QrScannerRoute());
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (result != null && result == eventId) {
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.like,
|
||||
state: KwDialogState.positive,
|
||||
title: 'youre_good_to_go'.tr(),
|
||||
message: 'shift_timer_started'.tr(),
|
||||
child: Text(
|
||||
'lunch_break_reminder'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
primaryButtonLabel: 'Continue to Dashboard');
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(const ShiftClockInEvent());
|
||||
} else {
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'oops_something_wrong'.tr(),
|
||||
message: 'qr_code_error'.tr(),
|
||||
primaryButtonLabel: 'retry_scanning'.tr(),
|
||||
secondaryButtonLabel: 'cancel'.tr(),
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
_onClockIn(context, eventId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/shifts/data/models/staff_shift.dart';
|
||||
|
||||
class ShiftTimerWidget extends StatefulWidget {
|
||||
final EventShiftRoleStaffStatus shiftStatus;
|
||||
final DateTime? clockIn;
|
||||
final DateTime? clockOut;
|
||||
|
||||
const ShiftTimerWidget(
|
||||
{super.key,
|
||||
required this.shiftStatus,
|
||||
required this.clockIn,
|
||||
required this.clockOut});
|
||||
|
||||
@override
|
||||
State<ShiftTimerWidget> createState() => _ShiftTimerWidgetState();
|
||||
}
|
||||
|
||||
class _ShiftTimerWidgetState extends State<ShiftTimerWidget> {
|
||||
Color _borderColor() =>
|
||||
widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
|
||||
? AppColors.tintDarkGreen
|
||||
: AppColors.grayStroke;
|
||||
|
||||
Color _labelColor() => widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
|
||||
? AppColors.statusSuccess
|
||||
: AppColors.blackGray;
|
||||
|
||||
Color _counterColor() =>
|
||||
widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
|
||||
? AppColors.statusSuccess
|
||||
: AppColors.blackBlack;
|
||||
|
||||
Color _bgColor() => widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
|
||||
? AppColors.tintGreen
|
||||
: AppColors.graySecondaryFrame;
|
||||
|
||||
Color _dividerColor() =>
|
||||
widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
|
||||
? AppColors.tintDarkGreen
|
||||
: AppColors.grayTintStroke;
|
||||
|
||||
Duration _getDuration() {
|
||||
return widget.shiftStatus == EventShiftRoleStaffStatus.ongoing
|
||||
? DateTime.now()
|
||||
.toUtc()
|
||||
.difference(widget.clockIn ?? DateTime.now().toUtc())
|
||||
: Duration.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var duration = _getDuration();
|
||||
var hours = duration.inHours.remainder(24).abs().toString().padLeft(2, '0');
|
||||
var minutes =
|
||||
duration.inMinutes.remainder(60).abs().toString().padLeft(2, '0');
|
||||
var seconds =
|
||||
duration.inSeconds.remainder(60).abs().toString().padLeft(2, '0');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: _bgColor(),
|
||||
border: Border.all(color: _borderColor()),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildTimeText('hours'.tr(), hours),
|
||||
_divider(),
|
||||
_buildTimeText('minutes'.tr(), minutes),
|
||||
_divider(),
|
||||
_buildTimeText('seconds'.tr(), seconds),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() {
|
||||
return SizedBox(
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 24,
|
||||
width: 1,
|
||||
color: _dividerColor(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeText(String label, String time) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(time,
|
||||
style: AppTextStyles.headingH3.copyWith(color: _counterColor())),
|
||||
const Gap(6),
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.bodySmallReg.copyWith(color: _labelColor()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user