feat: implement staff availability, clock-in, payments and fix UI navigation

This commit is contained in:
Suriya
2026-01-30 21:46:44 +05:30
parent 56aab9e1f6
commit ac7874c634
55 changed files with 1373 additions and 463 deletions

View File

@@ -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;
}
}
}

View File

@@ -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>[

View File

@@ -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);
});
}
}

View File

@@ -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 {
),
),
),
*/
],
),
);

View File

@@ -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: