feat: implement staff availability, clock-in, payments and fix UI navigation
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/payments_repository.dart';
|
||||
|
||||
extension TimestampExt on Timestamp {
|
||||
DateTime toDate() {
|
||||
return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000);
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [PaymentsRepository].
|
||||
///
|
||||
/// This class handles the retrieval of payment data by delegating to the
|
||||
@@ -9,14 +17,49 @@ import '../../domain/repositories/payments_repository.dart';
|
||||
///
|
||||
/// It resides in the data layer and depends on the domain layer for the repository interface.
|
||||
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
final FinancialRepositoryMock financialRepository;
|
||||
PaymentsRepositoryImpl();
|
||||
|
||||
/// Creates a [PaymentsRepositoryImpl] with the given [financialRepository].
|
||||
PaymentsRepositoryImpl({required this.financialRepository});
|
||||
|
||||
@override
|
||||
Future<List<StaffPayment>> getPayments() async {
|
||||
// TODO: Get actual logged in staff ID
|
||||
return await financialRepository.getStaffPayments('staff_1');
|
||||
// Get current staff ID from session
|
||||
final session = StaffSessionStore.instance.session;
|
||||
|
||||
if (session?.staff?.id == null) return [];
|
||||
final String currentStaffId = session!.staff!.id;
|
||||
|
||||
|
||||
try {
|
||||
final response = await ExampleConnector.instance
|
||||
.listRecentPaymentsByStaffId(staffId: currentStaffId)
|
||||
.execute();
|
||||
|
||||
return response.data.recentPayments.map((payment) {
|
||||
return StaffPayment(
|
||||
id: payment.id,
|
||||
staffId: payment.staffId,
|
||||
assignmentId: payment.applicationId, // Application implies assignment
|
||||
amount: payment.invoice.amount,
|
||||
status: _mapStatus(payment.status),
|
||||
paidAt: payment.invoice.issueDate.toDate(),
|
||||
);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
// Fallback or empty list on error
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
PaymentStatus _mapStatus(EnumValue<RecentPaymentStatus>? status) {
|
||||
if (status == null || status is! Known) return PaymentStatus.pending;
|
||||
|
||||
switch ((status as Known).value) {
|
||||
case RecentPaymentStatus.PAID:
|
||||
return PaymentStatus.paid;
|
||||
case RecentPaymentStatus.PENDING:
|
||||
return PaymentStatus.pending;
|
||||
case RecentPaymentStatus.FAILED:
|
||||
return PaymentStatus.failed;
|
||||
default:
|
||||
return PaymentStatus.pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../blocs/payments/payments_state.dart';
|
||||
import '../widgets/payment_stats_card.dart';
|
||||
import '../widgets/pending_pay_card.dart';
|
||||
import '../widgets/payment_history_item.dart';
|
||||
import '../widgets/earnings_graph.dart';
|
||||
|
||||
class PaymentsPage extends StatefulWidget {
|
||||
const PaymentsPage({super.key});
|
||||
@@ -133,6 +134,12 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Earnings Graph
|
||||
EarningsGraph(
|
||||
payments: state.history,
|
||||
period: state.activePeriod,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Quick Stats
|
||||
Row(
|
||||
children: <Widget>[
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
class EarningsGraph extends StatelessWidget {
|
||||
final List<StaffPayment> payments;
|
||||
final String period;
|
||||
|
||||
const EarningsGraph({
|
||||
super.key,
|
||||
required this.payments,
|
||||
required this.period,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Basic data processing for the graph
|
||||
// We'll aggregate payments by date
|
||||
final validPayments = payments.where((p) => p.paidAt != null).toList()
|
||||
..sort((a, b) => a.paidAt!.compareTo(b.paidAt!));
|
||||
|
||||
// If no data, show empty state or simple placeholder
|
||||
if (validPayments.isEmpty) {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Center(child: Text("No sufficient data for graph")),
|
||||
);
|
||||
}
|
||||
|
||||
final spots = _generateSpots(validPayments);
|
||||
final maxX = spots.isNotEmpty ? spots.last.x : 0.0;
|
||||
final maxY = spots.isNotEmpty ? spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) : 0.0;
|
||||
|
||||
return Container(
|
||||
height: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Earnings Trend",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: false),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
// Simple logic to show a few dates
|
||||
if (value % 2 != 0) return const SizedBox();
|
||||
final index = value.toInt();
|
||||
if (index >= 0 && index < validPayments.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
DateFormat('d').format(validPayments[index].paidAt!),
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
color: const Color(0xFF0032A0),
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFF0032A0).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
minX: 0,
|
||||
maxX: (spots.length - 1).toDouble(),
|
||||
minY: 0,
|
||||
maxY: maxY * 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<FlSpot> _generateSpots(List<StaffPayment> data) {
|
||||
// Generate spots based on index in the list for simplicity in this demo
|
||||
// Real implementation would map to actual dates on X-axis
|
||||
return List.generate(data.length, (index) {
|
||||
return FlSpot(index.toDouble(), data[index].amount);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ class PendingPayCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
/*
|
||||
ElevatedButton.icon(
|
||||
onPressed: onCashOut,
|
||||
icon: const Icon(LucideIcons.zap, size: 14),
|
||||
@@ -91,6 +92,7 @@ class PendingPayCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,9 +11,11 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_data_connect:
|
||||
flutter_modular: ^6.3.2
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.0
|
||||
fl_chart: ^0.66.0
|
||||
|
||||
# Internal packages
|
||||
design_system:
|
||||
|
||||
Reference in New Issue
Block a user