Merge pull request #291 from Oloodi/208-p0-auth-05-get-started-screen
feat: Client Shifts Screen
This commit is contained in:
@@ -704,6 +704,21 @@
|
|||||||
"immediate_start": "Immediate start",
|
"immediate_start": "Immediate start",
|
||||||
"no_experience": "No experience"
|
"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",
|
"immediate_start": "Immediate start",
|
||||||
"no_experience": "No experience"
|
"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`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 2
|
/// 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:23 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// 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 TranslationsStaffCertificatesEn staff_certificates = TranslationsStaffCertificatesEn._(_root);
|
||||||
late final TranslationsStaffProfileAttireEn staff_profile_attire = TranslationsStaffProfileAttireEn._(_root);
|
late final TranslationsStaffProfileAttireEn staff_profile_attire = TranslationsStaffProfileAttireEn._(_root);
|
||||||
late final TranslationsStaffShiftsEn staff_shifts = TranslationsStaffShiftsEn._(_root);
|
late final TranslationsStaffShiftsEn staff_shifts = TranslationsStaffShiftsEn._(_root);
|
||||||
|
late final TranslationsStaffTimeCardEn staff_time_card = TranslationsStaffTimeCardEn._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: common
|
// Path: common
|
||||||
@@ -387,6 +388,38 @@ class TranslationsStaffShiftsEn {
|
|||||||
late final TranslationsStaffShiftsTagsEn tags = TranslationsStaffShiftsTagsEn._(_root);
|
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
|
// Path: staff_authentication.get_started_page
|
||||||
class TranslationsStaffAuthenticationGetStartedPageEn {
|
class TranslationsStaffAuthenticationGetStartedPageEn {
|
||||||
TranslationsStaffAuthenticationGetStartedPageEn._(this._root);
|
TranslationsStaffAuthenticationGetStartedPageEn._(this._root);
|
||||||
@@ -1691,6 +1724,27 @@ class TranslationsStaffShiftsTagsEn {
|
|||||||
String get no_experience => 'No experience';
|
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
|
// Path: staff_authentication.profile_setup_page.steps
|
||||||
class TranslationsStaffAuthenticationProfileSetupPageStepsEn {
|
class TranslationsStaffAuthenticationProfileSetupPageStepsEn {
|
||||||
TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root);
|
TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root);
|
||||||
@@ -3113,6 +3167,19 @@ extension on Translations {
|
|||||||
'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago',
|
'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago',
|
||||||
'staff_shifts.tags.immediate_start' => 'Immediate start',
|
'staff_shifts.tags.immediate_start' => 'Immediate start',
|
||||||
'staff_shifts.tags.no_experience' => 'No experience',
|
'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,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class TranslationsEs with BaseTranslations<AppLocale, Translations> implements T
|
|||||||
@override late final _TranslationsStaffCertificatesEs staff_certificates = _TranslationsStaffCertificatesEs._(_root);
|
@override late final _TranslationsStaffCertificatesEs staff_certificates = _TranslationsStaffCertificatesEs._(_root);
|
||||||
@override late final _TranslationsStaffProfileAttireEs staff_profile_attire = _TranslationsStaffProfileAttireEs._(_root);
|
@override late final _TranslationsStaffProfileAttireEs staff_profile_attire = _TranslationsStaffProfileAttireEs._(_root);
|
||||||
@override late final _TranslationsStaffShiftsEs staff_shifts = _TranslationsStaffShiftsEs._(_root);
|
@override late final _TranslationsStaffShiftsEs staff_shifts = _TranslationsStaffShiftsEs._(_root);
|
||||||
|
@override late final _TranslationsStaffTimeCardEs staff_time_card = _TranslationsStaffTimeCardEs._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: common
|
// Path: common
|
||||||
@@ -292,6 +293,23 @@ class _TranslationsStaffShiftsEs implements TranslationsStaffShiftsEn {
|
|||||||
@override late final _TranslationsStaffShiftsTagsEs tags = _TranslationsStaffShiftsTagsEs._(_root);
|
@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
|
// Path: staff_authentication.get_started_page
|
||||||
class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn {
|
class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn {
|
||||||
_TranslationsStaffAuthenticationGetStartedPageEs._(this._root);
|
_TranslationsStaffAuthenticationGetStartedPageEs._(this._root);
|
||||||
@@ -1049,6 +1067,19 @@ class _TranslationsStaffShiftsTagsEs implements TranslationsStaffShiftsTagsEn {
|
|||||||
@override String get no_experience => 'No experience';
|
@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
|
// Path: staff_authentication.profile_setup_page.steps
|
||||||
class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn {
|
class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn {
|
||||||
_TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root);
|
_TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root);
|
||||||
@@ -2091,6 +2122,19 @@ extension on TranslationsEs {
|
|||||||
'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago',
|
'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago',
|
||||||
'staff_shifts.tags.immediate_start' => 'Immediate start',
|
'staff_shifts.tags.immediate_start' => 'Immediate start',
|
||||||
'staff_shifts.tags.no_experience' => 'No experience',
|
'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,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export 'src/entities/business/business.dart';
|
|||||||
export 'src/entities/business/business_setting.dart';
|
export 'src/entities/business/business_setting.dart';
|
||||||
export 'src/entities/business/hub.dart';
|
export 'src/entities/business/hub.dart';
|
||||||
export 'src/entities/business/hub_department.dart';
|
export 'src/entities/business/hub_department.dart';
|
||||||
|
export 'src/entities/business/vendor.dart';
|
||||||
|
|
||||||
// Events & Assignments
|
// Events & Assignments
|
||||||
export 'src/entities/events/event.dart';
|
export 'src/entities/events/event.dart';
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ extension AuthNavigator on IModularNavigator {
|
|||||||
|
|
||||||
/// Navigates to the worker home (external to this module).
|
/// Navigates to the worker home (external to this module).
|
||||||
void pushWorkerHome() {
|
void pushWorkerHome() {
|
||||||
pushNamed('/worker-main/home/');
|
pushNamed('/worker-main/home');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import 'package:krow_data_connect/krow_data_connect.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/repositories/payments_repository.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 {
|
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||||
final FinancialRepositoryMock financialRepository;
|
final FinancialRepositoryMock financialRepository;
|
||||||
|
|
||||||
|
/// Creates a [PaymentsRepositoryImpl] with the given [financialRepository].
|
||||||
PaymentsRepositoryImpl({required this.financialRepository});
|
PaymentsRepositoryImpl({required this.financialRepository});
|
||||||
|
|
||||||
@override
|
@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';
|
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 {
|
abstract class PaymentsRepository {
|
||||||
/// Fetches the list of payments.
|
/// Fetches the list of payments for the current staff member.
|
||||||
Future<List<StaffPayment>> getPayments();
|
Future<List<StaffPayment>> getPayments();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../arguments/get_payment_history_arguments.dart';
|
||||||
import '../repositories/payments_repository.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;
|
final PaymentsRepository repository;
|
||||||
|
|
||||||
|
/// Creates a [GetPaymentHistoryUseCase].
|
||||||
GetPaymentHistoryUseCase(this.repository);
|
GetPaymentHistoryUseCase(this.repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<StaffPayment>> call(String period) async {
|
Future<List<StaffPayment>> call(GetPaymentHistoryArguments arguments) async {
|
||||||
// TODO: Implement filtering by period
|
// TODO: Implement filtering by period
|
||||||
return await repository.getPayments();
|
return await repository.getPayments();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import 'package:krow_core/core.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../repositories/payments_repository.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>> {
|
class GetPaymentSummaryUseCase extends NoInputUseCase<List<StaffPayment>> {
|
||||||
final PaymentsRepository repository;
|
final PaymentsRepository repository;
|
||||||
|
|
||||||
|
/// Creates a [GetPaymentSummaryUseCase].
|
||||||
GetPaymentSummaryUseCase(this.repository);
|
GetPaymentSummaryUseCase(this.repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_domain/krow_domain.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_summary_usecase.dart';
|
||||||
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
||||||
import '../../models/payment_stats.dart';
|
import '../../models/payment_stats.dart';
|
||||||
@@ -27,7 +28,9 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
|||||||
final List<StaffPayment> allPayments = await getPaymentSummary();
|
final List<StaffPayment> allPayments = await getPaymentSummary();
|
||||||
final PaymentStats stats = _calculateStats(allPayments);
|
final PaymentStats stats = _calculateStats(allPayments);
|
||||||
|
|
||||||
final List<StaffPayment> history = await getPaymentHistory('week');
|
final List<StaffPayment> history = await getPaymentHistory(
|
||||||
|
const GetPaymentHistoryArguments('week'),
|
||||||
|
);
|
||||||
emit(PaymentsLoaded(
|
emit(PaymentsLoaded(
|
||||||
summary: stats,
|
summary: stats,
|
||||||
history: history,
|
history: history,
|
||||||
@@ -44,10 +47,10 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
|||||||
) async {
|
) async {
|
||||||
final PaymentsState currentState = state;
|
final PaymentsState currentState = state;
|
||||||
if (currentState is PaymentsLoaded) {
|
if (currentState is PaymentsLoaded) {
|
||||||
if (currentState.activePeriod == event.period) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final List<StaffPayment> newHistory = await getPaymentHistory(event.period);
|
final List<StaffPayment> newHistory = await getPaymentHistory(
|
||||||
|
GetPaymentHistoryArguments(event.period),
|
||||||
|
);
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
history: newHistory,
|
history: newHistory,
|
||||||
activePeriod: event.period,
|
activePeriod: event.period,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ extension ProfileNavigator on IModularNavigator {
|
|||||||
|
|
||||||
/// Navigates to the timecard page.
|
/// Navigates to the timecard page.
|
||||||
void pushTimecard() {
|
void pushTimecard() {
|
||||||
pushNamed('/time-card');
|
pushNamed('../time-card');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the FAQs page.
|
/// 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_attire/staff_attire.dart';
|
||||||
import 'package:staff_shifts/staff_shifts.dart';
|
import 'package:staff_shifts/staff_shifts.dart';
|
||||||
import 'package:staff_payments/staff_payements.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/blocs/staff_main_cubit.dart';
|
||||||
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
||||||
@@ -30,27 +31,58 @@ class StaffMainModule extends Module {
|
|||||||
'/',
|
'/',
|
||||||
child: (BuildContext context) => const StaffMainPage(),
|
child: (BuildContext context) => const StaffMainPage(),
|
||||||
children: <ParallelRoute<dynamic>>[
|
children: <ParallelRoute<dynamic>>[
|
||||||
ModuleRoute<dynamic>(
|
/// TOODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||||
|
/// Replace PlaceholderPage with actual module when implemented
|
||||||
|
ChildRoute<dynamic>(
|
||||||
StaffMainRoutes.shifts,
|
StaffMainRoutes.shifts,
|
||||||
module: StaffShiftsModule(),
|
child: (BuildContext context) => const PlaceholderPage(title: 'Shifts'),
|
||||||
),
|
),
|
||||||
ModuleRoute<dynamic>(
|
// ModuleRoute<dynamic>(
|
||||||
|
// StaffMainRoutes.shifts,
|
||||||
|
// module: StaffShiftsModule(),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
/// TOODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||||
|
/// Replace PlaceholderPage with actual module when implemented
|
||||||
|
ChildRoute<dynamic>(
|
||||||
StaffMainRoutes.payments,
|
StaffMainRoutes.payments,
|
||||||
module: StaffPaymentsModule(),
|
child: (BuildContext context) => const PlaceholderPage(title: 'Payments'),
|
||||||
),
|
),
|
||||||
ModuleRoute<dynamic>(
|
// ModuleRoute<dynamic>(
|
||||||
|
// StaffMainRoutes.payments,
|
||||||
|
// module: StaffPaymentsModule(),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
|
||||||
|
/// TOODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||||
|
/// Replace PlaceholderPage with actual module when implemented
|
||||||
|
ChildRoute<dynamic>(
|
||||||
StaffMainRoutes.home,
|
StaffMainRoutes.home,
|
||||||
module: StaffHomeModule(),
|
child: (BuildContext context) => const PlaceholderPage(title: 'Home'),
|
||||||
),
|
),
|
||||||
|
// ModuleRoute<dynamic>(
|
||||||
|
// StaffMainRoutes.home,
|
||||||
|
// module: StaffHomeModule(),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
|
||||||
ChildRoute<dynamic>(
|
ChildRoute<dynamic>(
|
||||||
StaffMainRoutes.clockIn,
|
StaffMainRoutes.clockIn,
|
||||||
child: (BuildContext context) =>
|
child: (BuildContext context) =>
|
||||||
const PlaceholderPage(title: 'Clock In'),
|
const PlaceholderPage(title: 'Clock In'),
|
||||||
),
|
),
|
||||||
ModuleRoute<dynamic>(
|
|
||||||
|
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||||
|
/// Replace PlaceholderPage with actual module when implemented
|
||||||
|
ChildRoute<dynamic>(
|
||||||
StaffMainRoutes.profile,
|
StaffMainRoutes.profile,
|
||||||
module: StaffProfileModule(),
|
child: (BuildContext context) => const PlaceholderPage(title: 'Profile'),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// ModuleRoute<dynamic>(
|
||||||
|
// StaffMainRoutes.profile,
|
||||||
|
// module: StaffProfileModule(),
|
||||||
|
// ),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||||
@@ -67,5 +99,9 @@ class StaffMainModule extends Module {
|
|||||||
'/certificates',
|
'/certificates',
|
||||||
module: StaffCertificatesModule(),
|
module: StaffCertificatesModule(),
|
||||||
);
|
);
|
||||||
|
r.module(
|
||||||
|
'/time-card',
|
||||||
|
module: StaffTimeCardModule(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ dependencies:
|
|||||||
path: ../shifts
|
path: ../shifts
|
||||||
staff_payments:
|
staff_payments:
|
||||||
path: ../payments
|
path: ../payments
|
||||||
|
staff_time_card:
|
||||||
|
path: ../profile_sections/finances/time_card
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1121,6 +1121,13 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
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:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user