selected outlet

This commit is contained in:
efrilm
2026-05-12 17:09:50 +07:00
parent 3541fc725c
commit ba2f0cd265
17 changed files with 1034 additions and 211 deletions
@@ -5,6 +5,7 @@ import 'package:line_icons/line_icons.dart';
import '../../../application/home/home_bloc.dart';
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
import '../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../common/constant/app_constant.dart';
import '../../../common/theme/theme.dart';
import '../../../injection.dart';
@@ -33,6 +34,10 @@ class HomePage extends StatefulWidget implements AutoRouteWrapper {
create: (context) => getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
),
BlocProvider(
create: (context) => getIt<SelectedOutletBloc>()
..add(const SelectedOutletEvent.loaded()),
),
],
child: this,
);
@@ -1,10 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icon.dart';
import 'package:line_icons/line_icons.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../domain/user/user.dart';
import '../../../components/spacer/spacer.dart';
@@ -185,40 +187,46 @@ class _HomeOmsetBalanceState extends State<HomeOmsetBalance> {
GestureDetector _top(BuildContext context) {
return GestureDetector(
onTap: () {},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
gradient: LinearGradient(colors: AppColor.primaryGradient),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Semua Outlet',
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
],
child: BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
builder: (context, state) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
gradient: LinearGradient(colors: AppColor.primaryGradient),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
),
SpaceWidth(6),
LineIcon(
LineIcons.alternateExchange,
color: AppColor.white,
size: 14,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.displayName,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
SpaceWidth(6),
LineIcon(
LineIcons.alternateExchange,
color: AppColor.white,
size: 14,
),
],
),
],
),
);
},
),
);
}
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
import '../../../../application/outlet/selected_outlet/selected_outlet_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/outlet/outlet.dart';
import '../../../components/spacer/spacer.dart';
@@ -13,32 +14,54 @@ class HomePromoBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
builder: (context, state) {
if (state.isFetching && state.outlets.isEmpty) {
builder: (context, outletListState) {
if (outletListState.isFetching && outletListState.outlets.isEmpty) {
return const _PromoBannerSkeleton();
}
if (state.outlets.isEmpty) {
if (outletListState.outlets.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.fromLTRB(
AppValue.padding,
24,
AppValue.padding,
0,
),
child: Row(
children: [
for (int i = 0; i < state.outlets.length; i++) ...[
Expanded(
child: _OutletCard(outlet: state.outlets[i]),
),
if (i < state.outlets.length - 1) const SpaceWidth(12),
],
],
),
return BlocBuilder<SelectedOutletBloc, SelectedOutletState>(
builder: (context, selectedState) {
return Padding(
padding: const EdgeInsets.fromLTRB(
AppValue.padding,
24,
AppValue.padding,
0,
),
child: Row(
children: [
for (int i = 0; i < outletListState.outlets.length; i++) ...[
Expanded(
child: _OutletCard(
outlet: outletListState.outlets[i],
isSelected: selectedState.selectedOutletId ==
outletListState.outlets[i].id,
onTap: () {
final tapped = outletListState.outlets[i];
if (selectedState.selectedOutletId == tapped.id) {
// Tap outlet yang sama → deselect (Semua Outlet)
context
.read<SelectedOutletBloc>()
.add(const SelectedOutletEvent.cleared());
} else {
context
.read<SelectedOutletBloc>()
.add(SelectedOutletEvent.selected(tapped));
}
},
),
),
if (i < outletListState.outlets.length - 1)
const SpaceWidth(12),
],
],
),
);
},
);
},
);
@@ -47,42 +70,73 @@ class HomePromoBanner extends StatelessWidget {
class _OutletCard extends StatelessWidget {
final Outlet outlet;
final bool isSelected;
final VoidCallback onTap;
const _OutletCard({required this.outlet});
const _OutletCard({
required this.outlet,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ParticleCard(
height: 110,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decorationOpacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Spacer(),
_buildHealthIndicator(outlet.isActive),
],
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColor.white : Colors.transparent,
width: 2,
),
const SpaceHeight(6),
Text(
outlet.name,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w800,
height: 1.25,
boxShadow: isSelected
? [
BoxShadow(
color: AppColor.white.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: [],
),
child: Opacity(
opacity: isSelected ? 1.0 : 0.55,
child: ParticleCard(
height: 110,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decorationOpacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Spacer(),
_buildStatusBadge(outlet.isActive),
],
),
const SpaceHeight(6),
Text(
outlet.name,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w800,
height: 1.25,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
Widget _buildHealthIndicator(bool isActive) {
Widget _buildStatusBadge(bool isActive) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(