Efril 0917c5132b
Some checks are pending
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
feat: update profit loss ui
2026-06-24 10:14:37 +07:00

339 lines
12 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/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart';
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../injection.dart';
import '../../components/spacer/spacer.dart';
import 'widgets/ingredient_card.dart';
import 'widgets/outlet_selector_field.dart';
import 'widgets/purchase_daily_tile.dart';
import 'widgets/purchase_header.dart';
import 'widgets/purchase_rincian_card.dart';
import 'widgets/vendor_card.dart';
@RoutePage()
class PurchasePage extends StatefulWidget implements AutoRouteWrapper {
const PurchasePage({super.key});
@override
State<PurchasePage> createState() => _PurchasePageState();
@override
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<PurchasingAnalyticLoaderBloc>()
..add(const PurchasingAnalyticLoaderEvent.fetched()),
),
BlocProvider(
create: (context) =>
getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
),
],
child: this,
);
}
class _PurchasePageState extends State<PurchasePage>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeOut,
);
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) _fadeController.forward();
});
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: MultiBlocListener(
listeners: [
BlocListener<
PurchasingAnalyticLoaderBloc,
PurchasingAnalyticLoaderState
>(
listenWhen: (prev, curr) =>
prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo,
listener: (context, _) => context
.read<PurchasingAnalyticLoaderBloc>()
.add(const PurchasingAnalyticLoaderEvent.fetched()),
),
BlocListener<
PurchasingAnalyticLoaderBloc,
PurchasingAnalyticLoaderState
>(
listenWhen: (prev, curr) => prev.outletId != curr.outletId,
listener: (context, _) => context
.read<PurchasingAnalyticLoaderBloc>()
.add(const PurchasingAnalyticLoaderEvent.fetched()),
),
],
child: BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, outletListState) {
return BlocBuilder<
PurchasingAnalyticLoaderBloc,
PurchasingAnalyticLoaderState
>(
builder: (context, state) {
return CustomScrollView(
slivers: [
// Header (same style as Sales)
SliverToBoxAdapter(
child: PurchaseHeader(
state: state,
onDateRangeChanged: (startDate, endDate) {
context.read<PurchasingAnalyticLoaderBloc>().add(
PurchasingAnalyticLoaderEvent.rangeDateChanged(
startDate,
endDate,
),
);
},
),
),
// Outlet Selector
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: PurchaseOutletSelectorField(
selectedOutletId: state.outletId,
outlets: outletListState.outlets,
isLoading: outletListState.isFetching,
onOutletChanged: (outletId) {
context.read<PurchasingAnalyticLoaderBloc>().add(
PurchasingAnalyticLoaderEvent.outletChanged(
outletId,
),
);
},
),
),
),
),
// Rincian Pembelian (same style as Sales)
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(16),
child: PurchaseRincianCard(state: state),
),
),
),
// Daily Breakdown Header
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
context.lang.daily_breakdown,
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(12)),
state.isFetching
? _buildListShimmer()
: _buildDailyList(state),
const SliverToBoxAdapter(child: SpaceHeight(24)),
// Ingredient Header
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Bahan Baku',
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(12)),
state.isFetching
? _buildListShimmer()
: _buildIngredientList(state),
const SliverToBoxAdapter(child: SpaceHeight(24)),
// Vendor Header
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Vendor',
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
),
const SliverToBoxAdapter(child: SpaceHeight(12)),
state.isFetching
? _buildListShimmer()
: _buildVendorList(state),
const SliverToBoxAdapter(child: SpaceHeight(80)),
],
);
},
);
},
),
),
);
}
// ─── Lists ────────────────────────────────────────────────────────────────
Widget _buildDailyList(PurchasingAnalyticLoaderState state) {
if (state.purchasing.data.isEmpty) {
return SliverToBoxAdapter(
child: _buildEmptyState('Tidak ada data harian'),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => PurchaseDailyTile(
data: state.purchasing.data[index],
index: index,
animation: _fadeAnimation,
),
childCount: state.purchasing.data.length,
),
);
}
Widget _buildIngredientList(PurchasingAnalyticLoaderState state) {
if (state.purchasing.ingredientData.isEmpty) {
return SliverToBoxAdapter(
child: _buildEmptyState('Tidak ada data bahan baku'),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => PurchaseIngredientCard(
data: state.purchasing.ingredientData[index],
index: index,
animation: _fadeAnimation,
),
childCount: state.purchasing.ingredientData.length,
),
);
}
Widget _buildVendorList(PurchasingAnalyticLoaderState state) {
if (state.purchasing.vendorData.isEmpty) {
return SliverToBoxAdapter(
child: _buildEmptyState('Tidak ada data vendor'),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => PurchaseVendorCard(
data: state.purchasing.vendorData[index],
index: index,
animation: _fadeAnimation,
),
childCount: state.purchasing.vendorData.length,
),
);
}
// ─── Empty State ──────────────────────────────────────────────────────────
Widget _buildEmptyState(String message) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border.withOpacity(0.3)),
),
child: Center(
child: Text(
message,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
);
}
// ─── Shimmer Loaders ──────────────────────────────────────────────────────
Widget _buildListShimmer() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(_, __) => Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: _shimmerCard(height: 72),
),
childCount: 4,
),
);
}
Widget _shimmerCard({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(12),
),
),
);
}
}