263 lines
8.7 KiB
Dart
263 lines
8.7 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
|
|
import '../../../application/analytic/profit_sharing_loader/profit_sharing_loader_bloc.dart';
|
|
import '../../../common/extension/extension.dart';
|
|
import '../../../common/theme/theme.dart';
|
|
import '../../../domain/analytic/analytic.dart';
|
|
import '../../../injection.dart';
|
|
import '../../components/field/date_range_picker_field.dart';
|
|
import '../../components/spacer/spacer.dart';
|
|
import '../../components/toast/flushbar.dart';
|
|
import '../../router/app_router.gr.dart';
|
|
import 'widgets/profit_sharing_allocation.dart';
|
|
import 'widgets/profit_sharing_categories.dart';
|
|
import 'widgets/profit_sharing_header.dart';
|
|
import 'widgets/profit_sharing_periods.dart';
|
|
import 'widgets/profit_sharing_sdl.dart';
|
|
|
|
@RoutePage()
|
|
class ProfitSharingPage extends StatefulWidget implements AutoRouteWrapper {
|
|
const ProfitSharingPage({super.key});
|
|
|
|
@override
|
|
State<ProfitSharingPage> createState() => _ProfitSharingPageState();
|
|
|
|
@override
|
|
Widget wrappedRoute(BuildContext context) => BlocProvider(
|
|
create: (_) =>
|
|
getIt<ProfitSharingLoaderBloc>()
|
|
..add(ProfitSharingLoaderEvent.fetched()),
|
|
child: this,
|
|
);
|
|
}
|
|
|
|
class _ProfitSharingPageState extends State<ProfitSharingPage>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _fadeController;
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 1000),
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn));
|
|
|
|
_fadeController.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fadeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppColor.background,
|
|
body: BlocListener<ProfitSharingLoaderBloc, ProfitSharingLoaderState>(
|
|
listenWhen: (previous, current) =>
|
|
previous.dateFrom != current.dateFrom ||
|
|
previous.dateTo != current.dateTo,
|
|
listener: (context, state) {
|
|
context.read<ProfitSharingLoaderBloc>().add(
|
|
const ProfitSharingLoaderEvent.fetched(),
|
|
);
|
|
},
|
|
child: BlocBuilder<ProfitSharingLoaderBloc, ProfitSharingLoaderState>(
|
|
builder: (context, state) {
|
|
return RefreshIndicator(
|
|
backgroundColor: AppColor.white,
|
|
color: AppColor.primary,
|
|
onRefresh: () async {
|
|
context.read<ProfitSharingLoaderBloc>().add(
|
|
const ProfitSharingLoaderEvent.fetched(),
|
|
);
|
|
await context.read<ProfitSharingLoaderBloc>().stream.firstWhere(
|
|
(s) => !s.isFetching,
|
|
);
|
|
},
|
|
child: CustomScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: ProfitSharingHeader(state: state),
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: DateRangePickerField(
|
|
startDate: state.dateFrom,
|
|
endDate: state.dateTo,
|
|
onChanged: (startDate, endDate) {
|
|
if (startDate == null || endDate == null) return;
|
|
context.read<ProfitSharingLoaderBloc>().add(
|
|
ProfitSharingLoaderEvent.rangeDateChanged(
|
|
startDate,
|
|
endDate,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (state.isFetching)
|
|
SliverToBoxAdapter(child: _buildShimmer())
|
|
else ...[
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: ProfitSharingCategories(
|
|
categories: state.profitSharing.categories,
|
|
onCategoryTap: (category) =>
|
|
_openDetail(context, state, category),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SpaceHeight(16)),
|
|
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: _buildSdlSection(context, state),
|
|
),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SpaceHeight(16)),
|
|
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: ProfitSharingAllocation(
|
|
budget: state.profitSharing.budget,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SpaceHeight(16)),
|
|
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: ProfitSharingPeriods(
|
|
state: state,
|
|
onPeriodTypeChanged: (type) {
|
|
context.read<ProfitSharingLoaderBloc>().add(
|
|
ProfitSharingLoaderEvent.periodTypeChanged(type),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
|
|
const SliverToBoxAdapter(child: SizedBox(height: 100)),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Detail kembali dengan `true` kalau kategori induknya sudah non-team;
|
|
/// list dimuat ulang supaya kategori itu hilang.
|
|
Future<void> _openDetail(
|
|
BuildContext context,
|
|
ProfitSharingLoaderState state,
|
|
ProfitSharingCategory category,
|
|
) async {
|
|
final isNotTeam = await context.router.push<bool>(
|
|
ProfitSharingDetailRoute(
|
|
parentCategoryId: category.parentCategoryId,
|
|
parentCategoryName: category.parentCategoryName,
|
|
dateFrom: state.dateFrom,
|
|
dateTo: state.dateTo,
|
|
),
|
|
);
|
|
if (isNotTeam != true || !context.mounted) return;
|
|
|
|
AppFlushbar.showError(context, context.lang.profit_sharing_not_team);
|
|
context.read<ProfitSharingLoaderBloc>().add(
|
|
const ProfitSharingLoaderEvent.fetched(),
|
|
);
|
|
}
|
|
|
|
Widget _buildSdlSection(
|
|
BuildContext context,
|
|
ProfitSharingLoaderState state,
|
|
) {
|
|
final total = state.profitSharing.budget.total;
|
|
final categories = [...state.profitSharing.categories]
|
|
..sort((a, b) => b.sdl.compareTo(a.sdl));
|
|
|
|
return ProfitSharingSdlSection(
|
|
sdl: total.sdl,
|
|
revenue: total.revenue,
|
|
// Fee owner beda tiap kategori induk, jadi yang ditampilkan persen
|
|
// efektifnya; persentase bawaan hanya dipakai saat belum ada omzet.
|
|
percent: total.hasRevenue
|
|
? total.sdlPercentage
|
|
: state.profitSharing.budget.percentages.owner,
|
|
breakdownTitle: context.lang.sdl_per_parent_category,
|
|
items: [
|
|
for (final category in categories)
|
|
ProfitSharingSdlItem(
|
|
label: category.parentCategoryName,
|
|
sdl: category.sdl,
|
|
revenue: category.totalRevenue,
|
|
feePercent: category.ownerFeePercent,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildShimmer() {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(
|
|
children: [
|
|
_shimmerBox(height: 240),
|
|
const SpaceHeight(16),
|
|
_shimmerBox(height: 300),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _shimmerBox({required double height}) {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey[300]!,
|
|
highlightColor: Colors.grey[100]!,
|
|
child: Container(
|
|
height: height,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|