feat: Dashboard Analytic
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/summary_response_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'summary_event.dart';
|
||||
@@ -8,15 +8,17 @@ part 'summary_state.dart';
|
||||
part 'summary_bloc.freezed.dart';
|
||||
|
||||
class SummaryBloc extends Bloc<SummaryEvent, SummaryState> {
|
||||
final OrderRemoteDatasource datasource;
|
||||
final AnalyticRemoteDatasource datasource;
|
||||
SummaryBloc(
|
||||
this.datasource,
|
||||
) : super(const _Initial()) {
|
||||
on<_GetSummary>((event, emit) async {
|
||||
emit(const _Loading());
|
||||
final result = await datasource.getSummaryByRangeDate(
|
||||
event.startDate, event.endDate);
|
||||
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!)));
|
||||
final result = await datasource.getDashboard(
|
||||
dateFrom: event.startDate,
|
||||
dateTo: event.endDate,
|
||||
);
|
||||
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,19 @@ mixin _$SummaryEvent {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getSummary,
|
||||
required TResult Function(DateTime startDate, DateTime endDate) getSummary,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getSummary,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getSummary,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -119,7 +119,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getSummary,
|
||||
required TResult Function(DateTime startDate, DateTime endDate) getSummary,
|
||||
}) {
|
||||
return started();
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getSummary,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
|
||||
}) {
|
||||
return started?.call();
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getSummary,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (started != null) {
|
||||
@@ -188,7 +188,7 @@ abstract class _$$GetSummaryImplCopyWith<$Res> {
|
||||
_$GetSummaryImpl value, $Res Function(_$GetSummaryImpl) then) =
|
||||
__$$GetSummaryImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({String startDate, String endDate});
|
||||
$Res call({DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -211,11 +211,11 @@ class __$$GetSummaryImplCopyWithImpl<$Res>
|
||||
null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as DateTime,
|
||||
null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -226,9 +226,9 @@ class _$GetSummaryImpl implements _GetSummary {
|
||||
const _$GetSummaryImpl(this.startDate, this.endDate);
|
||||
|
||||
@override
|
||||
final String startDate;
|
||||
final DateTime startDate;
|
||||
@override
|
||||
final String endDate;
|
||||
final DateTime endDate;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -260,7 +260,7 @@ class _$GetSummaryImpl implements _GetSummary {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getSummary,
|
||||
required TResult Function(DateTime startDate, DateTime endDate) getSummary,
|
||||
}) {
|
||||
return getSummary(startDate, endDate);
|
||||
}
|
||||
@@ -269,7 +269,7 @@ class _$GetSummaryImpl implements _GetSummary {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getSummary,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
|
||||
}) {
|
||||
return getSummary?.call(startDate, endDate);
|
||||
}
|
||||
@@ -278,7 +278,7 @@ class _$GetSummaryImpl implements _GetSummary {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getSummary,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (getSummary != null) {
|
||||
@@ -320,11 +320,11 @@ class _$GetSummaryImpl implements _GetSummary {
|
||||
}
|
||||
|
||||
abstract class _GetSummary implements SummaryEvent {
|
||||
const factory _GetSummary(final String startDate, final String endDate) =
|
||||
const factory _GetSummary(final DateTime startDate, final DateTime endDate) =
|
||||
_$GetSummaryImpl;
|
||||
|
||||
String get startDate;
|
||||
String get endDate;
|
||||
DateTime get startDate;
|
||||
DateTime get endDate;
|
||||
|
||||
/// Create a copy of SummaryEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -339,7 +339,7 @@ mixin _$SummaryState {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(SummaryModel summary) success,
|
||||
required TResult Function(DashboardAnalyticData data) success,
|
||||
required TResult Function(String message) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -347,7 +347,7 @@ mixin _$SummaryState {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(SummaryModel summary)? success,
|
||||
TResult? Function(DashboardAnalyticData data)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -355,7 +355,7 @@ mixin _$SummaryState {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(SummaryModel summary)? success,
|
||||
TResult Function(DashboardAnalyticData data)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
@@ -451,7 +451,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(SummaryModel summary) success,
|
||||
required TResult Function(DashboardAnalyticData data) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return initial();
|
||||
@@ -462,7 +462,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(SummaryModel summary)? success,
|
||||
TResult? Function(DashboardAnalyticData data)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return initial?.call();
|
||||
@@ -473,7 +473,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(SummaryModel summary)? success,
|
||||
TResult Function(DashboardAnalyticData data)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@@ -568,7 +568,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(SummaryModel summary) success,
|
||||
required TResult Function(DashboardAnalyticData data) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return loading();
|
||||
@@ -579,7 +579,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(SummaryModel summary)? success,
|
||||
TResult? Function(DashboardAnalyticData data)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return loading?.call();
|
||||
@@ -590,7 +590,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(SummaryModel summary)? success,
|
||||
TResult Function(DashboardAnalyticData data)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@@ -648,7 +648,7 @@ abstract class _$$SuccessImplCopyWith<$Res> {
|
||||
_$SuccessImpl value, $Res Function(_$SuccessImpl) then) =
|
||||
__$$SuccessImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({SummaryModel summary});
|
||||
$Res call({DashboardAnalyticData data});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -664,13 +664,13 @@ class __$$SuccessImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? summary = null,
|
||||
Object? data = null,
|
||||
}) {
|
||||
return _then(_$SuccessImpl(
|
||||
null == summary
|
||||
? _value.summary
|
||||
: summary // ignore: cast_nullable_to_non_nullable
|
||||
as SummaryModel,
|
||||
null == data
|
||||
? _value.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as DashboardAnalyticData,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -678,14 +678,14 @@ class __$$SuccessImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
|
||||
class _$SuccessImpl implements _Success {
|
||||
const _$SuccessImpl(this.summary);
|
||||
const _$SuccessImpl(this.data);
|
||||
|
||||
@override
|
||||
final SummaryModel summary;
|
||||
final DashboardAnalyticData data;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SummaryState.success(summary: $summary)';
|
||||
return 'SummaryState.success(data: $data)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -693,11 +693,11 @@ class _$SuccessImpl implements _Success {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SuccessImpl &&
|
||||
(identical(other.summary, summary) || other.summary == summary));
|
||||
(identical(other.data, data) || other.data == data));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, summary);
|
||||
int get hashCode => Object.hash(runtimeType, data);
|
||||
|
||||
/// Create a copy of SummaryState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -712,10 +712,10 @@ class _$SuccessImpl implements _Success {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(SummaryModel summary) success,
|
||||
required TResult Function(DashboardAnalyticData data) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return success(summary);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -723,10 +723,10 @@ class _$SuccessImpl implements _Success {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(SummaryModel summary)? success,
|
||||
TResult? Function(DashboardAnalyticData data)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return success?.call(summary);
|
||||
return success?.call(data);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -734,12 +734,12 @@ class _$SuccessImpl implements _Success {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(SummaryModel summary)? success,
|
||||
TResult Function(DashboardAnalyticData data)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(summary);
|
||||
return success(data);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
@@ -783,9 +783,9 @@ class _$SuccessImpl implements _Success {
|
||||
}
|
||||
|
||||
abstract class _Success implements SummaryState {
|
||||
const factory _Success(final SummaryModel summary) = _$SuccessImpl;
|
||||
const factory _Success(final DashboardAnalyticData data) = _$SuccessImpl;
|
||||
|
||||
SummaryModel get summary;
|
||||
DashboardAnalyticData get data;
|
||||
|
||||
/// Create a copy of SummaryState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -864,7 +864,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(SummaryModel summary) success,
|
||||
required TResult Function(DashboardAnalyticData data) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return error(message);
|
||||
@@ -875,7 +875,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(SummaryModel summary)? success,
|
||||
TResult? Function(DashboardAnalyticData data)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return error?.call(message);
|
||||
@@ -886,7 +886,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(SummaryModel summary)? success,
|
||||
TResult Function(DashboardAnalyticData data)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
|
||||
@@ -3,6 +3,6 @@ part of 'summary_bloc.dart';
|
||||
@freezed
|
||||
class SummaryEvent with _$SummaryEvent {
|
||||
const factory SummaryEvent.started() = _Started;
|
||||
const factory SummaryEvent.getSummary(String startDate, String endDate) =
|
||||
const factory SummaryEvent.getSummary(DateTime startDate, DateTime endDate) =
|
||||
_GetSummary;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ part of 'summary_bloc.dart';
|
||||
class SummaryState with _$SummaryState {
|
||||
const factory SummaryState.initial() = _Initial;
|
||||
const factory SummaryState.loading() = _Loading;
|
||||
const factory SummaryState.success(SummaryModel summary) = _Success;
|
||||
const factory SummaryState.success(DashboardAnalyticData data) = _Success;
|
||||
const factory SummaryState.error(String message) = _Error;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/dashboard_analytic_widget.dart';
|
||||
import 'package:enaklo_pos/presentation/sales/pages/sales_page.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:enaklo_pos/core/components/custom_date_picker.dart';
|
||||
@@ -18,7 +19,6 @@ import 'package:enaklo_pos/presentation/report/widgets/product_analytic_widget.d
|
||||
import 'package:enaklo_pos/presentation/report/widgets/report_menu.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/report_title.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/summary_report_widget.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/transaction_report_widget.dart';
|
||||
|
||||
import '../../../core/components/spaces.dart';
|
||||
@@ -166,10 +166,7 @@ class _ReportPageState extends State<ReportPage> {
|
||||
title = 'Ringkasan Laporan Penjualan';
|
||||
setState(() {});
|
||||
context.read<SummaryBloc>().add(
|
||||
SummaryEvent.getSummary(
|
||||
DateFormatter.formatDateTime(
|
||||
fromDate),
|
||||
DateFormatter.formatDateTime(toDate)),
|
||||
SummaryEvent.getSummary(fromDate, toDate),
|
||||
);
|
||||
|
||||
log("Date ${DateFormatter.formatDateTime(fromDate)}");
|
||||
@@ -284,9 +281,9 @@ class _ReportPageState extends State<ReportPage> {
|
||||
error: (message) {
|
||||
return Text(message);
|
||||
},
|
||||
success: (summary) {
|
||||
return SummaryReportWidget(
|
||||
summary: summary,
|
||||
success: (data) {
|
||||
return DashboardAnalyticWidget(
|
||||
data: data,
|
||||
title: title,
|
||||
searchDateFormatted:
|
||||
searchDateFormatted,
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
import 'package:enaklo_pos/core/constants/colors.dart';
|
||||
import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// App Colors
|
||||
class AppColorDashboard {
|
||||
static const secondary = Color(0xff7c3aed);
|
||||
static const success = Color(0xff10b981);
|
||||
static const warning = Color(0xfff59e0b);
|
||||
static const danger = Color(0xffef4444);
|
||||
static const info = Color(0xff3b82f6);
|
||||
}
|
||||
|
||||
class DashboardAnalyticWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final String searchDateFormatted;
|
||||
final DashboardAnalyticData data;
|
||||
|
||||
const DashboardAnalyticWidget({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.title,
|
||||
required this.searchDateFormatted,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 24),
|
||||
_buildKPICards(),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 2, child: _buildSalesChart()),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(flex: 1, child: _buildProductChart()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 2, child: _buildTopProductsList()),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(flex: 1, child: _buildOrderSummary()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Analisis performa penjualan outlet',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, color: Colors.white, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
searchDateFormatted,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPICards() {
|
||||
final successfulOrders = data.overview.totalOrders -
|
||||
data.overview.voidedOrders -
|
||||
data.overview.refundedOrders;
|
||||
|
||||
final kpiData = [
|
||||
{
|
||||
'title': 'Total Penjualan',
|
||||
'value': _formatCurrency(data.overview.totalSales),
|
||||
'icon': Icons.trending_up,
|
||||
'color': AppColorDashboard.success,
|
||||
'bgColor': AppColorDashboard.success.withOpacity(0.1),
|
||||
},
|
||||
{
|
||||
'title': 'Total Pesanan',
|
||||
'value': '${data.overview.totalOrders}',
|
||||
'icon': Icons.shopping_cart,
|
||||
'color': AppColorDashboard.info,
|
||||
'bgColor': AppColorDashboard.info.withOpacity(0.1),
|
||||
},
|
||||
{
|
||||
'title': 'Rata-rata Pesanan',
|
||||
'value': _formatCurrency(data.overview.averageOrderValue.toInt()),
|
||||
'icon': Icons.attach_money,
|
||||
'color': AppColorDashboard.warning,
|
||||
'bgColor': AppColorDashboard.warning.withOpacity(0.1),
|
||||
},
|
||||
{
|
||||
'title': 'Pesanan Sukses',
|
||||
'value': '$successfulOrders',
|
||||
'icon': Icons.check_circle,
|
||||
'color': AppColors.primary,
|
||||
'bgColor': AppColors.primary.withOpacity(0.1),
|
||||
},
|
||||
];
|
||||
|
||||
return Row(
|
||||
children: kpiData.map((kpi) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: kpi['bgColor'] as Color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
kpi['icon'] as IconData,
|
||||
color: kpi['color'] as Color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.trending_up,
|
||||
color: Colors.grey[400],
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
kpi['value'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
kpi['title'] as String,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSalesChart() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Tren Penjualan Harian',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 200000,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: Colors.grey[200]!,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toInt()}K',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 10,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index >= 0 && index < data.recentSales.length) {
|
||||
final date =
|
||||
DateTime.parse(data.recentSales[index].date);
|
||||
final formatter = DateFormat('dd MMM');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
formatter.format(date),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: data.recentSales.asMap().entries.map((entry) {
|
||||
return FlSpot(
|
||||
entry.key.toDouble(), entry.value.sales.toDouble());
|
||||
}).toList(),
|
||||
isCurved: true,
|
||||
color: AppColors.primary,
|
||||
// strokeWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductChart() {
|
||||
final colors = [
|
||||
AppColors.primary,
|
||||
AppColorDashboard.secondary,
|
||||
AppColorDashboard.info,
|
||||
AppColorDashboard.warning,
|
||||
AppColorDashboard.success,
|
||||
];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Distribusi Produk',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
sections: data.topProducts.asMap().entries.map((entry) {
|
||||
return PieChartSectionData(
|
||||
color: colors[entry.key % colors.length],
|
||||
value: entry.value.quantitySold.toDouble(),
|
||||
title: '${entry.value.quantitySold}',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
children: data.topProducts.take(3).map((product) {
|
||||
final index = data.topProducts.indexOf(product);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: colors[index % colors.length],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
product.productName,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${product.quantitySold}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopProductsList() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Produk Terlaris',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
children: data.topProducts.map((product) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.local_cafe,
|
||||
color: AppColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.productName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
product.categoryName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${product.quantitySold} unit',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatCurrency(product.revenue),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderSummary() {
|
||||
final successfulOrders = data.overview.totalOrders -
|
||||
data.overview.voidedOrders -
|
||||
data.overview.refundedOrders;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Ringkasan Pesanan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildSummaryItem('Total Pesanan', '${data.overview.totalOrders}',
|
||||
Icons.shopping_cart, AppColorDashboard.info),
|
||||
_buildSummaryItem('Pesanan Sukses', '$successfulOrders',
|
||||
Icons.check_circle, AppColorDashboard.success),
|
||||
_buildSummaryItem(
|
||||
'Pesanan Dibatalkan',
|
||||
'${data.overview.voidedOrders}',
|
||||
Icons.cancel,
|
||||
AppColorDashboard.danger),
|
||||
_buildSummaryItem('Pesanan Refund', '${data.overview.refundedOrders}',
|
||||
Icons.refresh, AppColorDashboard.warning),
|
||||
const SizedBox(height: 20),
|
||||
// Payment Methods
|
||||
if (data.paymentMethods.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Metode Pembayaran',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...data.paymentMethods
|
||||
.map((method) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
method.paymentMethodType == 'cash'
|
||||
? Icons.payments
|
||||
: Icons.credit_card,
|
||||
color: AppColors.primary,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
method.paymentMethodName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryItem(
|
||||
String title, String value, IconData icon, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 16),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatCurrency(int amount) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id_ID',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
return formatter.format(amount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user