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
|
// Financial & Payroll
|
||||||
export 'src/entities/financial/invoice.dart';
|
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_item.dart';
|
||||||
export 'src/entities/financial/invoice_decline.dart';
|
export 'src/entities/financial/invoice_decline.dart';
|
||||||
export 'src/entities/financial/staff_payment.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';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Status of a time card.
|
||||||
enum TimeCardStatus {
|
enum TimeCardStatus {
|
||||||
|
/// Waiting for approval or payment.
|
||||||
pending,
|
pending,
|
||||||
|
/// Approved by manager.
|
||||||
approved,
|
approved,
|
||||||
|
/// Payment has been issued.
|
||||||
paid,
|
paid,
|
||||||
|
/// Disputed by staff or client.
|
||||||
disputed;
|
disputed;
|
||||||
|
|
||||||
|
/// Whether the card is approved.
|
||||||
bool get isApproved => this == TimeCardStatus.approved;
|
bool get isApproved => this == TimeCardStatus.approved;
|
||||||
|
/// Whether the card is paid.
|
||||||
bool get isPaid => this == TimeCardStatus.paid;
|
bool get isPaid => this == TimeCardStatus.paid;
|
||||||
|
/// Whether the card is disputed.
|
||||||
bool get isDisputed => this == TimeCardStatus.disputed;
|
bool get isDisputed => this == TimeCardStatus.disputed;
|
||||||
|
/// Whether the card is pending.
|
||||||
bool get isPending => this == TimeCardStatus.pending;
|
bool get isPending => this == TimeCardStatus.pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a time card for a staff member.
|
||||||
class TimeCard extends Equatable {
|
class TimeCard extends Equatable {
|
||||||
|
/// Unique identifier of the time card (often matches Application ID).
|
||||||
final String id;
|
final String id;
|
||||||
|
/// Title of the shift.
|
||||||
final String shiftTitle;
|
final String shiftTitle;
|
||||||
|
/// Name of the client business.
|
||||||
final String clientName;
|
final String clientName;
|
||||||
|
/// Date of the shift.
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
|
/// Actual or scheduled start time.
|
||||||
final String startTime;
|
final String startTime;
|
||||||
|
/// Actual or scheduled end time.
|
||||||
final String endTime;
|
final String endTime;
|
||||||
|
/// Total hours worked.
|
||||||
final double totalHours;
|
final double totalHours;
|
||||||
|
/// Hourly pay rate.
|
||||||
final double hourlyRate;
|
final double hourlyRate;
|
||||||
|
/// Total pay amount.
|
||||||
final double totalPay;
|
final double totalPay;
|
||||||
|
/// Current status of the time card.
|
||||||
final TimeCardStatus status;
|
final TimeCardStatus status;
|
||||||
|
/// Location name.
|
||||||
final String? location;
|
final String? location;
|
||||||
|
|
||||||
|
/// Creates a [TimeCard].
|
||||||
const TimeCard({
|
const TimeCard({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.shiftTitle,
|
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 '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';
|
import '../../domain/repositories/time_card_repository.dart';
|
||||||
|
|
||||||
|
/// Implementation of [TimeCardRepository] using Firebase Data Connect.
|
||||||
class TimeCardRepositoryImpl implements TimeCardRepository {
|
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
|
@override
|
||||||
Future<List<TimeCard>> getTimeCards(DateTime month) async {
|
Future<List<TimeCard>> getTimeCards(DateTime month) async {
|
||||||
// We use ShiftsRepositoryMock as it contains shift details (title, client, etc).
|
final String staffId = await _getStaffId();
|
||||||
// In a real app, we might query 'TimeCards' directly or join Shift+Payment.
|
// Fetch applications. Limit can be adjusted, assuming 100 is safe for now.
|
||||||
// For now, we simulate TimeCards from Shifts.
|
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
|
||||||
final List<Shift> shifts = await shiftsRepository.getMyShifts();
|
await _dataConnect.getApplicationsByStaffId(staffId: staffId).limit(100).execute();
|
||||||
|
|
||||||
// Map to TimeCard and filter by the requested month.
|
return result.data.applications
|
||||||
return shifts
|
.where((dc.GetApplicationsByStaffIdApplications app) {
|
||||||
.map((Shift shift) {
|
final DateTime? shiftDate = app.shift.date?.toDateTime();
|
||||||
double hours = 8.0;
|
if (shiftDate == null) return false;
|
||||||
// Simple parse for mock
|
return shiftDate.year == month.year && shiftDate.month == month.month;
|
||||||
try {
|
})
|
||||||
// Assuming HH:mm
|
.map((dc.GetApplicationsByStaffIdApplications app) {
|
||||||
final int start = int.parse(shift.startTime.split(':')[0]);
|
final DateTime shiftDate = app.shift.date!.toDateTime();
|
||||||
final int end = int.parse(shift.endTime.split(':')[0]);
|
final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? '';
|
||||||
hours = (end - start).abs().toDouble();
|
final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? '';
|
||||||
if (hours == 0) hours = 8.0;
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
return TimeCard(
|
// Prefer shiftRole values for pay/hours
|
||||||
id: shift.id,
|
final double hours = app.shiftRole.hours ?? 0.0;
|
||||||
shiftTitle: shift.title,
|
final double rate = app.shiftRole.role.costPerHour;
|
||||||
clientName: shift.clientName,
|
final double pay = app.shiftRole.totalValue ?? 0.0;
|
||||||
date: DateTime.tryParse(shift.date) ?? DateTime.now(),
|
|
||||||
startTime: shift.startTime,
|
return TimeCardAdapter.fromPrimitives(
|
||||||
endTime: shift.endTime,
|
id: app.id,
|
||||||
|
shiftTitle: app.shift.title,
|
||||||
|
clientName: app.shift.order.business.businessName,
|
||||||
|
date: shiftDate,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
totalHours: hours,
|
totalHours: hours,
|
||||||
hourlyRate: shift.hourlyRate,
|
hourlyRate: rate,
|
||||||
totalPay: hours * shift.hourlyRate,
|
totalPay: pay,
|
||||||
status: _mapStatus(shift.status),
|
status: app.status.stringValue,
|
||||||
location: shift.location,
|
location: app.shift.location,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.where((TimeCard tc) =>
|
|
||||||
tc.date.year == month.year && tc.date.month == month.month)
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeCardStatus _mapStatus(String? shiftStatus) {
|
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||||
if (shiftStatus == null) return TimeCardStatus.pending;
|
if (timestamp == null) return null;
|
||||||
// Map shift status to TimeCardStatus
|
return DateFormat('HH:mm').format(timestamp.toDateTime());
|
||||||
switch (shiftStatus.toLowerCase()) {
|
|
||||||
case 'confirmed':
|
|
||||||
return TimeCardStatus.pending;
|
|
||||||
case 'completed':
|
|
||||||
return TimeCardStatus.approved;
|
|
||||||
case 'paid':
|
|
||||||
return TimeCardStatus.paid;
|
|
||||||
default:
|
|
||||||
return TimeCardStatus.pending;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import '../entities/time_card.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Repository interface for accessing time card data.
|
/// Repository interface for accessing time card data.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:krow_core/core.dart';
|
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 '../arguments/get_time_cards_arguments.dart';
|
||||||
import '../repositories/time_card_repository.dart';
|
import '../repositories/time_card_repository.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.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/arguments/get_time_cards_arguments.dart';
|
||||||
import '../../domain/usecases/get_time_cards_usecase.dart';
|
import '../../domain/usecases/get_time_cards_usecase.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:core_localization/core_localization.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';
|
import 'timesheet_card.dart';
|
||||||
|
|
||||||
/// Displays the list of shift history or an empty state.
|
/// 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:intl/intl.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:core_localization/core_localization.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.
|
/// A card widget displaying details of a single shift/timecard.
|
||||||
class TimesheetCard extends StatelessWidget {
|
class TimesheetCard extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
library staff_time_card;
|
library staff_time_card;
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -11,15 +12,23 @@ import 'presentation/pages/time_card_page.dart';
|
|||||||
|
|
||||||
export '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 {
|
class StaffTimeCardModule extends Module {
|
||||||
|
@override
|
||||||
|
List<Module> get imports => <Module>[DataConnectModule()];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repositories
|
// Repositories
|
||||||
// In a real app, ShiftsRepository might be provided by a Core Data Module.
|
i.add<TimeCardRepository>(
|
||||||
// For this self-contained feature/mock, we instantiate it here if not available globally.
|
() => TimeCardRepositoryImpl(
|
||||||
// Assuming we need a local instance for the mock to work or it's stateless.
|
dataConnect: ExampleConnector.instance,
|
||||||
i.add<ShiftsRepositoryMock>(ShiftsRepositoryMock.new);
|
firebaseAuth: FirebaseAuth.instance,
|
||||||
i.add<TimeCardRepository>(TimeCardRepositoryImpl.new);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new);
|
i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new);
|
||||||
|
|||||||
Reference in New Issue
Block a user