Feat: done add features revenue monthly

This commit is contained in:
orangdeso 2025-05-16 00:47:18 +07:00
parent 6edf8d531e
commit 986ff5445e
7 changed files with 246 additions and 4 deletions

View File

@ -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",

View File

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

View File

@ -18,4 +18,9 @@ abstract class StatisticRepository {
required String porterId,
required DateTime date,
});
Stream<double> getMonthlyRevenue({
required String porterId,
required DateTime month,
});
}

View File

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

View File

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

View File

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

View File

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