feat: add staff payments feature with mock data source and UI components
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../../domain/entities/payment_summary.dart';
|
||||||
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
|
import 'payments_remote_datasource.dart';
|
||||||
|
|
||||||
|
class PaymentsMockDataSource implements PaymentsRemoteDataSource {
|
||||||
|
@override
|
||||||
|
Future<PaymentSummary> fetchPaymentSummary() async {
|
||||||
|
// Simulate network delay
|
||||||
|
await Future.delayed(const Duration(milliseconds: 800));
|
||||||
|
|
||||||
|
// Mock data matching the prototype
|
||||||
|
return const PaymentSummary(
|
||||||
|
weeklyEarnings: 847.50,
|
||||||
|
monthlyEarnings: 3240.0,
|
||||||
|
pendingEarnings: 285.0,
|
||||||
|
totalEarnings: 12450.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PaymentTransaction>> fetchPaymentHistory(String period) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
|
|
||||||
|
// Mock data matching the prototype
|
||||||
|
// In a real scenario, this would filter by 'period' (week/month/year)
|
||||||
|
return [
|
||||||
|
PaymentTransaction(
|
||||||
|
id: '1',
|
||||||
|
title: 'Cook',
|
||||||
|
location: 'LA Convention Center',
|
||||||
|
address: '1201 S Figueroa St, Los Angeles, CA 90015',
|
||||||
|
workedTime: '2:00 PM - 10:00 PM',
|
||||||
|
amount: 160.00,
|
||||||
|
status: 'PAID',
|
||||||
|
hours: 8,
|
||||||
|
rate: 20.0,
|
||||||
|
date: DateTime(2025, 12, 6), // "Sat, Dec 6" (Using future date to match context if needed, but keeping it simple)
|
||||||
|
),
|
||||||
|
PaymentTransaction(
|
||||||
|
id: '2',
|
||||||
|
title: 'Server',
|
||||||
|
location: 'The Grand Hotel',
|
||||||
|
address: '456 Main St, Los Angeles, CA 90012',
|
||||||
|
workedTime: '5:00 PM - 11:00 PM',
|
||||||
|
amount: 176.00,
|
||||||
|
status: 'PAID',
|
||||||
|
hours: 8,
|
||||||
|
rate: 22.0,
|
||||||
|
date: DateTime(2025, 12, 5), // "Fri, Dec 5"
|
||||||
|
),
|
||||||
|
PaymentTransaction(
|
||||||
|
id: '3',
|
||||||
|
title: 'Bartender',
|
||||||
|
location: 'Club Luxe',
|
||||||
|
address: '789 Sunset Blvd, Los Angeles, CA 90028',
|
||||||
|
workedTime: '6:00 PM - 2:00 AM',
|
||||||
|
amount: 225.00,
|
||||||
|
status: 'PAID',
|
||||||
|
hours: 9,
|
||||||
|
rate: 25.0,
|
||||||
|
date: DateTime(2025, 12, 4), // "Thu, Dec 4"
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import '../../domain/entities/payment_summary.dart';
|
||||||
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
|
|
||||||
|
abstract class PaymentsRemoteDataSource {
|
||||||
|
Future<PaymentSummary> fetchPaymentSummary();
|
||||||
|
Future<List<PaymentTransaction>> fetchPaymentHistory(String period);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
// import 'package:data_connect/data_connect.dart';
|
||||||
|
import '../../domain/entities/payment_summary.dart';
|
||||||
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
|
import '../../domain/repositories/payments_repository.dart';
|
||||||
|
import '../datasources/payments_remote_datasource.dart';
|
||||||
|
|
||||||
|
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||||
|
final PaymentsRemoteDataSource remoteDataSource;
|
||||||
|
|
||||||
|
PaymentsRepositoryImpl({required this.remoteDataSource});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaymentSummary> getPaymentSummary() async {
|
||||||
|
return await remoteDataSource.fetchPaymentSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import '../entities/payment_summary.dart';
|
||||||
|
import '../entities/payment_transaction.dart';
|
||||||
|
|
||||||
|
abstract class PaymentsRepository {
|
||||||
|
/// Fetches the summary of earnings (weekly, monthly, total, pending).
|
||||||
|
Future<PaymentSummary> getPaymentSummary();
|
||||||
|
|
||||||
|
/// Fetches the list of recent payment transactions (history).
|
||||||
|
Future<List<PaymentTransaction>> getPaymentHistory(String period);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import '../entities/payment_transaction.dart';
|
||||||
|
import '../repositories/payments_repository.dart';
|
||||||
|
|
||||||
|
class GetPaymentHistoryUseCase {
|
||||||
|
final PaymentsRepository repository;
|
||||||
|
|
||||||
|
GetPaymentHistoryUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<List<PaymentTransaction>> call({String period = 'week'}) async {
|
||||||
|
return await repository.getPaymentHistory(period);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import '../entities/payment_summary.dart';
|
||||||
|
import '../repositories/payments_repository.dart';
|
||||||
|
|
||||||
|
class GetPaymentSummaryUseCase {
|
||||||
|
final PaymentsRepository repository;
|
||||||
|
|
||||||
|
GetPaymentSummaryUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<PaymentSummary> call() async {
|
||||||
|
return await repository.getPaymentSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'domain/repositories/payments_repository.dart';
|
||||||
|
import 'domain/usecases/get_payment_summary_usecase.dart';
|
||||||
|
import 'domain/usecases/get_payment_history_usecase.dart';
|
||||||
|
import 'data/datasources/payments_remote_datasource.dart';
|
||||||
|
import 'data/datasources/payments_mock_datasource.dart';
|
||||||
|
import 'data/repositories/payments_repository_impl.dart';
|
||||||
|
import 'presentation/blocs/payments/payments_bloc.dart';
|
||||||
|
import 'presentation/pages/payments_page.dart';
|
||||||
|
|
||||||
|
class StaffPaymentsModule extends Module {
|
||||||
|
@override
|
||||||
|
void binds(Injector i) {
|
||||||
|
// Data Sources
|
||||||
|
i.add<PaymentsRemoteDataSource>(PaymentsMockDataSource.new);
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
i.add(GetPaymentSummaryUseCase.new);
|
||||||
|
i.add(GetPaymentHistoryUseCase.new);
|
||||||
|
|
||||||
|
// Blocs
|
||||||
|
i.add(PaymentsBloc.new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void routes(RouteManager r) {
|
||||||
|
r.child('/', child: (context) => const PaymentsPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/payment_summary.dart';
|
||||||
|
import '../../../domain/entities/payment_transaction.dart';
|
||||||
|
import '../../../domain/usecases/get_payment_summary_usecase.dart';
|
||||||
|
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
||||||
|
import 'payments_event.dart';
|
||||||
|
import 'payments_state.dart';
|
||||||
|
|
||||||
|
class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
||||||
|
final GetPaymentSummaryUseCase getPaymentSummary;
|
||||||
|
final GetPaymentHistoryUseCase getPaymentHistory;
|
||||||
|
|
||||||
|
PaymentsBloc({
|
||||||
|
required this.getPaymentSummary,
|
||||||
|
required this.getPaymentHistory,
|
||||||
|
}) : super(PaymentsInitial()) {
|
||||||
|
on<LoadPaymentsEvent>(_onLoadPayments);
|
||||||
|
on<ChangePeriodEvent>(_onChangePeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadPayments(
|
||||||
|
LoadPaymentsEvent event,
|
||||||
|
Emitter<PaymentsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(PaymentsLoading());
|
||||||
|
try {
|
||||||
|
final PaymentSummary summary = await getPaymentSummary();
|
||||||
|
final List<PaymentTransaction> history = await getPaymentHistory(period: 'week');
|
||||||
|
emit(PaymentsLoaded(
|
||||||
|
summary: summary,
|
||||||
|
history: history,
|
||||||
|
activePeriod: 'week',
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(PaymentsError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onChangePeriod(
|
||||||
|
ChangePeriodEvent event,
|
||||||
|
Emitter<PaymentsState> emit,
|
||||||
|
) async {
|
||||||
|
final PaymentsState currentState = state;
|
||||||
|
if (currentState is PaymentsLoaded) {
|
||||||
|
if (currentState.activePeriod == event.period) return;
|
||||||
|
|
||||||
|
// Optimistic update or set loading state if expecting delay
|
||||||
|
// For now, we'll keep the current data and fetch new history
|
||||||
|
try {
|
||||||
|
final List<PaymentTransaction> newHistory = await getPaymentHistory(period: event.period);
|
||||||
|
emit(currentState.copyWith(
|
||||||
|
history: newHistory,
|
||||||
|
activePeriod: event.period,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(PaymentsError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
abstract class PaymentsEvent extends Equatable {
|
||||||
|
const PaymentsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadPaymentsEvent extends PaymentsEvent {}
|
||||||
|
|
||||||
|
class ChangePeriodEvent extends PaymentsEvent {
|
||||||
|
final String period;
|
||||||
|
|
||||||
|
const ChangePeriodEvent(this.period);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [period];
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import '../../../domain/entities/payment_summary.dart';
|
||||||
|
import '../../../domain/entities/payment_transaction.dart';
|
||||||
|
|
||||||
|
abstract class PaymentsState extends Equatable {
|
||||||
|
const PaymentsState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentsInitial extends PaymentsState {}
|
||||||
|
|
||||||
|
class PaymentsLoading extends PaymentsState {}
|
||||||
|
|
||||||
|
class PaymentsLoaded extends PaymentsState {
|
||||||
|
final PaymentSummary summary;
|
||||||
|
final List<PaymentTransaction> history;
|
||||||
|
final String activePeriod;
|
||||||
|
|
||||||
|
const PaymentsLoaded({
|
||||||
|
required this.summary,
|
||||||
|
required this.history,
|
||||||
|
this.activePeriod = 'week',
|
||||||
|
});
|
||||||
|
|
||||||
|
PaymentsLoaded copyWith({
|
||||||
|
PaymentSummary? summary,
|
||||||
|
List<PaymentTransaction>? history,
|
||||||
|
String? activePeriod,
|
||||||
|
}) {
|
||||||
|
return PaymentsLoaded(
|
||||||
|
summary: summary ?? this.summary,
|
||||||
|
history: history ?? this.history,
|
||||||
|
activePeriod: activePeriod ?? this.activePeriod,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [summary, history, activePeriod];
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentsError extends PaymentsState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const PaymentsError(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import '../../domain/entities/payment_transaction.dart';
|
||||||
|
import '../blocs/payments/payments_bloc.dart';
|
||||||
|
import '../blocs/payments/payments_event.dart';
|
||||||
|
import '../blocs/payments/payments_state.dart';
|
||||||
|
import '../widgets/payment_stats_card.dart';
|
||||||
|
import '../widgets/pending_pay_card.dart';
|
||||||
|
import '../widgets/payment_history_item.dart';
|
||||||
|
|
||||||
|
class PaymentsPage extends StatefulWidget {
|
||||||
|
const PaymentsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PaymentsPage> createState() => _PaymentsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentsPageState extends State<PaymentsPage> {
|
||||||
|
final PaymentsBloc _bloc = Modular.get<PaymentsBloc>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_bloc.add(LoadPaymentsEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<PaymentsBloc>.value(
|
||||||
|
value: _bloc,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF8FAFC),
|
||||||
|
body: BlocBuilder<PaymentsBloc, PaymentsState>(
|
||||||
|
builder: (BuildContext context, PaymentsState state) {
|
||||||
|
if (state is PaymentsLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else if (state is PaymentsError) {
|
||||||
|
return Center(child: Text('Error: ${state.message}'));
|
||||||
|
} else if (state is PaymentsLoaded) {
|
||||||
|
return _buildContent(context, state);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, PaymentsLoaded state) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// Header Section with Gradient
|
||||||
|
Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: <Color>[Color(0xFF0032A0), Color(0xFF333F48)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
20,
|
||||||
|
MediaQuery.of(context).padding.top + 24,
|
||||||
|
20,
|
||||||
|
32,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text(
|
||||||
|
"Earnings",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Main Balance
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const Text(
|
||||||
|
"Total Earnings",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFFF8E08E),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"\$${state.summary.totalEarnings.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Period Tabs
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildTab("Week", 'week', state.activePeriod),
|
||||||
|
_buildTab("Month", 'month', state.activePeriod),
|
||||||
|
_buildTab("Year", 'year', state.activePeriod),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Main Content - Offset upwards
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(0, -16),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Quick Stats
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: PaymentStatsCard(
|
||||||
|
icon: LucideIcons.trendingUp,
|
||||||
|
iconColor: const Color(0xFF059669),
|
||||||
|
label: "This Week",
|
||||||
|
amount: "\$${state.summary.weeklyEarnings}",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: PaymentStatsCard(
|
||||||
|
icon: LucideIcons.calendar,
|
||||||
|
iconColor: const Color(0xFF2563EB),
|
||||||
|
label: "This Month",
|
||||||
|
amount: "\$${state.summary.monthlyEarnings.toStringAsFixed(0)}",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Pending Pay
|
||||||
|
PendingPayCard(
|
||||||
|
amount: state.summary.pendingEarnings,
|
||||||
|
onCashOut: () {
|
||||||
|
Modular.to.pushNamed('/early-pay');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Recent Payments
|
||||||
|
const Text(
|
||||||
|
"Recent Payments",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF0F172A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Column(
|
||||||
|
children: state.history.map((PaymentTransaction payment) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: PaymentHistoryItem(
|
||||||
|
amount: payment.amount,
|
||||||
|
title: payment.title,
|
||||||
|
location: payment.location,
|
||||||
|
address: payment.address,
|
||||||
|
date: DateFormat('E, MMM d').format(payment.date),
|
||||||
|
workedTime: payment.workedTime,
|
||||||
|
hours: payment.hours,
|
||||||
|
rate: payment.rate,
|
||||||
|
status: payment.status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Export History Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('PDF Exported'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(LucideIcons.download, size: 16),
|
||||||
|
label: const Text("Export History"),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFF0F172A),
|
||||||
|
side: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTab(String label, String value, String activePeriod) {
|
||||||
|
final bool isSelected = activePeriod == value;
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _bloc.add(ChangePeriodEvent(value)),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? Colors.white : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
|
||||||
|
class PaymentHistoryItem extends StatelessWidget {
|
||||||
|
final double amount;
|
||||||
|
final String title;
|
||||||
|
final String location;
|
||||||
|
final String address;
|
||||||
|
final String date;
|
||||||
|
final String workedTime;
|
||||||
|
final int hours;
|
||||||
|
final double rate;
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
const PaymentHistoryItem({
|
||||||
|
super.key,
|
||||||
|
required this.amount,
|
||||||
|
required this.title,
|
||||||
|
required this.location,
|
||||||
|
required this.address,
|
||||||
|
required this.date,
|
||||||
|
required this.workedTime,
|
||||||
|
required this.hours,
|
||||||
|
required this.rate,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Status Badge
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF3B82F6), // blue-500
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text(
|
||||||
|
"PAID",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF2563EB), // blue-600
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Icon
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF1F5F9), // slate-100
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
LucideIcons.dollarSign,
|
||||||
|
color: Color(0xFF334155), // slate-700
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF0F172A), // slate-900
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
location,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF475569), // slate-600
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"\$${amount.toStringAsFixed(0)}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF0F172A), // slate-900
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"\$${rate.toStringAsFixed(0)}/hr · ${hours}h",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Color(0xFF64748B), // slate-500
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Date and Time
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
LucideIcons.calendar,
|
||||||
|
size: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
date,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(
|
||||||
|
LucideIcons.clock,
|
||||||
|
size: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
workedTime,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
// Address
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
LucideIcons.mapPin,
|
||||||
|
size: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
address,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
|
||||||
|
class PaymentStatsCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final String label;
|
||||||
|
final String amount;
|
||||||
|
|
||||||
|
const PaymentStatsCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.label,
|
||||||
|
required this.amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: iconColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF64748B), // slate-500
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
amount,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF0F172A), // slate-900
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
|
||||||
|
class PendingPayCard extends StatelessWidget {
|
||||||
|
final double amount;
|
||||||
|
final VoidCallback onCashOut;
|
||||||
|
|
||||||
|
const PendingPayCard({
|
||||||
|
super.key,
|
||||||
|
required this.amount,
|
||||||
|
required this.onCashOut,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE8F0FF),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
LucideIcons.dollarSign,
|
||||||
|
color: Color(0xFF0047FF),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Pending",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF0F172A), // slate-900
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"\$${amount.toStringAsFixed(0)} available",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF475569), // slate-600
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: onCashOut,
|
||||||
|
icon: const Icon(LucideIcons.zap, size: 14),
|
||||||
|
label: const Text("Early Pay"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF0047FF),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: Colors.black.withOpacity(0.2),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export 'src/payments_module.dart';
|
||||||
28
apps/mobile/packages/features/staff/payments/pubspec.yaml
Normal file
28
apps/mobile/packages/features/staff/payments/pubspec.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: staff_payments
|
||||||
|
description: Staff Payments 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
|
||||||
|
lucide_icons: ^0.257.0
|
||||||
|
intl: ^0.20.0
|
||||||
|
|
||||||
|
# Internal packages
|
||||||
|
design_system:
|
||||||
|
path: ../../../design_system
|
||||||
|
core_localization:
|
||||||
|
path: ../../../core_localization
|
||||||
|
krow_domain:
|
||||||
|
path: ../../../domain
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^3.0.0
|
||||||
@@ -11,6 +11,7 @@ import 'package:staff_documents/staff_documents.dart';
|
|||||||
import 'package:staff_certificates/staff_certificates.dart';
|
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_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';
|
||||||
@@ -33,10 +34,9 @@ class StaffMainModule extends Module {
|
|||||||
StaffMainRoutes.shifts,
|
StaffMainRoutes.shifts,
|
||||||
module: StaffShiftsModule(),
|
module: StaffShiftsModule(),
|
||||||
),
|
),
|
||||||
ChildRoute<dynamic>(
|
ModuleRoute<dynamic>(
|
||||||
StaffMainRoutes.payments,
|
StaffMainRoutes.payments,
|
||||||
child: (BuildContext context) =>
|
module: StaffPaymentsModule(),
|
||||||
const PlaceholderPage(title: 'Payments'),
|
|
||||||
),
|
),
|
||||||
ModuleRoute<dynamic>(
|
ModuleRoute<dynamic>(
|
||||||
StaffMainRoutes.home,
|
StaffMainRoutes.home,
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ dependencies:
|
|||||||
path: ../profile_sections/onboarding/attire
|
path: ../profile_sections/onboarding/attire
|
||||||
staff_shifts:
|
staff_shifts:
|
||||||
path: ../shifts
|
path: ../shifts
|
||||||
# staff_payments:
|
staff_payments:
|
||||||
# path: ../payments
|
path: ../payments
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1100,6 +1100,13 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
staff_payments:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "packages/features/staff/payments"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
staff_shifts:
|
staff_shifts:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user