265 lines
8.0 KiB
Dart
265 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../../common/theme/theme.dart';
|
|
import '../../../../domain/outlet/outlet.dart';
|
|
|
|
/// Outlet selector field — styled like DateRangePickerField.
|
|
/// Opens a bottom sheet to pick an outlet or "Semua Outlet".
|
|
class PurchaseOutletSelectorField extends StatefulWidget {
|
|
final String? selectedOutletId;
|
|
final List<Outlet> outlets;
|
|
final bool isLoading;
|
|
final ValueChanged<String?> onOutletChanged;
|
|
|
|
const PurchaseOutletSelectorField({
|
|
super.key,
|
|
required this.selectedOutletId,
|
|
required this.outlets,
|
|
required this.isLoading,
|
|
required this.onOutletChanged,
|
|
});
|
|
|
|
@override
|
|
State<PurchaseOutletSelectorField> createState() =>
|
|
_PurchaseOutletSelectorFieldState();
|
|
}
|
|
|
|
class _PurchaseOutletSelectorFieldState
|
|
extends State<PurchaseOutletSelectorField> {
|
|
bool _isPressed = false;
|
|
|
|
Outlet? get _selectedOutlet => widget.outlets
|
|
.where((o) => o.id == widget.selectedOutletId)
|
|
.firstOrNull;
|
|
|
|
String get _label => _selectedOutlet?.name ?? 'Semua Outlet';
|
|
|
|
bool get _hasValue => widget.selectedOutletId != null;
|
|
|
|
void _showSheet() {
|
|
if (widget.isLoading) return;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (_) => _OutletBottomSheet(
|
|
outlets: widget.outlets,
|
|
selectedOutletId: widget.selectedOutletId,
|
|
onSelected: (outletId) {
|
|
Navigator.pop(context);
|
|
widget.onOutletChanged(outletId);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: _showSheet,
|
|
onTapDown: (_) => setState(() => _isPressed = true),
|
|
onTapUp: (_) => setState(() => _isPressed = false),
|
|
onTapCancel: () => setState(() => _isPressed = false),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
height: 52,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: _isPressed ? AppColor.backgroundLight : AppColor.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: _isPressed ? AppColor.primary : AppColor.border,
|
|
width: _isPressed ? 2 : 1,
|
|
),
|
|
boxShadow: _isPressed
|
|
? [
|
|
BoxShadow(
|
|
color: AppColor.primary.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_label,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight:
|
|
_hasValue ? FontWeight.w500 : FontWeight.w400,
|
|
color: _hasValue
|
|
? AppColor.textPrimary
|
|
: AppColor.textSecondary,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: widget.isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: AppColor.primary,
|
|
),
|
|
)
|
|
: const Icon(
|
|
Icons.store_rounded,
|
|
size: 20,
|
|
color: AppColor.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Bottom Sheet ─────────────────────────────────────────────────────────────
|
|
|
|
class _OutletBottomSheet extends StatelessWidget {
|
|
final List<Outlet> outlets;
|
|
final String? selectedOutletId;
|
|
final ValueChanged<String?> onSelected;
|
|
|
|
const _OutletBottomSheet({
|
|
required this.outlets,
|
|
required this.selectedOutletId,
|
|
required this.onSelected,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Handle bar
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Pilih Outlet',
|
|
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// "Semua Outlet" option
|
|
_OutletItem(
|
|
label: 'Semua Outlet',
|
|
icon: Icons.store_rounded,
|
|
isSelected: selectedOutletId == null,
|
|
onTap: () => onSelected(null),
|
|
),
|
|
const Divider(height: 1),
|
|
|
|
// Individual outlets
|
|
...outlets.map(
|
|
(outlet) => Column(
|
|
children: [
|
|
_OutletItem(
|
|
label: outlet.name,
|
|
icon: Icons.storefront_rounded,
|
|
isSelected: selectedOutletId == outlet.id,
|
|
isActive: outlet.isActive,
|
|
onTap: () => onSelected(outlet.id),
|
|
),
|
|
if (outlet != outlets.last) const Divider(height: 1),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── List Item ────────────────────────────────────────────────────────────────
|
|
|
|
class _OutletItem extends StatelessWidget {
|
|
final String label;
|
|
final IconData icon;
|
|
final bool isSelected;
|
|
final bool? isActive;
|
|
final VoidCallback onTap;
|
|
|
|
const _OutletItem({
|
|
required this.label,
|
|
required this.icon,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
this.isActive,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
onTap: onTap,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
|
leading: Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColor.primary.withOpacity(0.1)
|
|
: AppColor.background,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
size: 18,
|
|
color: isSelected ? AppColor.primary : AppColor.textSecondary,
|
|
),
|
|
),
|
|
title: Text(
|
|
label,
|
|
style: AppStyle.md.copyWith(
|
|
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
|
color: isSelected ? AppColor.primary : AppColor.textPrimary,
|
|
),
|
|
),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (isActive != null) ...[
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isActive! ? AppColor.success : AppColor.error,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
if (isSelected)
|
|
const Icon(Icons.check_rounded, color: AppColor.primary, size: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|