feat: integrate Clock In functionality with Firebase support and refactor attendance management
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
/// Locales: 2
|
||||
/// Strings: 1044 (522 per locale)
|
||||
///
|
||||
/// Built on 2026-01-30 at 19:58 UTC
|
||||
/// Built on 2026-01-30 at 22:11 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -80,6 +80,8 @@ export 'src/entities/home/reorder_item.dart';
|
||||
|
||||
// Availability
|
||||
export 'src/adapters/availability/availability_adapter.dart';
|
||||
export 'src/entities/clock_in/attendance_status.dart';
|
||||
export 'src/adapters/clock_in/clock_in_adapter.dart';
|
||||
export 'src/entities/availability/availability_slot.dart';
|
||||
export 'src/entities/availability/day_availability.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import '../../entities/shifts/shift.dart';
|
||||
import '../../entities/clock_in/attendance_status.dart';
|
||||
|
||||
/// Adapter for Clock In related data.
|
||||
class ClockInAdapter {
|
||||
|
||||
/// Converts primitive attendance data to [AttendanceStatus].
|
||||
static AttendanceStatus toAttendanceStatus({
|
||||
required String status,
|
||||
DateTime? checkInTime,
|
||||
DateTime? checkOutTime,
|
||||
String? activeShiftId,
|
||||
}) {
|
||||
final bool isCheckedIn = status == 'CHECKED_IN' || status == 'LATE'; // Assuming LATE is also checked in?
|
||||
|
||||
// Statuses that imply active attendance: CHECKED_IN, LATE.
|
||||
// Statuses that imply completed: CHECKED_OUT.
|
||||
|
||||
return AttendanceStatus(
|
||||
isCheckedIn: isCheckedIn,
|
||||
checkInTime: checkInTime,
|
||||
checkOutTime: checkOutTime,
|
||||
activeShiftId: activeShiftId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Simple entity to hold attendance state
|
||||
class AttendanceStatus extends Equatable {
|
||||
final bool isCheckedIn;
|
||||
final DateTime? checkInTime;
|
||||
final DateTime? checkOutTime;
|
||||
final String? activeShiftId;
|
||||
|
||||
const AttendanceStatus({
|
||||
this.isCheckedIn = false,
|
||||
this.checkInTime,
|
||||
this.checkOutTime,
|
||||
this.activeShiftId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isCheckedIn, checkInTime, checkOutTime, activeShiftId];
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../domain/repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClockInRepositoryInterface] using Mock Data.
|
||||
///
|
||||
/// This implementation uses hardcoded data to match the prototype UI.
|
||||
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
|
||||
ClockInRepositoryImpl();
|
||||
|
||||
// Local state for the mock implementation
|
||||
bool _isCheckedIn = false;
|
||||
DateTime? _checkInTime;
|
||||
DateTime? _checkOutTime;
|
||||
|
||||
@override
|
||||
Future<Shift?> getTodaysShift() async {
|
||||
// Simulate network delay
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Mock Shift matching the prototype
|
||||
return Shift(
|
||||
id: '1',
|
||||
title: 'Warehouse Assistant',
|
||||
clientName: 'Amazon Warehouse',
|
||||
logoUrl:
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Amazon_2024.svg/500px-Amazon_2024.svg.png',
|
||||
hourlyRate: 22.50,
|
||||
location: 'San Francisco, CA',
|
||||
locationAddress: '123 Market St, San Francisco, CA 94105',
|
||||
date: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
createdDate: DateTime.now().subtract(const Duration(days: 2)).toIso8601String(),
|
||||
status: 'assigned',
|
||||
description: 'General warehouse duties including packing and sorting.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getAttendanceStatus() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
return {
|
||||
'isCheckedIn': _isCheckedIn,
|
||||
'checkInTime': _checkInTime,
|
||||
'checkOutTime': _checkOutTime,
|
||||
'activeShiftId': '1',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> clockIn({required String shiftId, String? notes}) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_isCheckedIn = true;
|
||||
_checkInTime = DateTime.now();
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> clockOut({String? notes, int? breakTimeMinutes}) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_isCheckedIn = false;
|
||||
_checkOutTime = DateTime.now();
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getActivityLog() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
return [
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 1)),
|
||||
'start': '09:00 AM',
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 2)),
|
||||
'start': '09:00 AM',
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 3)),
|
||||
'start': '09:00 AM',
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
|
||||
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final firebase.FirebaseAuth _firebaseAuth;
|
||||
|
||||
ClockInRepositoryImpl({
|
||||
required dc.ExampleConnector dataConnect,
|
||||
required firebase.FirebaseAuth firebaseAuth,
|
||||
}) : _dataConnect = dataConnect,
|
||||
_firebaseAuth = firebaseAuth;
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
final firebase.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) throw Exception('User not authenticated');
|
||||
|
||||
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw Exception('Staff profile not found');
|
||||
}
|
||||
return result.data.staffs.first.id;
|
||||
}
|
||||
|
||||
/// Helper to convert Data Connect Timestamp to DateTime
|
||||
DateTime? _toDateTime(dynamic t) {
|
||||
if (t == null) return null;
|
||||
// Attempt to use toJson assuming it matches the generated code's expectation of String
|
||||
try {
|
||||
// If t has toDate (e.g. cloud_firestore), usage would be t.toDate()
|
||||
// But here we rely on toJson or toString
|
||||
return DateTime.tryParse(t.toJson() as String);
|
||||
} catch (_) {
|
||||
try {
|
||||
return DateTime.tryParse(t.toString());
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create Timestamp from DateTime
|
||||
Timestamp _fromDateTime(DateTime d) {
|
||||
// Assuming Timestamp.fromJson takes an ISO string
|
||||
return Timestamp.fromJson(d.toIso8601String());
|
||||
}
|
||||
|
||||
/// Helper to find today's active application
|
||||
Future<dc.GetApplicationsByStaffIdApplications?> _getTodaysApplication(String staffId) async {
|
||||
final DateTime now = DateTime.now();
|
||||
|
||||
// Fetch recent applications (assuming meaningful limit)
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
|
||||
await _dataConnect.getApplicationsByStaffId(
|
||||
staffId: staffId,
|
||||
).limit(20).execute();
|
||||
|
||||
try {
|
||||
return result.data.applications.firstWhere((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime? shiftTime = _toDateTime(app.shift.startTime);
|
||||
|
||||
if (shiftTime == null) return false;
|
||||
|
||||
final bool isSameDay = shiftTime.year == now.year &&
|
||||
shiftTime.month == now.month &&
|
||||
shiftTime.day == now.day;
|
||||
|
||||
if (!isSameDay) return false;
|
||||
|
||||
// Check Status
|
||||
final dynamic status = app.status.stringValue;
|
||||
return status != 'PENDING' && status != 'REJECTED' && status != 'NO_SHOW' && status != 'CANCELED';
|
||||
});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Shift?> getTodaysShift() async {
|
||||
final String staffId = await _getStaffId();
|
||||
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
|
||||
|
||||
if (app == null) return null;
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
|
||||
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult =
|
||||
await _dataConnect.getShiftById(id: shift.id).execute();
|
||||
|
||||
if (shiftResult.data.shift == null) return null;
|
||||
|
||||
final dc.GetShiftByIdShift fullShift = shiftResult.data.shift!;
|
||||
|
||||
return Shift(
|
||||
id: fullShift.id,
|
||||
title: fullShift.title,
|
||||
clientName: fullShift.order.business.businessName,
|
||||
logoUrl: '', // Not available in GetShiftById
|
||||
hourlyRate: 0.0,
|
||||
location: fullShift.location ?? '',
|
||||
locationAddress: fullShift.locationAddress ?? '',
|
||||
date: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '',
|
||||
startTime: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '',
|
||||
endTime: _toDateTime(fullShift.endTime)?.toIso8601String() ?? '',
|
||||
createdDate: _toDateTime(fullShift.createdAt)?.toIso8601String() ?? '',
|
||||
status: fullShift.status?.stringValue,
|
||||
description: fullShift.description,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> getAttendanceStatus() async {
|
||||
final String staffId = await _getStaffId();
|
||||
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
|
||||
|
||||
if (app == null) {
|
||||
return const AttendanceStatus(isCheckedIn: false);
|
||||
}
|
||||
|
||||
return ClockInAdapter.toAttendanceStatus(
|
||||
status: app.status.stringValue,
|
||||
checkInTime: _toDateTime(app.checkInTime),
|
||||
checkOutTime: _toDateTime(app.checkOutTime),
|
||||
activeShiftId: app.shiftId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> clockIn({required String shiftId, String? notes}) async {
|
||||
final String staffId = await _getStaffId();
|
||||
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResult =
|
||||
await _dataConnect.getApplicationsByStaffId(staffId: staffId).execute();
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications app = appsResult.data.applications.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);
|
||||
|
||||
await _dataConnect.updateApplicationStatus(
|
||||
id: app.id,
|
||||
roleId: app.shiftRole.id,
|
||||
)
|
||||
.status(dc.ApplicationStatus.CHECKED_IN)
|
||||
.checkInTime(_fromDateTime(DateTime.now()))
|
||||
.execute();
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}) async {
|
||||
final String staffId = await _getStaffId();
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
|
||||
if (app == null) throw Exception('No active shift found to clock out');
|
||||
|
||||
await _dataConnect.updateApplicationStatus(
|
||||
id: app.id,
|
||||
roleId: app.shiftRole.id,
|
||||
)
|
||||
.status(dc.ApplicationStatus.CHECKED_OUT)
|
||||
.checkOutTime(_fromDateTime(DateTime.now()))
|
||||
.execute();
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getActivityLog() async {
|
||||
// Placeholder as this wasn't main focus and returns raw maps
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for Clock In/Out functionality
|
||||
abstract class ClockInRepository {
|
||||
|
||||
/// Retrieves the shift assigned to the user for the current day.
|
||||
/// Returns null if no shift is assigned for today.
|
||||
Future<Shift?> getTodaysShift();
|
||||
|
||||
/// Gets the current attendance status (e.g., checked in or not, times).
|
||||
/// This helps in restoring the UI state if the app was killed.
|
||||
Future<AttendanceStatus> getAttendanceStatus();
|
||||
|
||||
/// Checks the user in for the specified [shiftId].
|
||||
/// Returns the updated [AttendanceStatus].
|
||||
Future<AttendanceStatus> clockIn({required String shiftId, String? notes});
|
||||
|
||||
/// Checks the user out for the currently active shift.
|
||||
/// Optionally accepts [breakTimeMinutes] if tracked.
|
||||
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes});
|
||||
|
||||
/// Retrieves a list of recent clock-in/out activities.
|
||||
Future<List<Map<String, dynamic>>> getActivityLog();
|
||||
}
|
||||
|
||||
/// Simple entity to hold attendance state
|
||||
class AttendanceStatus {
|
||||
final bool isCheckedIn;
|
||||
final DateTime? checkInTime;
|
||||
final DateTime? checkOutTime;
|
||||
final String? activeShiftId;
|
||||
|
||||
const AttendanceStatus({
|
||||
this.isCheckedIn = false,
|
||||
this.checkInTime,
|
||||
this.checkOutTime,
|
||||
this.activeShiftId,
|
||||
});
|
||||
}
|
||||
@@ -1,35 +1,24 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Interface for the Clock In feature repository.
|
||||
///
|
||||
/// Defines the methods for managing clock-in/out operations and retrieving
|
||||
/// related shift and attendance data.
|
||||
abstract interface class ClockInRepositoryInterface {
|
||||
/// Retrieves the shift scheduled for today.
|
||||
/// Repository interface for Clock In/Out functionality
|
||||
abstract class ClockInRepositoryInterface {
|
||||
|
||||
/// Retrieves the shift assigned to the user for the current day.
|
||||
/// Returns null if no shift is assigned for today.
|
||||
Future<Shift?> getTodaysShift();
|
||||
|
||||
/// Retrieves the current attendance status (check-in time, check-out time, etc.).
|
||||
///
|
||||
/// Returns a Map containing:
|
||||
/// - 'isCheckedIn': bool
|
||||
/// - 'checkInTime': DateTime?
|
||||
/// - 'checkOutTime': DateTime?
|
||||
Future<Map<String, dynamic>> getAttendanceStatus();
|
||||
/// Gets the current attendance status (e.g., checked in or not, times).
|
||||
/// This helps in restoring the UI state if the app was killed.
|
||||
Future<AttendanceStatus> getAttendanceStatus();
|
||||
|
||||
/// Clocks the user in for a specific shift.
|
||||
Future<Map<String, dynamic>> clockIn({
|
||||
required String shiftId,
|
||||
String? notes,
|
||||
});
|
||||
/// Checks the user in for the specified [shiftId].
|
||||
/// Returns the updated [AttendanceStatus].
|
||||
Future<AttendanceStatus> clockIn({required String shiftId, String? notes});
|
||||
|
||||
/// Clocks the user out of the current shift.
|
||||
Future<Map<String, dynamic>> clockOut({
|
||||
String? notes,
|
||||
int? breakTimeMinutes,
|
||||
});
|
||||
/// Checks the user out for the currently active shift.
|
||||
/// Optionally accepts [breakTimeMinutes] if tracked.
|
||||
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes});
|
||||
|
||||
/// Retrieves the history of clock-in/out activity.
|
||||
///
|
||||
/// Returns a list of maps, where each map represents an activity entry.
|
||||
/// Retrieves a list of recent clock-in/out activities.
|
||||
Future<List<Map<String, dynamic>>> getActivityLog();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
import '../arguments/clock_in_arguments.dart';
|
||||
|
||||
/// Use case for clocking in a user.
|
||||
class ClockInUseCase implements UseCase<ClockInArguments, Map<String, dynamic>> {
|
||||
class ClockInUseCase implements UseCase<ClockInArguments, AttendanceStatus> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
ClockInUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> call(ClockInArguments arguments) {
|
||||
Future<AttendanceStatus> call(ClockInArguments arguments) {
|
||||
return _repository.clockIn(
|
||||
shiftId: arguments.shiftId,
|
||||
notes: arguments.notes,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
import '../arguments/clock_out_arguments.dart';
|
||||
|
||||
/// Use case for clocking out a user.
|
||||
class ClockOutUseCase implements UseCase<ClockOutArguments, Map<String, dynamic>> {
|
||||
class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
ClockOutUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> call(ClockOutArguments arguments) {
|
||||
Future<AttendanceStatus> call(ClockOutArguments arguments) {
|
||||
return _repository.clockOut(
|
||||
notes: arguments.notes,
|
||||
breakTimeMinutes: arguments.breakTimeMinutes,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for getting the current attendance status (check-in/out times).
|
||||
class GetAttendanceStatusUseCase implements NoInputUseCase<Map<String, dynamic>> {
|
||||
class GetAttendanceStatusUseCase implements NoInputUseCase<AttendanceStatus> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetAttendanceStatusUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> call() {
|
||||
Future<AttendanceStatus> call() {
|
||||
return _repository.getAttendanceStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,15 +37,6 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
add(ClockInPageLoaded());
|
||||
}
|
||||
|
||||
AttendanceStatus _mapToStatus(Map<String, dynamic> map) {
|
||||
return AttendanceStatus(
|
||||
isCheckedIn: map['isCheckedIn'] as bool? ?? false,
|
||||
checkInTime: map['checkInTime'] as DateTime?,
|
||||
checkOutTime: map['checkOutTime'] as DateTime?,
|
||||
activeShiftId: map['activeShiftId'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLoaded(
|
||||
ClockInPageLoaded event,
|
||||
Emitter<ClockInState> emit,
|
||||
@@ -53,13 +44,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
emit(state.copyWith(status: ClockInStatus.loading));
|
||||
try {
|
||||
final shift = await _getTodaysShift();
|
||||
final statusMap = await _getAttendanceStatus();
|
||||
final status = await _getAttendanceStatus();
|
||||
final activity = await _getActivityLog();
|
||||
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
todayShift: shift,
|
||||
attendance: _mapToStatus(statusMap),
|
||||
attendance: status,
|
||||
activityLog: activity,
|
||||
));
|
||||
} catch (e) {
|
||||
@@ -90,12 +81,12 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatusMap = await _clockIn(
|
||||
final newStatus = await _clockIn(
|
||||
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
attendance: _mapToStatus(newStatusMap),
|
||||
attendance: newStatus,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
@@ -111,7 +102,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatusMap = await _clockOut(
|
||||
final newStatus = await _clockOut(
|
||||
ClockOutArguments(
|
||||
notes: event.notes,
|
||||
breakTimeMinutes: 0, // Should be passed from event if supported
|
||||
@@ -119,7 +110,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
);
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
attendance: _mapToStatus(newStatusMap),
|
||||
attendance: newStatus,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
|
||||
@@ -3,24 +3,6 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||
|
||||
/// View model representing the user's current attendance state.
|
||||
class AttendanceStatus extends Equatable {
|
||||
final bool isCheckedIn;
|
||||
final DateTime? checkInTime;
|
||||
final DateTime? checkOutTime;
|
||||
final String? activeShiftId;
|
||||
|
||||
const AttendanceStatus({
|
||||
this.isCheckedIn = false,
|
||||
this.checkInTime,
|
||||
this.checkOutTime,
|
||||
this.activeShiftId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isCheckedIn, checkInTime, checkOutTime, activeShiftId];
|
||||
}
|
||||
|
||||
class ClockInState extends Equatable {
|
||||
final ClockInStatus status;
|
||||
final Shift? todayShift;
|
||||
|
||||
@@ -107,63 +107,6 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Today Attendance Section
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Today Attendance",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.0,
|
||||
children: [
|
||||
AttendanceCard(
|
||||
type: AttendanceType.checkin,
|
||||
title: "Check In",
|
||||
value: checkInStr,
|
||||
subtitle: checkInTime != null
|
||||
? "On Time"
|
||||
: "Pending",
|
||||
scheduledTime: "09:00 AM",
|
||||
),
|
||||
AttendanceCard(
|
||||
type: AttendanceType.checkout,
|
||||
title: "Check Out",
|
||||
value: checkOutStr,
|
||||
subtitle: checkOutTime != null
|
||||
? "Go Home"
|
||||
: "Pending",
|
||||
scheduledTime: "05:00 PM",
|
||||
),
|
||||
AttendanceCard(
|
||||
type: AttendanceType.breaks,
|
||||
title: "Break Time",
|
||||
// TODO: Connect to Data Connect when 'breakDuration' field is added to Shift schema.
|
||||
value: "00:30 min",
|
||||
subtitle: "Scheduled 00:30 min",
|
||||
),
|
||||
const AttendanceCard(
|
||||
type: AttendanceType.days,
|
||||
title: "Total Days",
|
||||
// TODO: Connect to Data Connect when 'staffStats' or similar aggregation API is available.
|
||||
// Currently avoided to prevent fetching full shift history for a simple count.
|
||||
value: "28",
|
||||
subtitle: "Working Days",
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Your Activity Header
|
||||
const Text(
|
||||
@@ -175,39 +118,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Check-in Mode Toggle
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Check-in Method",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF334155), // slate-700
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildModeTab(
|
||||
"Swipe",
|
||||
LucideIcons.mapPin,
|
||||
'swipe',
|
||||
state.checkInMode,
|
||||
),
|
||||
// _buildModeTab("NFC Tap", LucideIcons.wifi, 'nfc', state.checkInMode),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Selected Shift Info Card
|
||||
@@ -376,12 +287,13 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
] else ...[
|
||||
// No Shift State
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
child: const Column(
|
||||
children: [
|
||||
const Text(
|
||||
"No confirmed shifts for today",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'data/repositories/clock_in_repository_impl.dart';
|
||||
|
||||
import 'data/repositories_impl/clock_in_repository_impl.dart';
|
||||
import 'domain/repositories/clock_in_repository_interface.dart';
|
||||
import 'domain/usecases/clock_in_usecase.dart';
|
||||
import 'domain/usecases/clock_out_usecase.dart';
|
||||
@@ -13,11 +15,13 @@ import 'presentation/pages/clock_in_page.dart';
|
||||
class StaffClockInModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Data Sources (Mocks from data_connect)
|
||||
i.add<ShiftsRepositoryMock>(ShiftsRepositoryMock.new);
|
||||
|
||||
// Repositories
|
||||
i.add<ClockInRepositoryInterface>(ClockInRepositoryImpl.new);
|
||||
i.add<ClockInRepositoryInterface>(
|
||||
() => ClockInRepositoryImpl(
|
||||
dataConnect: ExampleConnector.instance,
|
||||
firebaseAuth: FirebaseAuth.instance,
|
||||
),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.add<GetTodaysShiftUseCase>(GetTodaysShiftUseCase.new);
|
||||
|
||||
@@ -31,3 +31,4 @@ dependencies:
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
geolocator: ^10.1.0
|
||||
permission_handler: ^11.0.1
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
Reference in New Issue
Block a user