feat: integrate TimeCard feature with Firebase support and restructure related components

This commit is contained in:
Achintha Isuru
2026-01-30 15:29:19 -05:00
parent 772d59a7dd
commit 4fb2f17ea5
10 changed files with 152 additions and 56 deletions

View File

@@ -1,64 +1,78 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/entities/time_card.dart';
// ignore: implementation_imports
import 'package:krow_domain/src/adapters/financial/time_card_adapter.dart';
import '../../domain/repositories/time_card_repository.dart';
/// Implementation of [TimeCardRepository] using Firebase Data Connect.
class TimeCardRepositoryImpl implements TimeCardRepository {
final ShiftsRepositoryMock shiftsRepository;
final dc.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
TimeCardRepositoryImpl({required this.shiftsRepository});
/// Creates a [TimeCardRepositoryImpl].
TimeCardRepositoryImpl({
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 fdc.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;
}
@override
Future<List<TimeCard>> getTimeCards(DateTime month) async {
// We use ShiftsRepositoryMock as it contains shift details (title, client, etc).
// In a real app, we might query 'TimeCards' directly or join Shift+Payment.
// For now, we simulate TimeCards from Shifts.
final List<Shift> shifts = await shiftsRepository.getMyShifts();
final String staffId = await _getStaffId();
// Fetch applications. Limit can be adjusted, assuming 100 is safe for now.
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
await _dataConnect.getApplicationsByStaffId(staffId: staffId).limit(100).execute();
// Map to TimeCard and filter by the requested month.
return shifts
.map((Shift shift) {
double hours = 8.0;
// Simple parse for mock
try {
// Assuming HH:mm
final int start = int.parse(shift.startTime.split(':')[0]);
final int end = int.parse(shift.endTime.split(':')[0]);
hours = (end - start).abs().toDouble();
if (hours == 0) hours = 8.0;
} catch (_) {}
return result.data.applications
.where((dc.GetApplicationsByStaffIdApplications app) {
final DateTime? shiftDate = app.shift.date?.toDateTime();
if (shiftDate == null) return false;
return shiftDate.year == month.year && shiftDate.month == month.month;
})
.map((dc.GetApplicationsByStaffIdApplications app) {
final DateTime shiftDate = app.shift.date!.toDateTime();
final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? '';
final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? '';
return TimeCard(
id: shift.id,
shiftTitle: shift.title,
clientName: shift.clientName,
date: DateTime.tryParse(shift.date) ?? DateTime.now(),
startTime: shift.startTime,
endTime: shift.endTime,
// Prefer shiftRole values for pay/hours
final double hours = app.shiftRole.hours ?? 0.0;
final double rate = app.shiftRole.role.costPerHour;
final double pay = app.shiftRole.totalValue ?? 0.0;
return TimeCardAdapter.fromPrimitives(
id: app.id,
shiftTitle: app.shift.title,
clientName: app.shift.order.business.businessName,
date: shiftDate,
startTime: startTime,
endTime: endTime,
totalHours: hours,
hourlyRate: shift.hourlyRate,
totalPay: hours * shift.hourlyRate,
status: _mapStatus(shift.status),
location: shift.location,
hourlyRate: rate,
totalPay: pay,
status: app.status.stringValue,
location: app.shift.location,
);
})
.where((TimeCard tc) =>
tc.date.year == month.year && tc.date.month == month.month)
.toList();
}
TimeCardStatus _mapStatus(String? shiftStatus) {
if (shiftStatus == null) return TimeCardStatus.pending;
// Map shift status to TimeCardStatus
switch (shiftStatus.toLowerCase()) {
case 'confirmed':
return TimeCardStatus.pending;
case 'completed':
return TimeCardStatus.approved;
case 'paid':
return TimeCardStatus.paid;
default:
return TimeCardStatus.pending;
}
String? _formatTime(fdc.Timestamp? timestamp) {
if (timestamp == null) return null;
return DateFormat('HH:mm').format(timestamp.toDateTime());
}
}

View File

@@ -1,56 +0,0 @@
import 'package:equatable/equatable.dart';
enum TimeCardStatus {
pending,
approved,
paid,
disputed;
bool get isApproved => this == TimeCardStatus.approved;
bool get isPaid => this == TimeCardStatus.paid;
bool get isDisputed => this == TimeCardStatus.disputed;
bool get isPending => this == TimeCardStatus.pending;
}
class TimeCard extends Equatable {
final String id;
final String shiftTitle;
final String clientName;
final DateTime date;
final String startTime;
final String endTime;
final double totalHours;
final double hourlyRate;
final double totalPay;
final TimeCardStatus status;
final String? location;
const TimeCard({
required this.id,
required this.shiftTitle,
required this.clientName,
required this.date,
required this.startTime,
required this.endTime,
required this.totalHours,
required this.hourlyRate,
required this.totalPay,
required this.status,
this.location,
});
@override
List<Object?> get props => [
id,
shiftTitle,
clientName,
date,
startTime,
endTime,
totalHours,
hourlyRate,
totalPay,
status,
location,
];
}

View File

@@ -1,4 +1,4 @@
import '../entities/time_card.dart';
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for accessing time card data.
///

View File

@@ -1,5 +1,5 @@
import 'package:krow_core/core.dart';
import '../../domain/entities/time_card.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_time_cards_arguments.dart';
import '../repositories/time_card_repository.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/entities/time_card.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/get_time_cards_arguments.dart';
import '../../domain/usecases/get_time_cards_usecase.dart';

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import '../../domain/entities/time_card.dart';
import 'package:krow_domain/krow_domain.dart';
import 'timesheet_card.dart';
/// Displays the list of shift history or an empty state.

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import '../../domain/entities/time_card.dart';
import 'package:krow_domain/krow_domain.dart';
/// A card widget displaying details of a single shift/timecard.
class TimesheetCard extends StatelessWidget {

View File

@@ -1,5 +1,6 @@
library staff_time_card;
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
@@ -11,15 +12,23 @@ import 'presentation/pages/time_card_page.dart';
export 'presentation/pages/time_card_page.dart';
/// Module for the Staff Time Card feature.
///
/// This module configures dependency injection for accessing time card data,
/// including the repositories, use cases, and BLoCs.
class StaffTimeCardModule extends Module {
@override
List<Module> get imports => <Module>[DataConnectModule()];
@override
void binds(Injector i) {
// Repositories
// In a real app, ShiftsRepository might be provided by a Core Data Module.
// For this self-contained feature/mock, we instantiate it here if not available globally.
// Assuming we need a local instance for the mock to work or it's stateless.
i.add<ShiftsRepositoryMock>(ShiftsRepositoryMock.new);
i.add<TimeCardRepository>(TimeCardRepositoryImpl.new);
i.add<TimeCardRepository>(
() => TimeCardRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// UseCases
i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new);