Feat: done add features revenue monthly
This commit is contained in:
parent
6edf8d531e
commit
986ff5445e
|
@ -680,7 +680,7 @@
|
|||
"languageVersion": "3.4"
|
||||
}
|
||||
],
|
||||
"generated": "2025-05-14T18:46:38.233635Z",
|
||||
"generated": "2025-05-15T05:07:55.383919Z",
|
||||
"generator": "pub",
|
||||
"generatorVersion": "3.5.0",
|
||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
||||
|
|
|
@ -9,6 +9,14 @@ class StatisticRepositoryImpl implements StatisticRepository {
|
|||
Timestamp.fromDate(DateTime(d.year, d.month, d.day));
|
||||
Timestamp _tsToDate(DateTime d) =>
|
||||
Timestamp.fromDate(DateTime(d.year, d.month, d.day).add(const Duration(days: 1)));
|
||||
|
||||
// Helper untuk menentukan tanggal awal bulan
|
||||
Timestamp _tsFirstDayOfMonth(DateTime month) =>
|
||||
Timestamp.fromDate(DateTime(month.year, month.month, 1));
|
||||
|
||||
// Helper untuk menentukan tanggal awal bulan berikutnya
|
||||
Timestamp _tsFirstDayOfNextMonth(DateTime month) =>
|
||||
Timestamp.fromDate(DateTime(month.year, month.month + 1, 1));
|
||||
|
||||
Query _baseQuery({
|
||||
required String porterId,
|
||||
|
@ -86,4 +94,55 @@ class StatisticRepositoryImpl implements StatisticRepository {
|
|||
return total;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<double> getMonthlyRevenue({
|
||||
required String porterId,
|
||||
required DateTime month,
|
||||
}) {
|
||||
return _firestore
|
||||
.collection('porterTransactions')
|
||||
.where('porterUserId', isEqualTo: porterId)
|
||||
.where('status', isEqualTo: 'selesai')
|
||||
.where('createdAt', isGreaterThanOrEqualTo: _tsFirstDayOfMonth(month))
|
||||
.where('createdAt', isLessThan: _tsFirstDayOfNextMonth(month))
|
||||
.snapshots()
|
||||
.asyncMap((snap) async {
|
||||
double total = 0;
|
||||
for (final doc in snap.docs) {
|
||||
final data = doc.data();
|
||||
final ticketId = (data as Map<String, dynamic>?)?['ticketId'] ?? '';
|
||||
final transactionId = (data as Map<String, dynamic>?)?['transactionId'] ?? '';
|
||||
if (ticketId != null && transactionId != null) {
|
||||
// join ke sub‐collection payments
|
||||
final payDoc = await _firestore
|
||||
.collection('tickets')
|
||||
.doc(ticketId)
|
||||
.collection('payments')
|
||||
.doc(transactionId)
|
||||
.get();
|
||||
if (payDoc.exists && payDoc.data()!.containsKey('porterServiceDetails')) {
|
||||
final ps = payDoc['porterServiceDetails'] as Map<String, dynamic>;
|
||||
|
||||
// Menambahkan semua service yang ada (arrival, departure, transit)
|
||||
if (ps.containsKey('arrival') && ps['arrival'] is Map<String, dynamic>) {
|
||||
final price = (ps['arrival']['price'] as num?)?.toDouble() ?? 0.0;
|
||||
total += price;
|
||||
}
|
||||
|
||||
if (ps.containsKey('departure') && ps['departure'] is Map<String, dynamic>) {
|
||||
final price = (ps['departure']['price'] as num?)?.toDouble() ?? 0.0;
|
||||
total += price;
|
||||
}
|
||||
|
||||
if (ps.containsKey('transit') && ps['transit'] is Map<String, dynamic>) {
|
||||
final price = (ps['transit']['price'] as num?)?.toDouble() ?? 0.0;
|
||||
total += price;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,4 +18,9 @@ abstract class StatisticRepository {
|
|||
required String porterId,
|
||||
required DateTime date,
|
||||
});
|
||||
|
||||
Stream<double> getMonthlyRevenue({
|
||||
required String porterId,
|
||||
required DateTime month,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,4 +23,9 @@ class StatisticUseCase {
|
|||
required String porterId,
|
||||
required DateTime date,
|
||||
}) => _repo.getRevenue(porterId: porterId, date: date);
|
||||
|
||||
Stream<double> getMonthlyRevenue({
|
||||
required String porterId,
|
||||
required DateTime month,
|
||||
}) => _repo.getMonthlyRevenue(porterId: porterId, month: month);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
import 'package:e_porter/_core/utils/snackbar/snackbar_helper.dart';
|
||||
import 'package:e_porter/domain/usecases/statistic_usecase.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class StatisticController extends GetxController {
|
||||
final StatisticUseCase useCase;
|
||||
final String porterId;
|
||||
|
||||
final date = DateTime.now().obs;
|
||||
final month = DateTime.now().obs;
|
||||
final monthlyDateRange = ''.obs;
|
||||
|
||||
final incoming = 0.obs;
|
||||
final incoming = 0.obs;
|
||||
final inProgress = 0.obs;
|
||||
final completed = 0.obs;
|
||||
final revenue = 0.0.obs;
|
||||
final completed = 0.obs;
|
||||
final revenue = 0.0.obs;
|
||||
final monthlyRevenue = 0.0.obs;
|
||||
|
||||
Timer? _monthlyUpdateTimer;
|
||||
|
||||
StatisticController({
|
||||
required this.useCase,
|
||||
|
@ -22,6 +30,55 @@ class StatisticController extends GetxController {
|
|||
super.onInit();
|
||||
_bindAllStreams();
|
||||
ever(date, (_) => _bindAllStreams());
|
||||
ever(month, (_) {
|
||||
_bindMonthlyStream();
|
||||
_updateMonthlyDateRange();
|
||||
});
|
||||
|
||||
_updateMonthlyDateRange();
|
||||
_setupMonthlyUpdateTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_monthlyUpdateTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _setupMonthlyUpdateTimer() {
|
||||
_monthlyUpdateTimer?.cancel();
|
||||
|
||||
final now = DateTime.now();
|
||||
final nextMidnight = DateTime(now.year, now.month, now.day + 1);
|
||||
final timeUntilMidnight = nextMidnight.difference(now);
|
||||
|
||||
_monthlyUpdateTimer = Timer(timeUntilMidnight, () {
|
||||
final currentMonth = DateTime.now();
|
||||
if (currentMonth.month != month.value.month || currentMonth.year != month.value.year) {
|
||||
month.value = currentMonth;
|
||||
}
|
||||
|
||||
_setupMonthlyUpdateTimer();
|
||||
});
|
||||
}
|
||||
|
||||
DateTime _getFirstDayOfMonth(DateTime month) {
|
||||
return DateTime(month.year, month.month, 1);
|
||||
}
|
||||
|
||||
DateTime _getLastDayOfMonth(DateTime month) {
|
||||
return DateTime(month.year, month.month + 1, 0);
|
||||
}
|
||||
|
||||
void _updateMonthlyDateRange() {
|
||||
final firstDay = _getFirstDayOfMonth(month.value);
|
||||
final lastDay = _getLastDayOfMonth(month.value);
|
||||
|
||||
final formatter = DateFormat('dd/MM/yyyy');
|
||||
final startDateStr = formatter.format(firstDay);
|
||||
final endDateStr = formatter.format(lastDay);
|
||||
|
||||
monthlyDateRange.value = '$startDateStr - $endDateStr';
|
||||
}
|
||||
|
||||
void _bindAllStreams() {
|
||||
|
@ -37,7 +94,40 @@ class StatisticController extends GetxController {
|
|||
revenue.bindStream(
|
||||
useCase.getRevenue(porterId: porterId, date: date.value),
|
||||
);
|
||||
|
||||
_bindMonthlyStream();
|
||||
}
|
||||
|
||||
void _bindMonthlyStream() {
|
||||
try {
|
||||
monthlyRevenue.bindStream(useCase.getMonthlyRevenue(porterId: porterId, month: month.value).handleError((error) {
|
||||
print('Error fetching monthly revenue: $error');
|
||||
SnackbarHelper.showError('Koneksi Gagal', 'Tidak dapat memuat data pendapatan. Periksa koneksi internet Anda.');
|
||||
return 0.0;
|
||||
}));
|
||||
} catch (e) {
|
||||
print('Exception in bindMonthlyStream: $e');
|
||||
monthlyRevenue.value = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void changeDate(DateTime newDate) => date.value = newDate;
|
||||
|
||||
void changeMonth(DateTime newMonth) {
|
||||
month.value = newMonth;
|
||||
}
|
||||
|
||||
void resetToCurrentMonth() {
|
||||
month.value = DateTime.now();
|
||||
}
|
||||
|
||||
void previousMonth() {
|
||||
final current = month.value;
|
||||
month.value = DateTime(current.year, current.month - 1, 1);
|
||||
}
|
||||
|
||||
void nextMonth() {
|
||||
final current = month.value;
|
||||
month.value = DateTime(current.year, current.month + 1, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:zoom_tap_animation/zoom_tap_animation.dart';
|
||||
|
||||
class DateSetting extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
final Widget icon;
|
||||
|
||||
const DateSetting({
|
||||
Key? key,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ZoomTapAnimation(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.01),
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: 14,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import 'package:e_porter/_core/service/preferences_service.dart';
|
|||
import 'package:e_porter/presentation/controllers/porter_queue_controller.dart';
|
||||
import 'package:e_porter/presentation/controllers/statistic_controller.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/card_service_porter.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/date_setting.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/profile_avatar.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/summary_card.dart';
|
||||
import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
|
||||
|
@ -392,6 +393,49 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TypographyStyles.h6('Pendapatan Perbulan', color: GrayColors.gray800),
|
||||
Row(
|
||||
children: [
|
||||
DateSetting(
|
||||
onTap: () => _statisticController.previousMonth(),
|
||||
icon: Icon(Icons.chevron_left, size: 20.r, color: PrimaryColors.primary800),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
DateSetting(
|
||||
onTap: () => _statisticController.resetToCurrentMonth(),
|
||||
icon: Icon(Icons.calendar_today, size: 20.r, color: PrimaryColors.primary800)),
|
||||
SizedBox(width: 8.w),
|
||||
DateSetting(
|
||||
onTap: () => _statisticController.previousMonth(),
|
||||
icon: Icon(Icons.chevron_right, size: 20.r, color: GrayColors.gray800),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Row(
|
||||
children: [
|
||||
Obx(
|
||||
() {
|
||||
final monthlyRevenue = _statisticController.monthlyRevenue.value;
|
||||
final dateRange = _statisticController.monthlyDateRange.value;
|
||||
final formatted =
|
||||
NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0)
|
||||
.format(monthlyRevenue);
|
||||
return SummaryCard(
|
||||
label: 'Jumlah pendapatan anda selama sebulan dari tanggal $dateRange',
|
||||
value: formatted,
|
||||
icon: CustomeIcons.IncomeFilled(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 32.h),
|
||||
TypographyStyles.h6('Ringkasan Hari ini', color: GrayColors.gray800),
|
||||
SizedBox(height: 16.h),
|
||||
Row(
|
||||
|
|
Loading…
Reference in New Issue