feat: integrate Clock In functionality with Firebase support and refactor attendance management
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
/// Locales: 2
|
/// Locales: 2
|
||||||
/// Strings: 1044 (522 per locale)
|
/// 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
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export 'src/entities/home/reorder_item.dart';
|
|||||||
|
|
||||||
// Availability
|
// Availability
|
||||||
export 'src/adapters/availability/availability_adapter.dart';
|
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/availability_slot.dart';
|
||||||
export 'src/entities/availability/day_availability.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';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Interface for the Clock In feature repository.
|
/// Repository interface for Clock In/Out functionality
|
||||||
///
|
abstract class ClockInRepositoryInterface {
|
||||||
/// Defines the methods for managing clock-in/out operations and retrieving
|
|
||||||
/// related shift and attendance data.
|
/// Retrieves the shift assigned to the user for the current day.
|
||||||
abstract interface class ClockInRepositoryInterface {
|
/// Returns null if no shift is assigned for today.
|
||||||
/// Retrieves the shift scheduled for today.
|
|
||||||
Future<Shift?> getTodaysShift();
|
Future<Shift?> getTodaysShift();
|
||||||
|
|
||||||
/// Retrieves the current attendance status (check-in time, check-out time, etc.).
|
/// Gets the current attendance status (e.g., checked in or not, times).
|
||||||
///
|
/// This helps in restoring the UI state if the app was killed.
|
||||||
/// Returns a Map containing:
|
Future<AttendanceStatus> getAttendanceStatus();
|
||||||
/// - 'isCheckedIn': bool
|
|
||||||
/// - 'checkInTime': DateTime?
|
|
||||||
/// - 'checkOutTime': DateTime?
|
|
||||||
Future<Map<String, dynamic>> getAttendanceStatus();
|
|
||||||
|
|
||||||
/// Clocks the user in for a specific shift.
|
/// Checks the user in for the specified [shiftId].
|
||||||
Future<Map<String, dynamic>> clockIn({
|
/// Returns the updated [AttendanceStatus].
|
||||||
required String shiftId,
|
Future<AttendanceStatus> clockIn({required String shiftId, String? notes});
|
||||||
String? notes,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Clocks the user out of the current shift.
|
/// Checks the user out for the currently active shift.
|
||||||
Future<Map<String, dynamic>> clockOut({
|
/// Optionally accepts [breakTimeMinutes] if tracked.
|
||||||
String? notes,
|
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes});
|
||||||
int? breakTimeMinutes,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Retrieves the history of clock-in/out activity.
|
/// Retrieves a list of recent clock-in/out activities.
|
||||||
///
|
|
||||||
/// Returns a list of maps, where each map represents an activity entry.
|
|
||||||
Future<List<Map<String, dynamic>>> getActivityLog();
|
Future<List<Map<String, dynamic>>> getActivityLog();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../repositories/clock_in_repository_interface.dart';
|
import '../repositories/clock_in_repository_interface.dart';
|
||||||
import '../arguments/clock_in_arguments.dart';
|
import '../arguments/clock_in_arguments.dart';
|
||||||
|
|
||||||
/// Use case for clocking in a user.
|
/// Use case for clocking in a user.
|
||||||
class ClockInUseCase implements UseCase<ClockInArguments, Map<String, dynamic>> {
|
class ClockInUseCase implements UseCase<ClockInArguments, AttendanceStatus> {
|
||||||
final ClockInRepositoryInterface _repository;
|
final ClockInRepositoryInterface _repository;
|
||||||
|
|
||||||
ClockInUseCase(this._repository);
|
ClockInUseCase(this._repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> call(ClockInArguments arguments) {
|
Future<AttendanceStatus> call(ClockInArguments arguments) {
|
||||||
return _repository.clockIn(
|
return _repository.clockIn(
|
||||||
shiftId: arguments.shiftId,
|
shiftId: arguments.shiftId,
|
||||||
notes: arguments.notes,
|
notes: arguments.notes,
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../repositories/clock_in_repository_interface.dart';
|
import '../repositories/clock_in_repository_interface.dart';
|
||||||
import '../arguments/clock_out_arguments.dart';
|
import '../arguments/clock_out_arguments.dart';
|
||||||
|
|
||||||
/// Use case for clocking out a user.
|
/// Use case for clocking out a user.
|
||||||
class ClockOutUseCase implements UseCase<ClockOutArguments, Map<String, dynamic>> {
|
class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
|
||||||
final ClockInRepositoryInterface _repository;
|
final ClockInRepositoryInterface _repository;
|
||||||
|
|
||||||
ClockOutUseCase(this._repository);
|
ClockOutUseCase(this._repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> call(ClockOutArguments arguments) {
|
Future<AttendanceStatus> call(ClockOutArguments arguments) {
|
||||||
return _repository.clockOut(
|
return _repository.clockOut(
|
||||||
notes: arguments.notes,
|
notes: arguments.notes,
|
||||||
breakTimeMinutes: arguments.breakTimeMinutes,
|
breakTimeMinutes: arguments.breakTimeMinutes,
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../repositories/clock_in_repository_interface.dart';
|
import '../repositories/clock_in_repository_interface.dart';
|
||||||
|
|
||||||
/// Use case for getting the current attendance status (check-in/out times).
|
/// 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;
|
final ClockInRepositoryInterface _repository;
|
||||||
|
|
||||||
GetAttendanceStatusUseCase(this._repository);
|
GetAttendanceStatusUseCase(this._repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> call() {
|
Future<AttendanceStatus> call() {
|
||||||
return _repository.getAttendanceStatus();
|
return _repository.getAttendanceStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,15 +37,6 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
add(ClockInPageLoaded());
|
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(
|
Future<void> _onLoaded(
|
||||||
ClockInPageLoaded event,
|
ClockInPageLoaded event,
|
||||||
Emitter<ClockInState> emit,
|
Emitter<ClockInState> emit,
|
||||||
@@ -53,13 +44,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
emit(state.copyWith(status: ClockInStatus.loading));
|
emit(state.copyWith(status: ClockInStatus.loading));
|
||||||
try {
|
try {
|
||||||
final shift = await _getTodaysShift();
|
final shift = await _getTodaysShift();
|
||||||
final statusMap = await _getAttendanceStatus();
|
final status = await _getAttendanceStatus();
|
||||||
final activity = await _getActivityLog();
|
final activity = await _getActivityLog();
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.success,
|
status: ClockInStatus.success,
|
||||||
todayShift: shift,
|
todayShift: shift,
|
||||||
attendance: _mapToStatus(statusMap),
|
attendance: status,
|
||||||
activityLog: activity,
|
activityLog: activity,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -90,12 +81,12 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||||
try {
|
try {
|
||||||
final newStatusMap = await _clockIn(
|
final newStatus = await _clockIn(
|
||||||
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
|
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
|
||||||
);
|
);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.success,
|
status: ClockInStatus.success,
|
||||||
attendance: _mapToStatus(newStatusMap),
|
attendance: newStatus,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
@@ -111,7 +102,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||||
try {
|
try {
|
||||||
final newStatusMap = await _clockOut(
|
final newStatus = await _clockOut(
|
||||||
ClockOutArguments(
|
ClockOutArguments(
|
||||||
notes: event.notes,
|
notes: event.notes,
|
||||||
breakTimeMinutes: 0, // Should be passed from event if supported
|
breakTimeMinutes: 0, // Should be passed from event if supported
|
||||||
@@ -119,7 +110,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
);
|
);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.success,
|
status: ClockInStatus.success,
|
||||||
attendance: _mapToStatus(newStatusMap),
|
attendance: newStatus,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
|||||||
@@ -3,24 +3,6 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
|
|
||||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
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 {
|
class ClockInState extends Equatable {
|
||||||
final ClockInStatus status;
|
final ClockInStatus status;
|
||||||
final Shift? todayShift;
|
final Shift? todayShift;
|
||||||
|
|||||||
@@ -107,63 +107,6 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
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
|
// Your Activity Header
|
||||||
const Text(
|
const Text(
|
||||||
@@ -175,39 +118,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
color: AppColors.krowCharcoal,
|
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),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Selected Shift Info Card
|
// Selected Shift Info Card
|
||||||
@@ -376,12 +287,13 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
] else ...[
|
] else ...[
|
||||||
// No Shift State
|
// No Shift State
|
||||||
Container(
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF1F5F9), // slate-100
|
color: const Color(0xFFF1F5F9), // slate-100
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
"No confirmed shifts for today",
|
"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:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.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/repositories/clock_in_repository_interface.dart';
|
||||||
import 'domain/usecases/clock_in_usecase.dart';
|
import 'domain/usecases/clock_in_usecase.dart';
|
||||||
import 'domain/usecases/clock_out_usecase.dart';
|
import 'domain/usecases/clock_out_usecase.dart';
|
||||||
@@ -13,11 +15,13 @@ import 'presentation/pages/clock_in_page.dart';
|
|||||||
class StaffClockInModule extends Module {
|
class StaffClockInModule extends Module {
|
||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Data Sources (Mocks from data_connect)
|
|
||||||
i.add<ShiftsRepositoryMock>(ShiftsRepositoryMock.new);
|
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
i.add<ClockInRepositoryInterface>(ClockInRepositoryImpl.new);
|
i.add<ClockInRepositoryInterface>(
|
||||||
|
() => ClockInRepositoryImpl(
|
||||||
|
dataConnect: ExampleConnector.instance,
|
||||||
|
firebaseAuth: FirebaseAuth.instance,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Use Cases
|
// Use Cases
|
||||||
i.add<GetTodaysShiftUseCase>(GetTodaysShiftUseCase.new);
|
i.add<GetTodaysShiftUseCase>(GetTodaysShiftUseCase.new);
|
||||||
|
|||||||
@@ -31,3 +31,4 @@ dependencies:
|
|||||||
firebase_data_connect: ^0.2.2+2
|
firebase_data_connect: ^0.2.2+2
|
||||||
geolocator: ^10.1.0
|
geolocator: ^10.1.0
|
||||||
permission_handler: ^11.0.1
|
permission_handler: ^11.0.1
|
||||||
|
firebase_auth: ^6.1.4
|
||||||
|
|||||||
Reference in New Issue
Block a user