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

@@ -45,6 +45,7 @@ export 'src/entities/skills/skill_kit.dart';
// Financial & Payroll
export 'src/entities/financial/invoice.dart';
export 'src/entities/financial/time_card.dart';
export 'src/entities/financial/invoice_item.dart';
export 'src/entities/financial/invoice_decline.dart';
export 'src/entities/financial/staff_payment.dart';

View File

@@ -0,0 +1,50 @@
import '../../entities/financial/time_card.dart';
/// Adapter for [TimeCard] to map data layer values to domain entity.
class TimeCardAdapter {
/// Maps primitive values to [TimeCard].
static TimeCard fromPrimitives({
required String id,
required String shiftTitle,
required String clientName,
required DateTime date,
required String startTime,
required String endTime,
required double totalHours,
required double hourlyRate,
required double totalPay,
required String status,
String? location,
}) {
return TimeCard(
id: id,
shiftTitle: shiftTitle,
clientName: clientName,
date: date,
startTime: startTime,
endTime: endTime,
totalHours: totalHours,
hourlyRate: hourlyRate,
totalPay: totalPay,
status: _stringToStatus(status),
location: location,
);
}
static TimeCardStatus _stringToStatus(String status) {
switch (status.toUpperCase()) {
case 'CHECKED_OUT':
case 'COMPLETED':
return TimeCardStatus.approved; // Assuming completed = approved for now
case 'PAID':
return TimeCardStatus.paid; // If this status exists
case 'DISPUTED':
return TimeCardStatus.disputed;
case 'CHECKED_IN':
case 'ACCEPTED':
case 'CONFIRMED':
default:
return TimeCardStatus.pending;
}
}
}

View File

@@ -1,30 +1,52 @@
import 'package:equatable/equatable.dart';
/// Status of a time card.
enum TimeCardStatus {
/// Waiting for approval or payment.
pending,
/// Approved by manager.
approved,
/// Payment has been issued.
paid,
/// Disputed by staff or client.
disputed;
/// Whether the card is approved.
bool get isApproved => this == TimeCardStatus.approved;
/// Whether the card is paid.
bool get isPaid => this == TimeCardStatus.paid;
/// Whether the card is disputed.
bool get isDisputed => this == TimeCardStatus.disputed;
/// Whether the card is pending.
bool get isPending => this == TimeCardStatus.pending;
}
/// Represents a time card for a staff member.
class TimeCard extends Equatable {
/// Unique identifier of the time card (often matches Application ID).
final String id;
/// Title of the shift.
final String shiftTitle;
/// Name of the client business.
final String clientName;
/// Date of the shift.
final DateTime date;
/// Actual or scheduled start time.
final String startTime;
/// Actual or scheduled end time.
final String endTime;
/// Total hours worked.
final double totalHours;
/// Hourly pay rate.
final double hourlyRate;
/// Total pay amount.
final double totalPay;
/// Current status of the time card.
final TimeCardStatus status;
/// Location name.
final String? location;
/// Creates a [TimeCard].
const TimeCard({
required this.id,
required this.shiftTitle,

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,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);