Merge remote-tracking branch 'origin/312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation' into 298-bug-enhance-order-creation-flow-implement-robust-timezone-handling-in-mobile-client-app
This commit is contained in:
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+1
|
version: 0.0.1
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
/// Locales: 2
|
/// Locales: 2
|
||||||
/// Strings: 1038 (519 per locale)
|
/// Strings: 1038 (519 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-01-29 at 06:58 UTC
|
/// Built on 2026-01-29 at 15:50 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -37,4 +37,7 @@ class UiConstants {
|
|||||||
static const double space12 = 48.0;
|
static const double space12 = 48.0;
|
||||||
static const double space14 = 56.0;
|
static const double space14 = 56.0;
|
||||||
static const double space16 = 64.0;
|
static const double space16 = 64.0;
|
||||||
|
static const double space20 = 80.0;
|
||||||
|
static const double space24 = 96.0;
|
||||||
|
static const double space32 = 128.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
import '../blocs/billing_bloc.dart';
|
import '../blocs/billing_bloc.dart';
|
||||||
import '../blocs/billing_event.dart';
|
import '../blocs/billing_event.dart';
|
||||||
import '../blocs/billing_state.dart';
|
import '../blocs/billing_state.dart';
|
||||||
import '../widgets/billing_header.dart';
|
import '../widgets/billing_header.dart';
|
||||||
import '../widgets/pending_invoices_section.dart';
|
|
||||||
import '../widgets/payment_method_card.dart';
|
|
||||||
import '../widgets/spending_breakdown_card.dart';
|
|
||||||
import '../widgets/savings_card.dart';
|
|
||||||
import '../widgets/invoice_history_section.dart';
|
import '../widgets/invoice_history_section.dart';
|
||||||
import '../widgets/export_invoices_button.dart';
|
import '../widgets/payment_method_card.dart';
|
||||||
|
import '../widgets/pending_invoices_section.dart';
|
||||||
|
import '../widgets/savings_card.dart';
|
||||||
|
import '../widgets/spending_breakdown_card.dart';
|
||||||
|
|
||||||
/// The entry point page for the client billing feature.
|
/// The entry point page for the client billing feature.
|
||||||
///
|
///
|
||||||
@@ -43,7 +43,6 @@ class BillingView extends StatelessWidget {
|
|||||||
return BlocBuilder<BillingBloc, BillingState>(
|
return BlocBuilder<BillingBloc, BillingState>(
|
||||||
builder: (BuildContext context, BillingState state) {
|
builder: (BuildContext context, BillingState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
BillingHeader(
|
BillingHeader(
|
||||||
@@ -89,9 +88,7 @@ class BillingView extends StatelessWidget {
|
|||||||
SavingsCard(savings: state.savings),
|
SavingsCard(savings: state.savings),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
InvoiceHistorySection(invoices: state.invoiceHistory),
|
InvoiceHistorySection(invoices: state.invoiceHistory),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space24),
|
||||||
const ExportInvoicesButton(),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: billing
|
name: billing
|
||||||
description: Client Billing feature package
|
description: Client Billing feature package
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 0.0.1
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class CoveragePage extends StatelessWidget {
|
|||||||
create: (BuildContext context) => Modular.get<CoverageBloc>()
|
create: (BuildContext context) => Modular.get<CoverageBloc>()
|
||||||
..add(CoverageLoadRequested(date: DateTime.now())),
|
..add(CoverageLoadRequested(date: DateTime.now())),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: UiColors.background,
|
|
||||||
body: BlocBuilder<CoverageBloc, CoverageState>(
|
body: BlocBuilder<CoverageBloc, CoverageState>(
|
||||||
builder: (BuildContext context, CoverageState state) {
|
builder: (BuildContext context, CoverageState state) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -121,6 +120,7 @@ class CoveragePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
CoverageShiftList(shifts: state.shifts),
|
CoverageShiftList(shifts: state.shifts),
|
||||||
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,24 +35,23 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
if (shifts.isEmpty) {
|
if (shifts.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space8),
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.bgPopup,
|
color: UiColors.bgPopup,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
spacing: UiConstants.space4,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(
|
const Icon(
|
||||||
UiIcons.users,
|
UiIcons.users,
|
||||||
size: UiConstants.space12,
|
size: UiConstants.space12,
|
||||||
color: UiColors.mutedForeground,
|
color: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
|
||||||
Text(
|
Text(
|
||||||
'No shifts scheduled for this day',
|
'No shifts scheduled for this day',
|
||||||
style: UiTypography.body2r.copyWith(
|
style: UiTypography.body2r.textSecondary,
|
||||||
color: UiColors.mutedForeground,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -160,12 +159,15 @@ class _ShiftHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
spacing: UiConstants.space4,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
width: UiConstants.space2,
|
width: UiConstants.space2,
|
||||||
@@ -175,42 +177,43 @@ class _ShiftHeader extends StatelessWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: UiTypography.body1b.copyWith(
|
style: UiTypography.body1b.textPrimary,
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(
|
Row(
|
||||||
UiIcons.mapPin,
|
spacing: UiConstants.space1,
|
||||||
size: UiConstants.space3,
|
children: <Widget>[
|
||||||
color: UiColors.mutedForeground,
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: UiConstants.space3,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
Expanded(child: Text(
|
||||||
|
location,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space1),
|
Row(
|
||||||
Text(
|
spacing: UiConstants.space1,
|
||||||
location,
|
children: <Widget>[
|
||||||
style: UiTypography.body3r.copyWith(
|
const Icon(
|
||||||
color: UiColors.mutedForeground,
|
UiIcons.clock,
|
||||||
),
|
size: UiConstants.space3,
|
||||||
),
|
color: UiColors.iconSecondary,
|
||||||
const SizedBox(width: UiConstants.space3),
|
),
|
||||||
const Icon(
|
Text(
|
||||||
UiIcons.clock,
|
startTime,
|
||||||
size: UiConstants.space3,
|
style: UiTypography.body3r.textSecondary,
|
||||||
color: UiColors.mutedForeground,
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(width: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
startTime,
|
|
||||||
style: UiTypography.body3r.copyWith(
|
|
||||||
color: UiColors.mutedForeground,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: client_coverage
|
name: client_coverage
|
||||||
description: Client coverage feature for tracking daily shift coverage and worker status
|
description: Client coverage feature for tracking daily shift coverage and worker status
|
||||||
version: 1.0.0
|
version: 0.0.1
|
||||||
publish_to: none
|
publish_to: none
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
@@ -33,4 +33,4 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ClientMainPage extends StatelessWidget {
|
|||||||
BlocProvider.of<ClientMainCubit>(context).navigateToTab(index);
|
BlocProvider.of<ClientMainCubit>(context).navigateToTab(index);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -99,13 +99,6 @@ class ClientMainBottomBar extends StatelessWidget {
|
|||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
),
|
),
|
||||||
_buildNavItem(
|
|
||||||
index: 4,
|
|
||||||
icon: UiIcons.chart,
|
|
||||||
label: t.client_main.tabs.reports,
|
|
||||||
activeColor: activeColor,
|
|
||||||
inactiveColor: inactiveColor,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
library client_home;
|
|
||||||
|
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'src/data/repositories_impl/home_repository_impl.dart';
|
import 'src/data/repositories_impl/home_repository_impl.dart';
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
|
|||||||
on<ClientHomeWidgetVisibilityToggled>(_onWidgetVisibilityToggled);
|
on<ClientHomeWidgetVisibilityToggled>(_onWidgetVisibilityToggled);
|
||||||
on<ClientHomeWidgetReordered>(_onWidgetReordered);
|
on<ClientHomeWidgetReordered>(_onWidgetReordered);
|
||||||
on<ClientHomeLayoutReset>(_onLayoutReset);
|
on<ClientHomeLayoutReset>(_onLayoutReset);
|
||||||
|
|
||||||
|
add(ClientHomeStarted());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ class ClientHomePage extends StatelessWidget {
|
|||||||
final TranslationsClientHomeEn i18n = t.client_home;
|
final TranslationsClientHomeEn i18n = t.client_home;
|
||||||
|
|
||||||
return BlocProvider<ClientHomeBloc>(
|
return BlocProvider<ClientHomeBloc>(
|
||||||
create: (BuildContext context) =>
|
create: (BuildContext context) => Modular.get<ClientHomeBloc>(),
|
||||||
Modular.get<ClientHomeBloc>()..add(ClientHomeStarted()),
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -59,19 +58,15 @@ class ClientHomePage extends StatelessWidget {
|
|||||||
100,
|
100,
|
||||||
),
|
),
|
||||||
onReorder: (int oldIndex, int newIndex) {
|
onReorder: (int oldIndex, int newIndex) {
|
||||||
BlocProvider.of<ClientHomeBloc>(context).add(
|
BlocProvider.of<ClientHomeBloc>(
|
||||||
ClientHomeWidgetReordered(oldIndex, newIndex),
|
context,
|
||||||
);
|
).add(ClientHomeWidgetReordered(oldIndex, newIndex));
|
||||||
},
|
},
|
||||||
children: state.widgetOrder.map((String id) {
|
children: state.widgetOrder.map((String id) {
|
||||||
return Container(
|
return Container(
|
||||||
key: ValueKey(id),
|
key: ValueKey<String>(id),
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||||
child: DashboardWidgetBuilder(
|
child: DashboardWidgetBuilder(id: id, state: state, isEditMode: true),
|
||||||
id: id,
|
|
||||||
state: state,
|
|
||||||
isEditMode: true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,21 +24,22 @@ class ActionsWidget extends StatelessWidget {
|
|||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||||
child: _ActionCard(
|
// Expanded(
|
||||||
title: i18n.rapid,
|
// child: _ActionCard(
|
||||||
subtitle: i18n.rapid_subtitle,
|
// title: i18n.rapid,
|
||||||
icon: UiIcons.zap,
|
// subtitle: i18n.rapid_subtitle,
|
||||||
color: const Color(0xFFFEF2F2),
|
// icon: UiIcons.zap,
|
||||||
borderColor: const Color(0xFFFECACA),
|
// color: const Color(0xFFFEF2F2),
|
||||||
iconBgColor: const Color(0xFFFEE2E2),
|
// borderColor: const Color(0xFFFECACA),
|
||||||
iconColor: const Color(0xFFDC2626),
|
// iconBgColor: const Color(0xFFFEE2E2),
|
||||||
textColor: const Color(0xFF7F1D1D),
|
// iconColor: const Color(0xFFDC2626),
|
||||||
subtitleColor: const Color(0xFFB91C1C),
|
// textColor: const Color(0xFF7F1D1D),
|
||||||
onTap: onRapidPressed,
|
// subtitleColor: const Color(0xFFB91C1C),
|
||||||
),
|
// onTap: onRapidPressed,
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: UiConstants.space2),
|
// ),
|
||||||
|
// const SizedBox(width: UiConstants.space2),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _ActionCard(
|
child: _ActionCard(
|
||||||
title: i18n.create_order,
|
title: i18n.create_order,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies:
|
|||||||
core_localization:
|
core_localization:
|
||||||
path: ../../../core_localization
|
path: ../../../core_localization
|
||||||
krow_domain: ^0.0.1
|
krow_domain: ^0.0.1
|
||||||
|
krow_data_connect: ^0.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: view_orders
|
name: view_orders
|
||||||
description: Client View Orders feature package
|
description: Client View Orders feature package
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 0.0.1
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -32,4 +32,4 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// ignore: unused_import
|
|
||||||
// import 'package:data_connect/data_connect.dart';
|
|
||||||
import '../../domain/entities/payment_summary.dart';
|
import '../../domain/entities/payment_summary.dart';
|
||||||
import '../../domain/entities/payment_transaction.dart';
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
import '../../domain/repositories/payments_repository.dart';
|
import '../../domain/repositories/payments_repository.dart';
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import '../../domain/entities/payment_summary.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
import '../../domain/repositories/payments_repository.dart';
|
import '../../domain/repositories/payments_repository.dart';
|
||||||
|
import '../datasources/payments_remote_datasource.dart';
|
||||||
|
|
||||||
/// Implementation of [PaymentsRepository].
|
/// 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 PaymentsRemoteDataSource remoteDataSource;
|
||||||
|
|
||||||
/// Creates a [PaymentsRepositoryImpl] with the given [financialRepository].
|
PaymentsRepositoryImpl({required this.remoteDataSource});
|
||||||
PaymentsRepositoryImpl({required this.financialRepository});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<StaffPayment>> getPayments() async {
|
Future<PaymentSummary> getPaymentSummary() async {
|
||||||
// TODO: Get actual logged in staff ID
|
return await remoteDataSource.fetchPaymentSummary();
|
||||||
return await financialRepository.getStaffPayments('staff_1');
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PaymentTransaction>> getPaymentHistory(String period) async {
|
||||||
|
return await remoteDataSource.fetchPaymentHistory(period);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class PaymentTransaction extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String location;
|
||||||
|
final String address;
|
||||||
|
final String workedTime;
|
||||||
|
final double amount;
|
||||||
|
final String status;
|
||||||
|
final int hours;
|
||||||
|
final double rate;
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
const PaymentTransaction({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.location,
|
||||||
|
required this.address,
|
||||||
|
required this.workedTime,
|
||||||
|
required this.amount,
|
||||||
|
required this.status,
|
||||||
|
required this.hours,
|
||||||
|
required this.rate,
|
||||||
|
required this.date,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
location,
|
||||||
|
address,
|
||||||
|
workedTime,
|
||||||
|
amount,
|
||||||
|
status,
|
||||||
|
hours,
|
||||||
|
rate,
|
||||||
|
date,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import '../entities/payment_summary.dart';
|
||||||
|
import '../entities/payment_transaction.dart';
|
||||||
|
|
||||||
/// Repository interface for Payments feature.
|
/// Repository interface for Payments feature.
|
||||||
///
|
///
|
||||||
/// Defines the contract for data access related to staff payments.
|
/// Defines the contract for data access related to staff payments.
|
||||||
/// Implementations of this interface should reside in the data layer.
|
/// Implementations of this interface should reside in the data layer.
|
||||||
abstract class PaymentsRepository {
|
abstract class PaymentsRepository {
|
||||||
/// Fetches the list of payments for the current staff member.
|
/// Fetches the payment summary (earnings).
|
||||||
Future<List<StaffPayment>> getPayments();
|
Future<PaymentSummary> getPaymentSummary();
|
||||||
|
|
||||||
|
/// Fetches the payment history for a specific period.
|
||||||
|
Future<List<PaymentTransaction>> getPaymentHistory(String period);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
import '../arguments/get_payment_history_arguments.dart';
|
import '../arguments/get_payment_history_arguments.dart';
|
||||||
|
import '../entities/payment_transaction.dart';
|
||||||
import '../repositories/payments_repository.dart';
|
import '../repositories/payments_repository.dart';
|
||||||
|
|
||||||
/// Use case to retrieve payment history filtered by a period.
|
/// Use case to retrieve payment history filtered by a period.
|
||||||
///
|
///
|
||||||
/// This use case delegates the data retrieval to [PaymentsRepository].
|
/// This use case delegates the data retrieval to [PaymentsRepository].
|
||||||
class GetPaymentHistoryUseCase extends UseCase<GetPaymentHistoryArguments, List<StaffPayment>> {
|
class GetPaymentHistoryUseCase extends UseCase<GetPaymentHistoryArguments, List<PaymentTransaction>> {
|
||||||
final PaymentsRepository repository;
|
final PaymentsRepository repository;
|
||||||
|
|
||||||
/// Creates a [GetPaymentHistoryUseCase].
|
/// Creates a [GetPaymentHistoryUseCase].
|
||||||
GetPaymentHistoryUseCase(this.repository);
|
GetPaymentHistoryUseCase(this.repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<StaffPayment>> call(GetPaymentHistoryArguments arguments) async {
|
Future<List<PaymentTransaction>> call(GetPaymentHistoryArguments arguments) async {
|
||||||
// TODO: Implement filtering by period
|
return await repository.getPaymentHistory(arguments.period);
|
||||||
return await repository.getPayments();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import '../entities/payment_summary.dart';
|
||||||
import '../repositories/payments_repository.dart';
|
import '../repositories/payments_repository.dart';
|
||||||
|
|
||||||
/// Use case to retrieve payment summary information.
|
/// Use case to retrieve payment summary information.
|
||||||
///
|
class GetPaymentSummaryUseCase extends NoInputUseCase<PaymentSummary> {
|
||||||
/// 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;
|
final PaymentsRepository repository;
|
||||||
|
|
||||||
/// Creates a [GetPaymentSummaryUseCase].
|
/// Creates a [GetPaymentSummaryUseCase].
|
||||||
GetPaymentSummaryUseCase(this.repository);
|
GetPaymentSummaryUseCase(this.repository);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<StaffPayment>> call() async {
|
Future<PaymentSummary> call() async {
|
||||||
return await repository.getPayments();
|
return await repository.getPaymentSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
import '../../../domain/arguments/get_payment_history_arguments.dart';
|
import '../../../domain/arguments/get_payment_history_arguments.dart';
|
||||||
import '../../../domain/usecases/get_payment_summary_usecase.dart';
|
import '../../../domain/entities/payment_summary.dart';
|
||||||
|
import '../../../domain/entities/payment_transaction.dart';
|
||||||
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
||||||
import '../../models/payment_stats.dart';
|
import '../../../domain/usecases/get_payment_summary_usecase.dart';
|
||||||
import 'payments_event.dart';
|
import 'payments_event.dart';
|
||||||
import 'payments_state.dart';
|
import 'payments_state.dart';
|
||||||
|
|
||||||
@@ -25,14 +25,13 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
|||||||
) async {
|
) async {
|
||||||
emit(PaymentsLoading());
|
emit(PaymentsLoading());
|
||||||
try {
|
try {
|
||||||
final List<StaffPayment> allPayments = await getPaymentSummary();
|
final PaymentSummary currentSummary = await getPaymentSummary();
|
||||||
final PaymentStats stats = _calculateStats(allPayments);
|
|
||||||
|
|
||||||
final List<StaffPayment> history = await getPaymentHistory(
|
final List<PaymentTransaction> history = await getPaymentHistory(
|
||||||
const GetPaymentHistoryArguments('week'),
|
const GetPaymentHistoryArguments('week'),
|
||||||
);
|
);
|
||||||
emit(PaymentsLoaded(
|
emit(PaymentsLoaded(
|
||||||
summary: stats,
|
summary: currentSummary,
|
||||||
history: history,
|
history: history,
|
||||||
activePeriod: 'week',
|
activePeriod: 'week',
|
||||||
));
|
));
|
||||||
@@ -48,7 +47,7 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
|||||||
final PaymentsState currentState = state;
|
final PaymentsState currentState = state;
|
||||||
if (currentState is PaymentsLoaded) {
|
if (currentState is PaymentsLoaded) {
|
||||||
try {
|
try {
|
||||||
final List<StaffPayment> newHistory = await getPaymentHistory(
|
final List<PaymentTransaction> newHistory = await getPaymentHistory(
|
||||||
GetPaymentHistoryArguments(event.period),
|
GetPaymentHistoryArguments(event.period),
|
||||||
);
|
);
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
@@ -60,38 +59,4 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaymentStats _calculateStats(List<StaffPayment> payments) {
|
|
||||||
double total = 0;
|
|
||||||
double pending = 0;
|
|
||||||
double weekly = 0;
|
|
||||||
double monthly = 0;
|
|
||||||
|
|
||||||
final DateTime now = DateTime.now();
|
|
||||||
|
|
||||||
for (final StaffPayment p in payments) {
|
|
||||||
// Assuming all payments count towards total history
|
|
||||||
total += p.amount;
|
|
||||||
|
|
||||||
if (p.status == PaymentStatus.pending) {
|
|
||||||
pending += p.amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.paidAt != null) {
|
|
||||||
if (now.difference(p.paidAt!).inDays < 7) {
|
|
||||||
weekly += p.amount;
|
|
||||||
}
|
|
||||||
if (now.month == p.paidAt!.month && now.year == p.paidAt!.year) {
|
|
||||||
monthly += p.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PaymentStats(
|
|
||||||
totalEarnings: total,
|
|
||||||
pendingEarnings: pending,
|
|
||||||
weeklyEarnings: weekly,
|
|
||||||
monthlyEarnings: monthly,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import '../../../domain/entities/payment_summary.dart';
|
||||||
import '../../models/payment_stats.dart';
|
import '../../../domain/entities/payment_transaction.dart';
|
||||||
|
|
||||||
abstract class PaymentsState extends Equatable {
|
abstract class PaymentsState extends Equatable {
|
||||||
const PaymentsState();
|
const PaymentsState();
|
||||||
@@ -14,8 +14,8 @@ class PaymentsInitial extends PaymentsState {}
|
|||||||
class PaymentsLoading extends PaymentsState {}
|
class PaymentsLoading extends PaymentsState {}
|
||||||
|
|
||||||
class PaymentsLoaded extends PaymentsState {
|
class PaymentsLoaded extends PaymentsState {
|
||||||
final PaymentStats summary;
|
final PaymentSummary summary;
|
||||||
final List<StaffPayment> history;
|
final List<PaymentTransaction> history;
|
||||||
final String activePeriod;
|
final String activePeriod;
|
||||||
|
|
||||||
const PaymentsLoaded({
|
const PaymentsLoaded({
|
||||||
@@ -25,8 +25,8 @@ class PaymentsLoaded extends PaymentsState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
PaymentsLoaded copyWith({
|
PaymentsLoaded copyWith({
|
||||||
PaymentStats? summary,
|
PaymentSummary? summary,
|
||||||
List<StaffPayment>? history,
|
List<PaymentTransaction>? history,
|
||||||
String? activePeriod,
|
String? activePeriod,
|
||||||
}) {
|
}) {
|
||||||
return PaymentsLoaded(
|
return PaymentsLoaded(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
import '../blocs/payments/payments_bloc.dart';
|
import '../blocs/payments/payments_bloc.dart';
|
||||||
import '../blocs/payments/payments_event.dart';
|
import '../blocs/payments/payments_event.dart';
|
||||||
import '../blocs/payments/payments_state.dart';
|
import '../blocs/payments/payments_state.dart';
|
||||||
@@ -177,19 +177,19 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Column(
|
Column(
|
||||||
children: state.history.map((StaffPayment payment) {
|
children: state.history.map((PaymentTransaction payment) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: PaymentHistoryItem(
|
child: PaymentHistoryItem(
|
||||||
amount: payment.amount,
|
amount: payment.amount,
|
||||||
title: 'Assignment ${payment.assignmentId}',
|
title: payment.title,
|
||||||
location: 'Location', // TODO: Fetch from assignment
|
location: payment.location,
|
||||||
address: '',
|
address: payment.address,
|
||||||
date: payment.paidAt != null ? DateFormat('E, MMM d').format(payment.paidAt!) : 'Pending',
|
date: DateFormat('E, MMM d').format(payment.date),
|
||||||
workedTime: '00:00 - 00:00', // TODO: Fetch from assignment
|
workedTime: payment.workedTime,
|
||||||
hours: 0,
|
hours: payment.hours,
|
||||||
rate: 0,
|
rate: payment.rate,
|
||||||
status: payment.status.toString().split('.').last,
|
status: payment.status,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ publish_to: none
|
|||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ name: staff_time_card
|
|||||||
description: Staff Time Card Feature
|
description: Staff Time Card Feature
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ publish_to: 'none'
|
|||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -31,4 +31,4 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|||||||
@@ -65,13 +65,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
billing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/client/billing"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "1.0.0+1"
|
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -192,13 +185,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.2"
|
version: "0.4.2"
|
||||||
client_coverage:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/client/client_coverage"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "1.0.0"
|
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1120,13 +1106,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
version: "1.12.1"
|
||||||
staff_attire:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/profile_sections/onboarding/attire"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.0.1"
|
|
||||||
staff_availability:
|
staff_availability:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1134,20 +1113,6 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
staff_bank_account:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/profile_sections/finances/staff_bank_account"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.0.1"
|
|
||||||
staff_certificates:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/profile_sections/compliance/certificates"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.0.1"
|
|
||||||
staff_clock_in:
|
staff_clock_in:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1155,41 +1120,6 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
staff_documents:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/profile_sections/compliance/documents"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.0.1"
|
|
||||||
staff_payments:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/payments"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.0.1"
|
|
||||||
staff_shifts:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/shifts"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.0.1"
|
|
||||||
staff_tax_forms:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "packages/features/staff/profile_sections/compliance/tax_forms"
|
|
||||||
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:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -12,16 +12,26 @@ workspace:
|
|||||||
- packages/features/staff/authentication
|
- packages/features/staff/authentication
|
||||||
- packages/features/staff/home
|
- packages/features/staff/home
|
||||||
- packages/features/staff/staff_main
|
- packages/features/staff/staff_main
|
||||||
|
- packages/features/staff/payments
|
||||||
- packages/features/staff/profile
|
- packages/features/staff/profile
|
||||||
- packages/features/staff/profile_sections/onboarding/emergency_contact
|
- packages/features/staff/profile_sections/onboarding/emergency_contact
|
||||||
- packages/features/staff/profile_sections/onboarding/experience
|
- packages/features/staff/profile_sections/onboarding/experience
|
||||||
- packages/features/staff/profile_sections/onboarding/profile_info
|
- packages/features/staff/profile_sections/onboarding/profile_info
|
||||||
|
- packages/features/staff/profile_sections/onboarding/attire
|
||||||
|
- packages/features/staff/profile_sections/finances/staff_bank_account
|
||||||
|
- packages/features/staff/profile_sections/finances/time_card
|
||||||
|
- packages/features/staff/profile_sections/compliance/certificates
|
||||||
|
- packages/features/staff/profile_sections/compliance/documents
|
||||||
|
- packages/features/staff/profile_sections/compliance/tax_forms
|
||||||
|
- packages/features/staff/shifts
|
||||||
- packages/features/client/authentication
|
- packages/features/client/authentication
|
||||||
|
- packages/features/client/billing
|
||||||
- packages/features/client/home
|
- packages/features/client/home
|
||||||
- packages/features/client/settings
|
- packages/features/client/settings
|
||||||
- packages/features/client/hubs
|
- packages/features/client/hubs
|
||||||
- packages/features/client/create_order
|
- packages/features/client/create_order
|
||||||
- packages/features/client/view_orders
|
- packages/features/client/view_orders
|
||||||
|
- packages/features/client/client_coverage
|
||||||
- packages/features/client/client_main
|
- packages/features/client/client_main
|
||||||
- apps/staff
|
- apps/staff
|
||||||
- apps/client
|
- apps/client
|
||||||
|
|||||||
Reference in New Issue
Block a user