Files

339 lines
12 KiB
Dart
Raw Permalink Normal View History

2025-08-15 23:53:05 +07:00
import 'package:auto_route/auto_route.dart';
2025-08-15 23:06:47 +07:00
import 'package:flutter/material.dart';
2026-05-19 23:33:12 +07:00
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shimmer/shimmer.dart';
2025-08-15 23:06:47 +07:00
2026-05-19 23:33:12 +07:00
import '../../../application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart';
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
2025-08-20 13:52:49 +07:00
import '../../../common/extension/extension.dart';
2025-08-15 23:53:05 +07:00
import '../../../common/theme/theme.dart';
2026-05-19 23:33:12 +07:00
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';
2026-06-24 10:14:37 +07:00
import 'widgets/purchase_header.dart';
import 'widgets/purchase_rincian_card.dart';
2026-05-19 23:33:12 +07:00
import 'widgets/vendor_card.dart';
2025-08-15 23:06:47 +07:00
2025-08-15 23:53:05 +07:00
@RoutePage()
2026-05-19 23:33:12 +07:00
class PurchasePage extends StatefulWidget implements AutoRouteWrapper {
2025-08-15 23:53:05 +07:00
const PurchasePage({super.key});
2025-08-15 23:06:47 +07:00
@override
State<PurchasePage> createState() => _PurchasePageState();
2026-05-19 23:33:12 +07:00
@override
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
2026-06-24 10:14:37 +07:00
create: (context) =>
getIt<PurchasingAnalyticLoaderBloc>()
..add(const PurchasingAnalyticLoaderEvent.fetched()),
2026-05-19 23:33:12 +07:00
),
BlocProvider(
2026-06-24 10:14:37 +07:00
create: (context) =>
getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
2026-05-19 23:33:12 +07:00
),
],
child: this,
);
2025-08-15 23:06:47 +07:00
}
class _PurchasePageState extends State<PurchasePage>
with TickerProviderStateMixin {
2026-05-19 23:33:12 +07:00
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
2025-08-15 23:06:47 +07:00
@override
void initState() {
super.initState();
2026-05-19 23:33:12 +07:00
_fadeController = AnimationController(
duration: const Duration(milliseconds: 600),
2025-08-15 23:06:47 +07:00
vsync: this,
);
2026-05-19 23:33:12 +07:00
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeOut,
);
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) _fadeController.forward();
});
2025-08-15 23:06:47 +07:00
}
@override
void dispose() {
2026-05-19 23:33:12 +07:00
_fadeController.dispose();
2025-08-15 23:06:47 +07:00
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
2026-05-19 23:33:12 +07:00
body: MultiBlocListener(
listeners: [
2026-06-24 10:14:37 +07:00
BlocListener<
PurchasingAnalyticLoaderBloc,
PurchasingAnalyticLoaderState
>(
2026-05-19 23:33:12 +07:00
listenWhen: (prev, curr) =>
2026-06-24 10:14:37 +07:00
prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo,
2026-05-19 23:33:12 +07:00
listener: (context, _) => context
.read<PurchasingAnalyticLoaderBloc>()
.add(const PurchasingAnalyticLoaderEvent.fetched()),
2025-08-15 23:06:47 +07:00
),
2026-06-24 10:14:37 +07:00
BlocListener<
PurchasingAnalyticLoaderBloc,
PurchasingAnalyticLoaderState
>(
2026-05-19 23:33:12 +07:00
listenWhen: (prev, curr) => prev.outletId != curr.outletId,
listener: (context, _) => context
.read<PurchasingAnalyticLoaderBloc>()
.add(const PurchasingAnalyticLoaderEvent.fetched()),
2025-08-15 23:06:47 +07:00
),
2026-05-19 23:33:12 +07:00
],
child: BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, outletListState) {
2026-06-24 10:14:37 +07:00
return BlocBuilder<
PurchasingAnalyticLoaderBloc,
PurchasingAnalyticLoaderState
>(
2026-05-19 23:33:12 +07:00
builder: (context, state) {
return CustomScrollView(
slivers: [
2026-06-24 10:14:37 +07:00
// Header (same style as Sales)
SliverToBoxAdapter(
child: PurchaseHeader(
state: state,
onDateRangeChanged: (startDate, endDate) {
context.read<PurchasingAnalyticLoaderBloc>().add(
PurchasingAnalyticLoaderEvent.rangeDateChanged(
startDate,
endDate,
),
);
},
),
2026-05-19 23:33:12 +07:00
),
2025-08-15 23:06:47 +07:00
2026-06-24 10:14:37 +07:00
// Outlet Selector
2026-05-19 23:33:12 +07:00
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
2026-06-24 10:14:37 +07:00
child: PurchaseOutletSelectorField(
selectedOutletId: state.outletId,
outlets: outletListState.outlets,
isLoading: outletListState.isFetching,
onOutletChanged: (outletId) {
context.read<PurchasingAnalyticLoaderBloc>().add(
PurchasingAnalyticLoaderEvent.outletChanged(
outletId,
),
);
},
2026-05-19 23:33:12 +07:00
),
2025-08-15 23:06:47 +07:00
),
),
2026-05-19 23:33:12 +07:00
),
2026-06-24 10:14:37 +07:00
// Rincian Pembelian (same style as Sales)
2026-05-19 23:33:12 +07:00
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
2026-06-24 10:14:37 +07:00
padding: const EdgeInsets.all(16),
child: PurchaseRincianCard(state: state),
2025-08-15 23:06:47 +07:00
),
),
2026-05-19 23:33:12 +07:00
),
// 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(
2026-06-24 10:14:37 +07:00
child: _buildEmptyState('Tidak ada data harian'),
);
2026-05-19 23:33:12 +07:00
}
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(
2026-06-24 10:14:37 +07:00
child: _buildEmptyState('Tidak ada data bahan baku'),
);
2026-05-19 23:33:12 +07:00
}
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(
2026-06-24 10:14:37 +07:00
child: _buildEmptyState('Tidak ada data vendor'),
);
2026-05-19 23:33:12 +07:00
}
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),
),
2025-08-15 23:06:47 +07:00
),
);
}
}