feat: add staff time card feature with localization and repository implementation
- Implemented Spanish translations for time card related strings. - Created TimeCardRepository to fetch time card data based on shifts. - Added use cases for retrieving time cards and payment history. - Developed BLoC for managing time card state and events. - Designed UI components for displaying time card summary and shift history. - Integrated time card navigation within the staff main module. - Updated pubspec.yaml files to include new dependencies and modules.
This commit is contained in:
@@ -704,6 +704,21 @@
|
||||
"immediate_start": "Immediate start",
|
||||
"no_experience": "No experience"
|
||||
}
|
||||
},
|
||||
"staff_time_card": {
|
||||
"title": "Timecard",
|
||||
"hours_worked": "Hours Worked",
|
||||
"total_earnings": "Total Earnings",
|
||||
"shift_history": "Shift History",
|
||||
"no_shifts": "No shifts for this month",
|
||||
"hours": "hours",
|
||||
"per_hr": "/hr",
|
||||
"status": {
|
||||
"approved": "Approved",
|
||||
"disputed": "Disputed",
|
||||
"paid": "Paid",
|
||||
"pending": "Pending"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -703,5 +703,20 @@
|
||||
"immediate_start": "Immediate start",
|
||||
"no_experience": "No experience"
|
||||
}
|
||||
},
|
||||
"staff_time_card": {
|
||||
"title": "Tarjeta de tiempo",
|
||||
"hours_worked": "Horas trabajadas",
|
||||
"total_earnings": "Ganancias totales",
|
||||
"shift_history": "Historial de turnos",
|
||||
"no_shifts": "No hay turnos para este mes",
|
||||
"hours": "horas",
|
||||
"per_hr": "/hr",
|
||||
"status": {
|
||||
"approved": "Aprobado",
|
||||
"disputed": "Disputado",
|
||||
"paid": "Pagado",
|
||||
"pending": "Pendiente"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 2
|
||||
/// Strings: 1004 (502 per locale)
|
||||
/// Strings: 1026 (513 per locale)
|
||||
///
|
||||
/// Built on 2026-01-25 at 22:04 UTC
|
||||
/// Built on 2026-01-26 at 02:14 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -56,6 +56,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||
late final TranslationsStaffCertificatesEn staff_certificates = TranslationsStaffCertificatesEn._(_root);
|
||||
late final TranslationsStaffProfileAttireEn staff_profile_attire = TranslationsStaffProfileAttireEn._(_root);
|
||||
late final TranslationsStaffShiftsEn staff_shifts = TranslationsStaffShiftsEn._(_root);
|
||||
late final TranslationsStaffTimeCardEn staff_time_card = TranslationsStaffTimeCardEn._(_root);
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -387,6 +388,38 @@ class TranslationsStaffShiftsEn {
|
||||
late final TranslationsStaffShiftsTagsEn tags = TranslationsStaffShiftsTagsEn._(_root);
|
||||
}
|
||||
|
||||
// Path: staff_time_card
|
||||
class TranslationsStaffTimeCardEn {
|
||||
TranslationsStaffTimeCardEn._(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Timecard'
|
||||
String get title => 'Timecard';
|
||||
|
||||
/// en: 'Hours Worked'
|
||||
String get hours_worked => 'Hours Worked';
|
||||
|
||||
/// en: 'Total Earnings'
|
||||
String get total_earnings => 'Total Earnings';
|
||||
|
||||
/// en: 'Shift History'
|
||||
String get shift_history => 'Shift History';
|
||||
|
||||
/// en: 'No shifts for this month'
|
||||
String get no_shifts => 'No shifts for this month';
|
||||
|
||||
/// en: 'hours'
|
||||
String get hours => 'hours';
|
||||
|
||||
/// en: '/hr'
|
||||
String get per_hr => '/hr';
|
||||
|
||||
late final TranslationsStaffTimeCardStatusEn status = TranslationsStaffTimeCardStatusEn._(_root);
|
||||
}
|
||||
|
||||
// Path: staff_authentication.get_started_page
|
||||
class TranslationsStaffAuthenticationGetStartedPageEn {
|
||||
TranslationsStaffAuthenticationGetStartedPageEn._(this._root);
|
||||
@@ -1691,6 +1724,27 @@ class TranslationsStaffShiftsTagsEn {
|
||||
String get no_experience => 'No experience';
|
||||
}
|
||||
|
||||
// Path: staff_time_card.status
|
||||
class TranslationsStaffTimeCardStatusEn {
|
||||
TranslationsStaffTimeCardStatusEn._(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Approved'
|
||||
String get approved => 'Approved';
|
||||
|
||||
/// en: 'Disputed'
|
||||
String get disputed => 'Disputed';
|
||||
|
||||
/// en: 'Paid'
|
||||
String get paid => 'Paid';
|
||||
|
||||
/// en: 'Pending'
|
||||
String get pending => 'Pending';
|
||||
}
|
||||
|
||||
// Path: staff_authentication.profile_setup_page.steps
|
||||
class TranslationsStaffAuthenticationProfileSetupPageStepsEn {
|
||||
TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root);
|
||||
@@ -3113,6 +3167,19 @@ extension on Translations {
|
||||
'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago',
|
||||
'staff_shifts.tags.immediate_start' => 'Immediate start',
|
||||
'staff_shifts.tags.no_experience' => 'No experience',
|
||||
'staff_time_card.title' => 'Timecard',
|
||||
'staff_time_card.hours_worked' => 'Hours Worked',
|
||||
'staff_time_card.total_earnings' => 'Total Earnings',
|
||||
'staff_time_card.shift_history' => 'Shift History',
|
||||
'staff_time_card.no_shifts' => 'No shifts for this month',
|
||||
'staff_time_card.hours' => 'hours',
|
||||
'staff_time_card.per_hr' => '/hr',
|
||||
'staff_time_card.status.approved' => 'Approved',
|
||||
'staff_time_card.status.disputed' => 'Disputed',
|
||||
'staff_time_card.status.paid' => 'Paid',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'staff_time_card.status.pending' => 'Pending',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class TranslationsEs with BaseTranslations<AppLocale, Translations> implements T
|
||||
@override late final _TranslationsStaffCertificatesEs staff_certificates = _TranslationsStaffCertificatesEs._(_root);
|
||||
@override late final _TranslationsStaffProfileAttireEs staff_profile_attire = _TranslationsStaffProfileAttireEs._(_root);
|
||||
@override late final _TranslationsStaffShiftsEs staff_shifts = _TranslationsStaffShiftsEs._(_root);
|
||||
@override late final _TranslationsStaffTimeCardEs staff_time_card = _TranslationsStaffTimeCardEs._(_root);
|
||||
}
|
||||
|
||||
// Path: common
|
||||
@@ -292,6 +293,23 @@ class _TranslationsStaffShiftsEs implements TranslationsStaffShiftsEn {
|
||||
@override late final _TranslationsStaffShiftsTagsEs tags = _TranslationsStaffShiftsTagsEs._(_root);
|
||||
}
|
||||
|
||||
// Path: staff_time_card
|
||||
class _TranslationsStaffTimeCardEs implements TranslationsStaffTimeCardEn {
|
||||
_TranslationsStaffTimeCardEs._(this._root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'Tarjeta de tiempo';
|
||||
@override String get hours_worked => 'Horas trabajadas';
|
||||
@override String get total_earnings => 'Ganancias totales';
|
||||
@override String get shift_history => 'Historial de turnos';
|
||||
@override String get no_shifts => 'No hay turnos para este mes';
|
||||
@override String get hours => 'horas';
|
||||
@override String get per_hr => '/hr';
|
||||
@override late final _TranslationsStaffTimeCardStatusEs status = _TranslationsStaffTimeCardStatusEs._(_root);
|
||||
}
|
||||
|
||||
// Path: staff_authentication.get_started_page
|
||||
class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn {
|
||||
_TranslationsStaffAuthenticationGetStartedPageEs._(this._root);
|
||||
@@ -1049,6 +1067,19 @@ class _TranslationsStaffShiftsTagsEs implements TranslationsStaffShiftsTagsEn {
|
||||
@override String get no_experience => 'No experience';
|
||||
}
|
||||
|
||||
// Path: staff_time_card.status
|
||||
class _TranslationsStaffTimeCardStatusEs implements TranslationsStaffTimeCardStatusEn {
|
||||
_TranslationsStaffTimeCardStatusEs._(this._root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get approved => 'Aprobado';
|
||||
@override String get disputed => 'Disputado';
|
||||
@override String get paid => 'Pagado';
|
||||
@override String get pending => 'Pendiente';
|
||||
}
|
||||
|
||||
// Path: staff_authentication.profile_setup_page.steps
|
||||
class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn {
|
||||
_TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root);
|
||||
@@ -2091,6 +2122,19 @@ extension on TranslationsEs {
|
||||
'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago',
|
||||
'staff_shifts.tags.immediate_start' => 'Immediate start',
|
||||
'staff_shifts.tags.no_experience' => 'No experience',
|
||||
'staff_time_card.title' => 'Tarjeta de tiempo',
|
||||
'staff_time_card.hours_worked' => 'Horas trabajadas',
|
||||
'staff_time_card.total_earnings' => 'Ganancias totales',
|
||||
'staff_time_card.shift_history' => 'Historial de turnos',
|
||||
'staff_time_card.no_shifts' => 'No hay turnos para este mes',
|
||||
'staff_time_card.hours' => 'horas',
|
||||
'staff_time_card.per_hr' => '/hr',
|
||||
'staff_time_card.status.approved' => 'Aprobado',
|
||||
'staff_time_card.status.disputed' => 'Disputado',
|
||||
'staff_time_card.status.paid' => 'Pagado',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'staff_time_card.status.pending' => 'Pendiente',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@ import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/payments_repository.dart';
|
||||
|
||||
/// Implementation of [PaymentsRepository].
|
||||
///
|
||||
/// This class handles the retrieval of payment data by delegating to the
|
||||
/// [FinancialRepositoryMock] from the data connect package.
|
||||
///
|
||||
/// It resides in the data layer and depends on the domain layer for the repository interface.
|
||||
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
final FinancialRepositoryMock financialRepository;
|
||||
|
||||
/// Creates a [PaymentsRepositoryImpl] with the given [financialRepository].
|
||||
PaymentsRepositoryImpl({required this.financialRepository});
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// Arguments for getting payment history.
|
||||
class GetPaymentHistoryArguments extends UseCaseArgument {
|
||||
/// The period to filter by (e.g., "monthly", "weekly").
|
||||
final String period;
|
||||
|
||||
const GetPaymentHistoryArguments(this.period);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [period];
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PaymentSummary extends Equatable {
|
||||
final double weeklyEarnings;
|
||||
final double monthlyEarnings;
|
||||
final double pendingEarnings;
|
||||
final double totalEarnings;
|
||||
|
||||
const PaymentSummary({
|
||||
required this.weeklyEarnings,
|
||||
required this.monthlyEarnings,
|
||||
required this.pendingEarnings,
|
||||
required this.totalEarnings,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
weeklyEarnings,
|
||||
monthlyEarnings,
|
||||
pendingEarnings,
|
||||
totalEarnings,
|
||||
];
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PaymentTransaction extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final String location;
|
||||
final String address;
|
||||
final DateTime date;
|
||||
final String workedTime;
|
||||
final double amount;
|
||||
final String status;
|
||||
final int hours;
|
||||
final double rate;
|
||||
|
||||
const PaymentTransaction({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.location,
|
||||
required this.address,
|
||||
required this.date,
|
||||
required this.workedTime,
|
||||
required this.amount,
|
||||
required this.status,
|
||||
required this.hours,
|
||||
required this.rate,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
title,
|
||||
location,
|
||||
address,
|
||||
date,
|
||||
workedTime,
|
||||
amount,
|
||||
status,
|
||||
hours,
|
||||
rate,
|
||||
];
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for Payments feature.
|
||||
///
|
||||
/// Defines the contract for data access related to staff payments.
|
||||
/// Implementations of this interface should reside in the data layer.
|
||||
abstract class PaymentsRepository {
|
||||
/// Fetches the list of payments.
|
||||
/// Fetches the list of payments for the current staff member.
|
||||
Future<List<StaffPayment>> getPayments();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../arguments/get_payment_history_arguments.dart';
|
||||
import '../repositories/payments_repository.dart';
|
||||
|
||||
class GetPaymentHistoryUseCase extends UseCase<String, List<StaffPayment>> {
|
||||
/// Use case to retrieve payment history filtered by a period.
|
||||
///
|
||||
/// This use case delegates the data retrieval to [PaymentsRepository].
|
||||
class GetPaymentHistoryUseCase extends UseCase<GetPaymentHistoryArguments, List<StaffPayment>> {
|
||||
final PaymentsRepository repository;
|
||||
|
||||
/// Creates a [GetPaymentHistoryUseCase].
|
||||
GetPaymentHistoryUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<List<StaffPayment>> call(String period) async {
|
||||
Future<List<StaffPayment>> call(GetPaymentHistoryArguments arguments) async {
|
||||
// TODO: Implement filtering by period
|
||||
return await repository.getPayments();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,14 @@ import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/payments_repository.dart';
|
||||
|
||||
/// Use case to retrieve payment summary information.
|
||||
///
|
||||
/// It fetches the full list of payments, which ideally should be aggregated
|
||||
/// by the presentation layer or a specific data source method.
|
||||
class GetPaymentSummaryUseCase extends NoInputUseCase<List<StaffPayment>> {
|
||||
final PaymentsRepository repository;
|
||||
|
||||
/// Creates a [GetPaymentSummaryUseCase].
|
||||
GetPaymentSummaryUseCase(this.repository);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../../domain/arguments/get_payment_history_arguments.dart';
|
||||
import '../../../domain/usecases/get_payment_summary_usecase.dart';
|
||||
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
||||
import '../../models/payment_stats.dart';
|
||||
@@ -27,7 +28,9 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
||||
final List<StaffPayment> allPayments = await getPaymentSummary();
|
||||
final PaymentStats stats = _calculateStats(allPayments);
|
||||
|
||||
final List<StaffPayment> history = await getPaymentHistory('week');
|
||||
final List<StaffPayment> history = await getPaymentHistory(
|
||||
const GetPaymentHistoryArguments('week'),
|
||||
);
|
||||
emit(PaymentsLoaded(
|
||||
summary: stats,
|
||||
history: history,
|
||||
@@ -44,10 +47,10 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
||||
) async {
|
||||
final PaymentsState currentState = state;
|
||||
if (currentState is PaymentsLoaded) {
|
||||
if (currentState.activePeriod == event.period) return;
|
||||
|
||||
try {
|
||||
final List<StaffPayment> newHistory = await getPaymentHistory(event.period);
|
||||
final List<StaffPayment> newHistory = await getPaymentHistory(
|
||||
GetPaymentHistoryArguments(event.period),
|
||||
);
|
||||
emit(currentState.copyWith(
|
||||
history: newHistory,
|
||||
activePeriod: event.period,
|
||||
|
||||
@@ -63,7 +63,7 @@ extension ProfileNavigator on IModularNavigator {
|
||||
|
||||
/// Navigates to the timecard page.
|
||||
void pushTimecard() {
|
||||
pushNamed('/time-card');
|
||||
pushNamed('../time-card');
|
||||
}
|
||||
|
||||
/// Navigates to the FAQs page.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/entities/time_card.dart';
|
||||
import '../../domain/repositories/time_card_repository.dart';
|
||||
|
||||
class TimeCardRepositoryImpl implements TimeCardRepository {
|
||||
final ShiftsRepositoryMock shiftsRepository;
|
||||
|
||||
TimeCardRepositoryImpl({required this.shiftsRepository});
|
||||
|
||||
@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();
|
||||
|
||||
// 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 TimeCard(
|
||||
id: shift.id,
|
||||
shiftTitle: shift.title,
|
||||
clientName: shift.clientName,
|
||||
date: DateTime.tryParse(shift.date) ?? DateTime.now(),
|
||||
startTime: shift.startTime,
|
||||
endTime: shift.endTime,
|
||||
totalHours: hours,
|
||||
hourlyRate: shift.hourlyRate,
|
||||
totalPay: hours * shift.hourlyRate,
|
||||
status: _mapStatus(shift.status),
|
||||
location: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// Arguments for the GetTimeCardsUseCase.
|
||||
class GetTimeCardsArguments extends UseCaseArgument {
|
||||
/// The month to fetch time cards for.
|
||||
final DateTime month;
|
||||
|
||||
const GetTimeCardsArguments(this.month);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [month];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import '../entities/time_card.dart';
|
||||
|
||||
/// Repository interface for accessing time card data.
|
||||
///
|
||||
/// This repository handles fetching time cards and related financial data
|
||||
/// for the staff member.
|
||||
abstract class TimeCardRepository {
|
||||
/// Retrieves a list of [TimeCard]s for a specific month.
|
||||
///
|
||||
/// [month] is a [DateTime] representing the month to filter by.
|
||||
Future<List<TimeCard>> getTimeCards(DateTime month);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../../domain/entities/time_card.dart';
|
||||
import '../arguments/get_time_cards_arguments.dart';
|
||||
import '../repositories/time_card_repository.dart';
|
||||
|
||||
/// UseCase to retrieve time cards for a given month.
|
||||
class GetTimeCardsUseCase extends UseCase<GetTimeCardsArguments, List<TimeCard>> {
|
||||
final TimeCardRepository repository;
|
||||
|
||||
GetTimeCardsUseCase(this.repository);
|
||||
|
||||
/// Executes the use case.
|
||||
///
|
||||
/// Returns a list of [TimeCard]s for the specified month in [arguments].
|
||||
@override
|
||||
Future<List<TimeCard>> call(GetTimeCardsArguments arguments) {
|
||||
return repository.getTimeCards(arguments.month);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/time_card.dart';
|
||||
import '../../domain/arguments/get_time_cards_arguments.dart';
|
||||
import '../../domain/usecases/get_time_cards_usecase.dart';
|
||||
|
||||
part 'time_card_event.dart';
|
||||
part 'time_card_state.dart';
|
||||
|
||||
/// BLoC to manage Time Card state.
|
||||
class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState> {
|
||||
final GetTimeCardsUseCase getTimeCards;
|
||||
|
||||
TimeCardBloc({required this.getTimeCards}) : super(TimeCardInitial()) {
|
||||
on<LoadTimeCards>(_onLoadTimeCards);
|
||||
on<ChangeMonth>(_onChangeMonth);
|
||||
}
|
||||
|
||||
/// Handles fetching time cards for the requested month.
|
||||
Future<void> _onLoadTimeCards(LoadTimeCards event, Emitter<TimeCardState> emit) async {
|
||||
emit(TimeCardLoading());
|
||||
try {
|
||||
final List<TimeCard> cards = await getTimeCards(GetTimeCardsArguments(event.month));
|
||||
|
||||
final double totalHours = cards.fold(0.0, (double sum, TimeCard t) => sum + t.totalHours);
|
||||
final double totalEarnings = cards.fold(0.0, (double sum, TimeCard t) => sum + t.totalPay);
|
||||
|
||||
emit(TimeCardLoaded(
|
||||
timeCards: cards,
|
||||
selectedMonth: event.month,
|
||||
totalHours: totalHours,
|
||||
totalEarnings: totalEarnings,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(TimeCardError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangeMonth(ChangeMonth event, Emitter<TimeCardState> emit) async {
|
||||
add(LoadTimeCards(event.month));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
part of 'time_card_bloc.dart';
|
||||
|
||||
abstract class TimeCardEvent extends Equatable {
|
||||
const TimeCardEvent();
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadTimeCards extends TimeCardEvent {
|
||||
final DateTime month;
|
||||
const LoadTimeCards(this.month);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [month];
|
||||
}
|
||||
|
||||
class ChangeMonth extends TimeCardEvent {
|
||||
final DateTime month;
|
||||
const ChangeMonth(this.month);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [month];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
part of 'time_card_bloc.dart';
|
||||
|
||||
abstract class TimeCardState extends Equatable {
|
||||
const TimeCardState();
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class TimeCardInitial extends TimeCardState {}
|
||||
class TimeCardLoading extends TimeCardState {}
|
||||
class TimeCardLoaded extends TimeCardState {
|
||||
final List<TimeCard> timeCards;
|
||||
final DateTime selectedMonth;
|
||||
final double totalHours;
|
||||
final double totalEarnings;
|
||||
|
||||
const TimeCardLoaded({
|
||||
required this.timeCards,
|
||||
required this.selectedMonth,
|
||||
required this.totalHours,
|
||||
required this.totalEarnings,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [timeCards, selectedMonth, totalHours, totalEarnings];
|
||||
}
|
||||
class TimeCardError extends TimeCardState {
|
||||
final String message;
|
||||
const TimeCardError(this.message);
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import '../blocs/time_card_bloc.dart';
|
||||
import '../widgets/month_selector.dart';
|
||||
import '../widgets/shift_history_list.dart';
|
||||
import '../widgets/time_card_summary.dart';
|
||||
|
||||
/// The main page for displaying the staff time card.
|
||||
class TimeCardPage extends StatefulWidget {
|
||||
const TimeCardPage({super.key});
|
||||
|
||||
@override
|
||||
State<TimeCardPage> createState() => _TimeCardPageState();
|
||||
}
|
||||
|
||||
class _TimeCardPageState extends State<TimeCardPage> {
|
||||
final TimeCardBloc _bloc = Modular.get<TimeCardBloc>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc.add(LoadTimeCards(DateTime.now()));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: UiColors.bgPrimary,
|
||||
appBar: AppBar(
|
||||
backgroundColor: UiColors.bgPopup,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
|
||||
onPressed: () => Modular.to.pop(),
|
||||
),
|
||||
title: Text(
|
||||
t.staff_time_card.title,
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
child: Container(color: UiColors.border, height: 1.0),
|
||||
),
|
||||
),
|
||||
body: BlocBuilder<TimeCardBloc, TimeCardState>(
|
||||
builder: (context, state) {
|
||||
if (state is TimeCardLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is TimeCardError) {
|
||||
return Center(child: Text('Error: ${state.message}'));
|
||||
} else if (state is TimeCardLoaded) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
MonthSelector(
|
||||
selectedDate: state.selectedMonth,
|
||||
onPreviousMonth: () => _bloc.add(ChangeMonth(
|
||||
DateTime(state.selectedMonth.year, state.selectedMonth.month - 1),
|
||||
)),
|
||||
onNextMonth: () => _bloc.add(ChangeMonth(
|
||||
DateTime(state.selectedMonth.year, state.selectedMonth.month + 1),
|
||||
)),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
TimeCardSummary(
|
||||
totalHours: state.totalHours,
|
||||
totalEarnings: state.totalEarnings,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ShiftHistoryList(timesheets: state.timeCards),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// A widget that allows the user to navigate between months.
|
||||
class MonthSelector extends StatelessWidget {
|
||||
final DateTime selectedDate;
|
||||
final VoidCallback onPreviousMonth;
|
||||
final VoidCallback onNextMonth;
|
||||
|
||||
const MonthSelector({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onPreviousMonth,
|
||||
required this.onNextMonth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space1),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
|
||||
onPressed: onPreviousMonth,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMM yyyy').format(selectedDate),
|
||||
style: UiTypography.title2b.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onPressed: onNextMonth,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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 'timesheet_card.dart';
|
||||
|
||||
/// Displays the list of shift history or an empty state.
|
||||
class ShiftHistoryList extends StatelessWidget {
|
||||
final List<TimeCard> timesheets;
|
||||
|
||||
const ShiftHistoryList({super.key, required this.timesheets});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.staff_time_card.shift_history,
|
||||
style: UiTypography.title2b.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
if (timesheets.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space12),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(UiIcons.clock, size: 48, color: UiColors.iconSecondary),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Text(
|
||||
t.staff_time_card.no_shifts,
|
||||
style: UiTypography.body1r.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...timesheets.map((ts) => TimesheetCard(timesheet: ts)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
/// Displays the total hours worked and total earnings for the selected month.
|
||||
class TimeCardSummary extends StatelessWidget {
|
||||
final double totalHours;
|
||||
final double totalEarnings;
|
||||
|
||||
const TimeCardSummary({
|
||||
super.key,
|
||||
required this.totalHours,
|
||||
required this.totalEarnings,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _SummaryCard(
|
||||
icon: UiIcons.clock,
|
||||
label: t.staff_time_card.hours_worked,
|
||||
value: totalHours.toStringAsFixed(1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: _SummaryCard(
|
||||
icon: UiIcons.dollar,
|
||||
label: t.staff_time_card.total_earnings,
|
||||
value: '\$${totalEarnings.toStringAsFixed(2)}',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _SummaryCard({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.headline1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
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';
|
||||
|
||||
/// A card widget displaying details of a single shift/timecard.
|
||||
class TimesheetCard extends StatelessWidget {
|
||||
final TimeCard timesheet;
|
||||
|
||||
const TimesheetCard({super.key, required this.timesheet});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = timesheet.status;
|
||||
Color statusBg;
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
|
||||
switch (status) {
|
||||
case TimeCardStatus.approved:
|
||||
statusBg = UiColors.textSuccess.withOpacity(0.12);
|
||||
statusColor = UiColors.textSuccess;
|
||||
statusText = t.staff_time_card.status.approved;
|
||||
break;
|
||||
case TimeCardStatus.disputed:
|
||||
statusBg = UiColors.destructive.withOpacity(0.12);
|
||||
statusColor = UiColors.destructive;
|
||||
statusText = t.staff_time_card.status.disputed;
|
||||
break;
|
||||
case TimeCardStatus.paid:
|
||||
statusBg = UiColors.primary.withOpacity(0.12);
|
||||
statusColor = UiColors.primary;
|
||||
statusText = t.staff_time_card.status.paid;
|
||||
break;
|
||||
case TimeCardStatus.pending:
|
||||
statusBg = UiColors.textWarning.withOpacity(0.12);
|
||||
statusColor = UiColors.textWarning;
|
||||
statusText = t.staff_time_card.status.pending;
|
||||
break;
|
||||
}
|
||||
|
||||
final dateStr = DateFormat('EEE, MMM d').format(timesheet.date);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
timesheet.shiftTitle,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timesheet.clientName,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusBg,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: UiTypography.titleUppercase4b.copyWith(
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Wrap(
|
||||
spacing: UiConstants.space3,
|
||||
runSpacing: UiConstants.space1,
|
||||
children: [
|
||||
_IconText(icon: UiIcons.calendar, text: dateStr),
|
||||
_IconText(
|
||||
icon: UiIcons.clock,
|
||||
text: '${_formatTime(timesheet.startTime)} - ${_formatTime(timesheet.endTime)}',
|
||||
),
|
||||
if (timesheet.location != null)
|
||||
_IconText(icon: UiIcons.mapPin, text: timesheet.location!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: UiConstants.space3),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${timesheet.totalHours.toStringAsFixed(1)} ${t.staff_time_card.hours} @ \$${timesheet.hourlyRate.toStringAsFixed(2)}${t.staff_time_card.per_hr}',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
Text(
|
||||
'\$${timesheet.totalPay.toStringAsFixed(2)}',
|
||||
style: UiTypography.title2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to safely format time strings like "HH:mm"
|
||||
String _formatTime(String t) {
|
||||
if (t.isEmpty) return '--:--';
|
||||
try {
|
||||
final parts = t.split(':');
|
||||
if (parts.length >= 2) {
|
||||
final dt = DateTime(2000, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
|
||||
return DateFormat('h:mm a').format(dt);
|
||||
}
|
||||
return t;
|
||||
} catch (_) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IconText extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
const _IconText({required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: UiColors.iconSecondary),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
text,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
library staff_time_card;
|
||||
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
|
||||
import 'data/repositories_impl/time_card_repository_impl.dart';
|
||||
import 'domain/repositories/time_card_repository.dart';
|
||||
import 'domain/usecases/get_time_cards_usecase.dart';
|
||||
import 'presentation/blocs/time_card_bloc.dart';
|
||||
import 'presentation/pages/time_card_page.dart';
|
||||
|
||||
export 'presentation/pages/time_card_page.dart';
|
||||
|
||||
class StaffTimeCardModule extends Module {
|
||||
@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);
|
||||
|
||||
// UseCases
|
||||
i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new);
|
||||
|
||||
// Blocs
|
||||
i.add<TimeCardBloc>(TimeCardBloc.new);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child('/', child: (context) => const TimeCardPage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'src/staff_time_card_module.dart';
|
||||
@@ -0,0 +1,31 @@
|
||||
name: staff_time_card
|
||||
description: Staff Time Card Feature
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: ">=3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_modular: ^6.3.2
|
||||
flutter_bloc: ^8.1.3
|
||||
equatable: ^2.0.5
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.0
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
core_localization:
|
||||
path: ../../../../../core_localization
|
||||
krow_core:
|
||||
path: ../../../../../core
|
||||
krow_domain:
|
||||
path: ../../../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@@ -12,6 +12,7 @@ import 'package:staff_certificates/staff_certificates.dart';
|
||||
import 'package:staff_attire/staff_attire.dart';
|
||||
import 'package:staff_shifts/staff_shifts.dart';
|
||||
import 'package:staff_payments/staff_payements.dart';
|
||||
import 'package:staff_time_card/staff_time_card.dart';
|
||||
|
||||
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
|
||||
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
||||
@@ -67,5 +68,9 @@ class StaffMainModule extends Module {
|
||||
'/certificates',
|
||||
module: StaffCertificatesModule(),
|
||||
);
|
||||
r.module(
|
||||
'/time-card',
|
||||
module: StaffTimeCardModule(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ dependencies:
|
||||
path: ../shifts
|
||||
staff_payments:
|
||||
path: ../payments
|
||||
staff_time_card:
|
||||
path: ../profile_sections/finances/time_card
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1121,6 +1121,13 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
staff_time_card:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "packages/features/staff/profile_sections/finances/time_card"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user