import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:latlong2/latlong.dart'; import 'package:niogu_app/core/components/top_back_bar_app.dart'; import 'package:niogu_app/core/constants/app_color.dart'; import 'package:niogu_app/core/constants/app_font_size.dart'; import 'package:niogu_app/core/providers/app_provider.dart'; import 'package:niogu_app/core/router/app_route.dart'; import 'package:niogu_app/core/utils/image_service.dart'; import 'package:niogu_app/core/utils/log_message.dart'; import 'package:niogu_app/core/widgets/custom_snackbar.dart'; import 'package:niogu_app/core/widgets/custom_text_form_field.dart'; import 'package:niogu_app/features/outlets/domain/entities/outlet.dart'; import 'package:niogu_app/features/outlets/presentation/providers/outlet_provider.dart'; import 'package:niogu_app/features/outlets/presentation/screens/delete_outlet_screen.dart'; import 'package:niogu_app/features/outlets/presentation/widgets/edit_outlet_shimmer.dart'; import 'package:sizer/sizer.dart'; class EditOutletScreen extends ConsumerStatefulWidget { final String outletId; const EditOutletScreen({super.key, required this.outletId}); @override ConsumerState createState() => _EditOutletScreenState(); } class _EditOutletScreenState extends ConsumerState { final GlobalKey _generalKey = GlobalKey(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _phoneNumberController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final ImagePicker _picker = ImagePicker(); final _emailRegex = RegExp( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', ); final List _imagePathTemps = []; String? _imagePath; bool _initializeIsMainOutlet = false; bool _isMainOutlet = false; bool _isActive = false; int _transactionAmount = 0; bool _isLoading = true; @override void initState() { // TODO: implement initState super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { Future.delayed(const Duration(milliseconds: 800), () { ref.invalidate(mapOutletAddressProvider); _fetchOutlet(); }); }); } @override void dispose() { // TODO: implement dispose _nameController.dispose(); _phoneNumberController.dispose(); _emailController.dispose(); super.dispose(); } Future _fetchOutlet() async { try { final outlet = await ref .read(outletRepositoryProvider) .getOutletById(widget.outletId); if (!mounted) return; _imagePath = outlet.bannerPath; _nameController.text = outlet.name; _phoneNumberController.text = outlet.phoneNumber; if (outlet.email != null) { _emailController.text = outlet.email!; } if (outlet.fullAddress != null && outlet.latitude != null && outlet.longitude != null) { ref .read(mapOutletAddressProvider.notifier) .state = OutletAddressSelected( fullAddress: outlet.fullAddress!, currentLocation: LatLng(outlet.latitude!, outlet.longitude!), ); } _initializeIsMainOutlet = outlet.isMainOutlet; _isMainOutlet = outlet.isMainOutlet; _isActive = outlet.isActive; _transactionAmount = outlet.transactionAmount; setState(() { _isLoading = false; }); } catch (e, st) { if (!mounted) return; setState(() { _isLoading = false; }); LogMessage.log.e(e.toString(), error: e, stackTrace: st); CustomSnackbar.showError(context, "Ups, terjadi kesalahan"); context.pop(); } } Future _showPickerOptions() async { final bool isTablet = 100.w >= 600; showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)), ), constraints: const BoxConstraints(maxWidth: double.infinity), builder: (BuildContext context) { return SafeArea( child: Container( width: 100.w, padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w), clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Icon(Icons.photo_library, size: 6.w), title: Text( 'Galeri', style: TextStyle( fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, ), ), onTap: () async { await _getImage(ImageSource.gallery); context.pop(); }, ), SizedBox(height: 2.h), ListTile( leading: Icon(Icons.photo_camera, size: 6.w), title: Text( 'Kamera', style: TextStyle( fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, ), ), onTap: () async { await _getImage(ImageSource.camera); context.pop(); }, ), ], ), ), ); }, ); } Future _getImage(ImageSource ImageSource) async { try { final XFile? pickedFile = await _picker.pickImage( source: ImageSource, imageQuality: 70, ); if (pickedFile != null) { File tempFile = File(pickedFile.path); String? imagePath = await ImageService.saveImageToLocalDirectory( tempFile, "outlet_banner_images", ); if (imagePath != null) { _imagePathTemps.add(imagePath); setState(() { _imagePath = imagePath; }); } } } catch (e, st) { LogMessage.log.e(e.toString(), error: e, stackTrace: st); CustomSnackbar.showWarning(context, "Akses ditolak"); } } Future _cleanUpImages() async { for (final image in _imagePathTemps) { if (image != null) { await ImageService.deleteLocalImage(image); } } } String? _validation(String? value, String message, {bool isEmail = false}) { if (value == null || value.trim().isEmpty) { return message; } if (isEmail && !_emailRegex.hasMatch(value)) { return "Email tidak valid"; } return null; } Future _saveOutlet() async { if (!_generalKey.currentState!.validate()) return; final phoneNumber = _phoneNumberController.text.trim(); final email = _emailController.text.trim().isNotEmpty ? _emailController.text.trim() : null; final validatePhoneAndEmail = await ref .read(outletRepositoryProvider) .validatePhoneAndEmail( phoneNumber: phoneNumber, email: email, excludeId: widget.outletId, ); if (validatePhoneAndEmail) { CustomSnackbar.showError( context, 'No. hp atau email outlet telah tersedia', ); return; } final selectedOutletAddress = ref.read(mapOutletAddressProvider); final upsertOutlet = UpsertOutlet( id: widget.outletId, bannerPath: _imagePath, name: _nameController.text.trim(), phoneNumber: _phoneNumberController.text.trim(), email: _emailController.text.trim().isNotEmpty ? _emailController.text.trim() : null, fullAddress: selectedOutletAddress?.fullAddress, latitude: selectedOutletAddress?.currentLocation.latitude, longitude: selectedOutletAddress?.currentLocation.longitude, isMainOutlet: _isMainOutlet, isActive: _isMainOutlet ? true : _isActive, ); if (_imagePath == null && _imagePathTemps.isNotEmpty) { await _cleanUpImages(); } else if (_imagePath != null && _imagePathTemps.isNotEmpty && _imagePathTemps.length > 1) { final imagePathTemps = _imagePathTemps .where((i) => i != _imagePath) .toList(); for (final image in imagePathTemps) { if (image != null) { await ImageService.deleteLocalImage(image); } } } try { await ref .read(outletControllerProvider.notifier) .saveOutlet(upsertOutlet); if (!mounted) return; ref.invalidate(mapOutletAddressProvider); final currentOutletId = ref.read(currentOutletIdProvider); if (currentOutletId == upsertOutlet.id) { ref.read(currentOutletNameProvider.notifier).state = upsertOutlet.name; } CustomSnackbar.showSuccess(context, 'Outlet berhasil diubah'); context.pop(); if (currentOutletId == upsertOutlet.id && !upsertOutlet.isActive) { context.pop(); } } catch (e, st) { LogMessage.log.e(e.toString(), error: e, stackTrace: st); CustomSnackbar.showError(context, 'Ups, terjadi kesalahan'); } } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final bool isTablet = 100.w >= 600; final outletControllerState = ref.watch(outletControllerProvider); final mapOutletAddressState = ref.watch(mapOutletAddressProvider); final adminState = ref.watch(staffAdminProvider(widget.outletId)); final List admins = []; adminState.whenData((a) => admins.addAll(a)); final File imageFile = File(_imagePath ?? "image not found"); final bool imageFileExists = imageFile.existsSync(); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; await _cleanUpImages(); context.pop(); WidgetsBinding.instance.addPostFrameCallback((_) { ref.invalidate(mapOutletAddressProvider); }); }, child: SafeArea( top: false, bottom: true, right: false, left: false, child: Scaffold( backgroundColor: Colors.white, appBar: TopBackBarApp( title: "Edit Outlet", onTap: () async { await _cleanUpImages(); context.pop(); WidgetsBinding.instance.addPostFrameCallback((_) { ref.invalidate(mapOutletAddressProvider); }); }, ), body: _isLoading ? Padding( padding: EdgeInsets.all(5.w), child: const EditOutletShimmer(), ) : SingleChildScrollView( padding: EdgeInsets.all(5.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only(bottom: 1.5.h), child: Text( "Foto Outlet", style: TextStyle( fontSize: (AppFontSize.medium - 1.25).sp, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ), Stack( clipBehavior: Clip.none, children: [ Container( height: 20.h, width: double.infinity, decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(3.w), border: Border.all( color: Colors.grey.shade300, style: BorderStyle.none, ), ), child: imageFileExists ? ClipRRect( borderRadius: BorderRadius.circular( 2.5.w, ), child: Image.file( imageFile, fit: BoxFit.cover, width: double.infinity, height: double.infinity, errorBuilder: (context, error, stackTrace) { return Icon( Icons.error, color: Colors.grey[400], size: 10.w, ); }, ), ) : GestureDetector( onTap: _showPickerOptions, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.add_a_photo_outlined, size: 10.w, color: Colors.grey.shade700, ), SizedBox(height: 1.h), Text( "Unggah Foto Landscape", style: TextStyle( fontSize: isTablet ? (AppFontSize.medium - 2) .sp : (AppFontSize.small - 2) .sp, color: Colors.grey.shade700, ), ), ], ), ), ), if (_imagePath != null) Positioned( top: 1.w, right: 1.w, child: Material( color: Colors.transparent, type: MaterialType.transparency, child: InkWell( onTap: () { setState(() { _imagePath = null; }); }, child: Container( padding: EdgeInsets.all(1.w), decoration: BoxDecoration( color: Colors.red[50], shape: BoxShape.circle, border: Border.all( color: Colors.red.withOpacity(0.2), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity( 0.1, ), blurRadius: 2, offset: Offset(0, 1), ), ], ), child: Icon( Icons.remove, color: Colors.red, size: 4.w, ), ), ), ), ), ], ), SizedBox(height: 3.h), Form( key: _generalKey, child: Column( children: [ CustomTextFormField( label: "Nama Outlet", hint: "Contoh: Outlet Perum Gunung Batu", prefixIcon: Icons.store_mall_directory_outlined, controller: _nameController, validator: (value) => _validation( value, "Nama outlet belum diisi", ), ), SizedBox(height: 3.h), CustomTextFormField( label: "No. Handphone / WA Outlet", hint: "081234567890", prefixIcon: Icons.phone_android_outlined, controller: _phoneNumberController, keyboardType: TextInputType.phone, validator: (value) => _validation( value, "No. handphone outlet belum diisi", ), ), SizedBox(height: 3.h), CustomTextFormField( label: "Email Outlet", hint: "(Opsional)", prefixIcon: Icons.email_outlined, keyboardType: TextInputType.emailAddress, controller: _emailController, validator: (value) { if (value == null || value.isEmpty) { return null; } if (!_emailRegex.hasMatch(value)) { return "Email outlet tidak valid"; } return null; }, ), ], ), ), SizedBox(height: 3.h), Material( color: Colors.transparent, type: MaterialType.transparency, child: InkWell( onTap: () async { final adminState = ref.read( staffAdminProvider(widget.outletId), ); final List admins = []; adminState.whenData((a) => admins.addAll(a)); await Future.delayed( const Duration(milliseconds: 400), ); context.pushNamed( AppRoute.editStaffAdminScreen, extra: { "outlet_id": widget.outletId, "admins": admins, }, ); }, child: Container( width: double.infinity, padding: EdgeInsets.symmetric( horizontal: 4.w, vertical: 2.h, ), decoration: BoxDecoration( border: Border.all( color: Colors.grey.shade300, ), borderRadius: BorderRadius.circular(2.05.w), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.person_outline, color: Colors.black, size: 5.w, ), SizedBox(width: 2.5.w), Expanded( child: Text( admins.isEmpty ? "Staf admin belum biatur" : "${admins.length} Staf admin", style: TextStyle( fontWeight: FontWeight.bold, fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, height: 1.4, ), ), ), ], ), ), ), ), SizedBox(height: 3.h), Material( color: Colors.transparent, type: MaterialType.transparency, child: InkWell( onTap: () { context.pushNamed( AppRoute.mapOutletAdressScreen, ); }, child: Container( width: double.infinity, padding: EdgeInsets.symmetric( horizontal: 4.w, vertical: 2.h, ), decoration: BoxDecoration( border: Border.all( color: Colors.grey.shade300, ), borderRadius: BorderRadius.circular(2.05.w), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.map_outlined, color: Colors.black, size: 5.w, ), SizedBox(width: 2.5.w), Expanded( child: Text( mapOutletAddressState != null ? mapOutletAddressState.fullAddress : "Atur alamat outlet", style: TextStyle( fontWeight: mapOutletAddressState != null ? FontWeight.normal : FontWeight.bold, fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, height: 1.4, ), ), ), ], ), ), ), ), if (!_initializeIsMainOutlet) ...[ SizedBox(height: 3.h), Container( width: double.infinity, padding: EdgeInsets.symmetric( horizontal: 4.w, vertical: 2.h, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(2.05.w), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( "Jadikan Outlet Utama", style: TextStyle( fontWeight: FontWeight.bold, fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, height: 1.4, ), ), ), SizedBox( width: 10.w, child: FittedBox( fit: BoxFit.fill, child: Switch( value: _isMainOutlet, onChanged: (_) { setState(() { _isMainOutlet = !_isMainOutlet; if (!_isActive) { _isActive = !_isActive; } }); }, activeColor: _isMainOutlet ? AppColor.primaryColor : Colors.grey, ), ), ), ], ), ), ], SizedBox(height: 3.h), Container( padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: Colors.red.withOpacity(0.05), borderRadius: BorderRadius.circular(3.w), border: Border.all( color: Colors.red.withOpacity(0.1), ), ), child: Column( children: [ ListTile( contentPadding: EdgeInsets.zero, leading: Icon( Icons.power_settings_new, size: 5.w, color: _isActive ? Colors.green : Colors.grey, ), title: Text( "Status Operasional Outlet", style: TextStyle( fontWeight: FontWeight.bold, fontSize: AppFontSize.medium.sp, ), ), subtitle: Text( _isActive ? "Aktif / Buka" : "Non Aktif / Tutup", style: TextStyle( fontWeight: FontWeight.bold, fontSize: isTablet ? AppFontSize.medium.sp : AppFontSize.small.sp, ), ), trailing: _initializeIsMainOutlet || _isMainOutlet ? null : SizedBox( width: 10.w, child: FittedBox( fit: BoxFit.fill, child: Switch( value: _isActive, onChanged: (_) { setState(() { _isActive = !_isActive; }); }, activeColor: _isActive ? Colors.green : Colors.grey, ), ), ), ), if (_initializeIsMainOutlet) ...[ Divider(color: Colors.red.withOpacity(0.1)), Text( "Outlet Utama", style: TextStyle( color: Colors.grey.shade700, fontWeight: FontWeight.bold, fontSize: (AppFontSize.medium - 1.25).sp, ), ), ] else if (!_isMainOutlet) ...[ Divider(color: Colors.red.withOpacity(0.1)), TextButton.icon( onPressed: () { showDialog( context: context, builder: (context) => DeleteOutletScreen( id: widget.outletId, name: _nameController.text, transactionAmount: _transactionAmount, ), ); }, icon: Icon( Icons.delete_forever, size: 5.w, color: Colors.red, ), label: Text( "Hapus Outlet Permanen", style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: (AppFontSize.medium - 1.25).sp, ), ), ), ], ], ), ), SizedBox(height: 3.h), ElevatedButton( onPressed: outletControllerState.isLoading ? null : _saveOutlet, style: ElevatedButton.styleFrom( backgroundColor: AppColor.primaryColor, minimumSize: Size(double.infinity, 7.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(3.w), ), disabledBackgroundColor: Colors.grey.shade300, ), child: Text( "Simpan", style: TextStyle( fontSize: AppFontSize.medium.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), SizedBox(height: 3.h), ], ), ), ), ), ); }, ); } }