Efril 843c11b200
Some checks are pending
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
feat: update stock ui
2026-06-24 10:28:13 +07:00

413 lines
13 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/painter/wave_painter.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/bottom_sheet/date_range_bottom_sheet.dart';
import '../../../components/spacer/spacer.dart';
class InventoryHeader extends StatelessWidget {
final InventoryAnalyticLoaderState state;
final int selectedTabIndex;
final ValueChanged<int> onTabChanged;
final void Function(DateTime startDate, DateTime endDate)? onDateRangeChanged;
const InventoryHeader({
super.key,
required this.state,
required this.selectedTabIndex,
required this.onTabChanged,
this.onDateRangeChanged,
});
@override
Widget build(BuildContext context) {
final outletLabel = state.inventoryAnalytic.summary.outletName.isNotEmpty
? state.inventoryAnalytic.summary.outletName
: 'Semua Outlet';
final dateLabel = _formatDateRange(state.dateFrom, state.dateTo);
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: Stack(
children: [
// Decorative circles
Positioned(
top: -20,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.08),
),
),
),
Positioned(
top: 30,
right: 20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.05),
),
),
),
Positioned(
top: 10,
left: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.04),
),
),
),
// Wave pattern
Positioned.fill(
child: CustomPaint(
painter: WavePainter(
animation: 0.0,
color: AppColor.textWhite.withOpacity(0.1),
),
),
),
// Content
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Title row + Calendar button
Row(
children: [
GestureDetector(
onTap: () => context.router.maybePop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.chevron_left_rounded,
color: AppColor.textWhite,
size: 24,
),
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.inventory,
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
'$dateLabel · $outletLabel',
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SpaceWidth(8),
// Date filter button
GestureDetector(
onTap: () => _showDatePicker(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.calendar_month_rounded,
color: AppColor.textWhite,
size: 20,
),
),
),
],
),
const SpaceHeight(20),
// Tab selector (Product / Ingredient)
_buildTabSelector(context),
const SpaceHeight(24),
// Total Value label
Text(
'Total Nilai Inventori',
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 13,
),
),
const SpaceHeight(4),
// Big total value
state.isFetching
? _buildHeaderValueShimmer()
: Text(
state
.inventoryAnalytic
.summary
.totalValue
.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w900,
fontSize: 32,
),
),
const SpaceHeight(16),
// Chips row
state.isFetching
? _buildHeaderChipsShimmer()
: _buildHeaderChips(context),
],
),
),
),
],
),
);
}
Widget _buildTabSelector(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: AppColor.textWhite.withOpacity(0.2)),
),
child: Row(
children: [
Expanded(
child: _buildTab(
icon: Icons.inventory_2_rounded,
label: context.lang.product,
isSelected: selectedTabIndex == 0,
onTap: () => onTabChanged(0),
),
),
Expanded(
child: _buildTab(
icon: Icons.restaurant_menu_rounded,
label: context.lang.ingredients,
isSelected: selectedTabIndex == 1,
onTap: () => onTabChanged(1),
),
),
],
),
);
}
Widget _buildTab({
required IconData icon,
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? AppColor.white : Colors.transparent,
borderRadius: BorderRadius.circular(26),
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? AppColor.textPrimary : AppColor.textWhite,
),
const SizedBox(width: 6),
Text(
label,
style: AppStyle.md.copyWith(
color: isSelected ? AppColor.textPrimary : AppColor.textWhite,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
);
}
void _showDatePicker(BuildContext context) {
DateRangePickerBottomSheet.show(
context: context,
primaryColor: AppColor.primary,
initialStartDate: state.dateFrom,
initialEndDate: state.dateTo,
maxDate: DateTime.now(),
onChanged: (startDate, endDate) {
if (startDate != null && endDate != null) {
onDateRangeChanged?.call(startDate, endDate);
}
},
);
}
Widget _buildHeaderValueShimmer() {
return Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.3),
highlightColor: AppColor.textWhite.withOpacity(0.6),
child: Container(
width: 200,
height: 36,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
),
);
}
Widget _buildHeaderChipsShimmer() {
return Row(
children: List.generate(
3,
(index) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.15),
highlightColor: AppColor.textWhite.withOpacity(0.3),
child: Container(
width: 90,
height: 32,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
),
),
),
),
);
}
Widget _buildHeaderChips(BuildContext context) {
final summary = state.inventoryAnalytic.summary;
if (selectedTabIndex == 0) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip('${summary.totalProducts} ${context.lang.product}'),
_buildChip('${summary.lowStockProducts} ${context.lang.low_stock}'),
_buildChip('${summary.zeroStockProducts} ${context.lang.zero_stock}'),
],
);
} else {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip('${summary.totalIngredients} ${context.lang.ingredients}'),
_buildChip(
'${summary.lowStockIngredients} ${context.lang.low_stock}',
),
_buildChip(
'${summary.zeroStockIngredients} ${context.lang.zero_stock}',
),
],
);
}
}
Widget _buildChip(String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.textWhite.withOpacity(0.25)),
),
child: Text(
label,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
String _formatDateRange(DateTime from, DateTime to) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
if (from.year == to.year && from.month == to.month && from.day == to.day) {
return '${from.day} ${months[from.month - 1]} ${from.year}';
}
return '${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}';
}
}