439 lines
17 KiB
Dart
439 lines
17 KiB
Dart
import 'dart:math';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart';
|
|
import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/core/system/system_setting.dart';
|
|
import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart';
|
|
import 'package:niogu_ecommerce_v1/core/widgets/triangle_painter.dart';
|
|
import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart';
|
|
import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart';
|
|
import 'package:sizer/sizer.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_color.dart';
|
|
|
|
class OutletMapScreen extends ConsumerStatefulWidget {
|
|
final LatLng userLocation;
|
|
final List<OtherOutlet> outlets;
|
|
const OutletMapScreen({
|
|
super.key,
|
|
required this.userLocation,
|
|
required this.outlets,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<OutletMapScreen> createState() => _OutletMapScreenState();
|
|
}
|
|
|
|
class _OutletMapScreenState extends ConsumerState<OutletMapScreen> {
|
|
final MapController _mapController = MapController();
|
|
|
|
late final LatLng _userLocation;
|
|
|
|
late final List<OtherOutlet> _outlets;
|
|
|
|
int _selectedOutletIndex = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_userLocation = widget.userLocation;
|
|
_outlets = widget.outlets;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_fitBounds();
|
|
});
|
|
}
|
|
|
|
double _calculateDistance(
|
|
double lat1,
|
|
double lon1,
|
|
double lat2,
|
|
double lon2,
|
|
) {
|
|
final p = 0.017453292519943295;
|
|
final c = cos;
|
|
final a =
|
|
0.5 -
|
|
c((lat2 - lat1) * p) / 2 +
|
|
c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2;
|
|
return 12742 * asin(sqrt(a));
|
|
}
|
|
|
|
void _fitBounds() {
|
|
_mapController.fitCamera(
|
|
CameraFit.bounds(
|
|
bounds: LatLngBounds(
|
|
_userLocation,
|
|
_outlets[_selectedOutletIndex].coordinate!,
|
|
),
|
|
padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 10.w),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return SafeArea(
|
|
top: false,
|
|
bottom: true,
|
|
right: false,
|
|
left: false,
|
|
child: Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
FlutterMap(
|
|
mapController: _mapController,
|
|
options: MapOptions(
|
|
initialCenter: _outlets[0].coordinate!,
|
|
initialZoom: 16.0,
|
|
minZoom: 3.0,
|
|
maxZoom: 18.0,
|
|
),
|
|
children: [
|
|
TileLayer(
|
|
urlTemplate:
|
|
'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
|
subdomains: const ['a', 'b', 'c', 'd'],
|
|
),
|
|
|
|
MarkerLayer(
|
|
markers: [
|
|
..._outlets.asMap().entries.map((entry) {
|
|
int idx = entry.key;
|
|
var data = entry.value;
|
|
final isSelected = _selectedOutletIndex == idx;
|
|
|
|
final distance = _calculateDistance(
|
|
_userLocation.latitude,
|
|
_userLocation.longitude,
|
|
data.coordinate!.latitude,
|
|
data.coordinate!.longitude,
|
|
);
|
|
|
|
var strDistance =
|
|
'${distance.toStringAsFixed(1)} Km dari lokasimu';
|
|
|
|
if (distance < 1) {
|
|
strDistance =
|
|
'${(distance * 1000).toStringAsFixed(0)} meter dari lokasimu';
|
|
}
|
|
|
|
return Marker(
|
|
point: data.coordinate!,
|
|
width: 70.w,
|
|
height: 25.h,
|
|
alignment: Alignment.topCenter,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 300),
|
|
opacity: isSelected ? 1.0 : 0.0,
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 3.w,
|
|
vertical: 0.8.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
border: Border.all(
|
|
color: isSelected
|
|
? AppColor.primaryColor.withOpacity(
|
|
0.5,
|
|
)
|
|
: Colors.grey.shade200,
|
|
),
|
|
),
|
|
child: Text(
|
|
strDistance,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1).sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (isSelected)
|
|
CustomPaint(
|
|
size: Size(3.w, 1.5.w),
|
|
painter: TrianglePainter(
|
|
Colors.white,
|
|
), // Reuse painter
|
|
),
|
|
Icon(
|
|
Icons.location_on,
|
|
color: isSelected
|
|
? AppColor.primaryColor
|
|
: Colors.grey.shade400,
|
|
size: 10.w,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
Positioned(
|
|
top: 6.h,
|
|
left: 4.w,
|
|
child: CircleAvatar(
|
|
backgroundColor: Colors.white,
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.arrow_back,
|
|
color: Colors.black,
|
|
size: 7.w,
|
|
),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
bottom: 12.h,
|
|
left: 0,
|
|
right: 0,
|
|
child: SizedBox(
|
|
height: 20.h,
|
|
child: PageView.builder(
|
|
controller: PageController(viewportFraction: 0.85),
|
|
itemCount: _outlets.length,
|
|
onPageChanged: (index) {
|
|
setState(() => _selectedOutletIndex = index);
|
|
_fitBounds();
|
|
},
|
|
itemBuilder: (context, index) {
|
|
final outlet = _outlets[index];
|
|
return CachedNetworkImage(
|
|
imageUrl: outlet.image ?? 'error',
|
|
imageBuilder: (context, imageProvider) {
|
|
return Container(
|
|
width: 75.w,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: imageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.black87,
|
|
],
|
|
),
|
|
),
|
|
padding: EdgeInsets.all(4.w),
|
|
child: _buildBranchInformation(
|
|
name: outlet.name,
|
|
location: outlet.location,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorWidget: (context, url, error) {
|
|
return Container(
|
|
width: 75.w,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
decoration: BoxDecoration(
|
|
border: BoxBorder.all(
|
|
color: _selectedOutletIndex == index
|
|
? AppColor.primaryColor
|
|
: Colors.transparent,
|
|
),
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: AssetImage(AppAsset.OUTLET_MOCK),
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.black87,
|
|
],
|
|
),
|
|
),
|
|
padding: EdgeInsets.all(4.w),
|
|
child: _buildBranchInformation(
|
|
name: outlet.name,
|
|
location: outlet.location,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Container(
|
|
padding: EdgeInsets.all(4.w),
|
|
color: Colors.white,
|
|
child: SafeArea(
|
|
top: false,
|
|
child: ElevatedButton(
|
|
onPressed: () async {
|
|
final outlet = _outlets[_selectedOutletIndex];
|
|
|
|
await SystemSetting.switchOutlet(
|
|
outletId: outlet.id,
|
|
outletName: outlet.name,
|
|
outletPhone: outlet.phoneNumber,
|
|
outletLocation: outlet.location,
|
|
outletCoordinate: outlet.coordinate,
|
|
);
|
|
|
|
ref.read(currentOutletIdProvider.notifier).state =
|
|
outlet.id;
|
|
|
|
ref.read(currentOutletNameProvider.notifier).state =
|
|
outlet.name;
|
|
|
|
ref.read(currentOutletPhoneProvider.notifier).state =
|
|
outlet.phoneNumber;
|
|
|
|
ref
|
|
.read(currentOutletLocationProvider.notifier)
|
|
.state =
|
|
outlet.location;
|
|
|
|
ref
|
|
.read(currentOutletCoordinateProvider.notifier)
|
|
.state = outlet
|
|
.coordinate;
|
|
|
|
await ref
|
|
.read(homeControllerProvider.notifier)
|
|
.refresh();
|
|
|
|
CustomSnackbar.showSuccess(
|
|
context,
|
|
"Berhasil mengunjungi ${outlet.name}",
|
|
);
|
|
|
|
context.pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColor.primaryColor,
|
|
minimumSize: Size(double.infinity, 6.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
child: Text(
|
|
"Pilih Outlet Ini",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.medium.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildBranchInformation({required String name, String? location}) {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
if (location != null)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.location_on, size: 4.w, color: Colors.white70),
|
|
|
|
SizedBox(width: 0.75.w),
|
|
Expanded(
|
|
child: Text(
|
|
location,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: (AppFontSize.small - 2).sp,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
/**
|
|
SizedBox(width: 2.5.w),
|
|
|
|
Row(
|
|
children: [
|
|
Icon(Icons.straighten, size: 4.w, color: Colors.white70),
|
|
|
|
SizedBox(width: 0.75.w),
|
|
|
|
Text(
|
|
"4km",
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: (AppFontSize.small - 2).sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
*/
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|