feat: integrate TimeCard feature with Firebase support and restructure related components
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../entities/time_card.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for accessing time card data.
|
||||
///
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user