Feat: done add features revenue monthly
This commit is contained in:
parent
6edf8d531e
commit
986ff5445e
|
@ -680,7 +680,7 @@
|
||||||
"languageVersion": "3.4"
|
"languageVersion": "3.4"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generated": "2025-05-14T18:46:38.233635Z",
|
"generated": "2025-05-15T05:07:55.383919Z",
|
||||||
"generator": "pub",
|
"generator": "pub",
|
||||||
"generatorVersion": "3.5.0",
|
"generatorVersion": "3.5.0",
|
||||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.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.fromDate(DateTime(d.year, d.month, d.day));
|
||||||
Timestamp _tsToDate(DateTime d) =>
|
Timestamp _tsToDate(DateTime d) =>
|
||||||
Timestamp.fromDate(DateTime(d.year, d.month, d.day).add(const Duration(days: 1)));
|
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({
|
Query _baseQuery({
|
||||||
required String porterId,
|
required String porterId,
|
||||||
|
@ -86,4 +94,55 @@ class StatisticRepositoryImpl implements StatisticRepository {
|
||||||
return total;
|
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 String porterId,
|
||||||
required DateTime date,
|
required DateTime date,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Stream<double> getMonthlyRevenue({
|
||||||
|
required String porterId,
|
||||||
|
required DateTime month,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,9 @@ class StatisticUseCase {
|
||||||
required String porterId,
|
required String porterId,
|
||||||
required DateTime date,
|
required DateTime date,
|
||||||
}) => _repo.getRevenue(porterId: porterId, date: 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:e_porter/domain/usecases/statistic_usecase.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
class StatisticController extends GetxController {
|
class StatisticController extends GetxController {
|
||||||
final StatisticUseCase useCase;
|
final StatisticUseCase useCase;
|
||||||
final String porterId;
|
final String porterId;
|
||||||
|
|
||||||
final date = DateTime.now().obs;
|
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 inProgress = 0.obs;
|
||||||
final completed = 0.obs;
|
final completed = 0.obs;
|
||||||
final revenue = 0.0.obs;
|
final revenue = 0.0.obs;
|
||||||
|
final monthlyRevenue = 0.0.obs;
|
||||||
|
|
||||||
|
Timer? _monthlyUpdateTimer;
|
||||||
|
|
||||||
StatisticController({
|
StatisticController({
|
||||||
required this.useCase,
|
required this.useCase,
|
||||||
|
@ -22,6 +30,55 @@ class StatisticController extends GetxController {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_bindAllStreams();
|
_bindAllStreams();
|
||||||
ever(date, (_) => _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() {
|
void _bindAllStreams() {
|
||||||
|
@ -37,7 +94,40 @@ class StatisticController extends GetxController {
|
||||||
revenue.bindStream(
|
revenue.bindStream(
|
||||||
useCase.getRevenue(porterId: porterId, date: date.value),
|
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 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/porter_queue_controller.dart';
|
||||||
import 'package:e_porter/presentation/controllers/statistic_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/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/profile_avatar.dart';
|
||||||
import 'package:e_porter/presentation/screens/home/component/summary_card.dart';
|
import 'package:e_porter/presentation/screens/home/component/summary_card.dart';
|
||||||
import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
|
import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
|
||||||
|
@ -392,6 +393,49 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
TypographyStyles.h6('Ringkasan Hari ini', color: GrayColors.gray800),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
Row(
|
Row(
|
||||||
|
|
Loading…
Reference in New Issue