import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../config.dart'; import 'dart:convert'; import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:google_fonts/google_fonts.dart'; import 'laundry/payment.dart'; import 'profile.dart'; class LaundryPage extends StatefulWidget { final Map userData; final bool isTab; final Map? pickupData; const LaundryPage( {super.key, required this.userData, this.isTab = false, this.pickupData}); @override State createState() => _LaundryPageState(); } class _LaundryPageState extends State { // --- Tailwind DESIGN TOKENS --- final Color primary = const Color(0xFF0058BE); final Color onPrimary = const Color(0xFFFFFFFF); final Color primaryContainer = const Color(0xFF2170E4); final Color primaryLight = const Color(0xFFD8E2FF); final Color background = const Color(0xFFF7F9FB); final Color surface = const Color(0xFFF7F9FB); final Color surfaceContainerLowest = const Color(0xFFFFFFFF); final Color surfaceContainer = const Color(0xFFECEEF0); final Color onSurface = const Color(0xFF191C1E); final Color onSurfaceVariant = const Color(0xFF424754); final Color outlineVariant = const Color(0xFFC2C6D6); final Color outline = const Color(0xFF727785); final Color error = const Color(0xFFBA1A1A); final Color secondary = const Color(0xFF416656); final Color tertiary = const Color(0xFF545C72); final Color successGreen = const Color(0xFF10B981); // for status final Color surfaceVariant = const Color(0xFFE0E3E5); // added // State Data String? selectedServiceId; String? selectedEstimationId = "1"; double weight = 0.0; int quantity = 1; int currentItemPrice = 0; bool isLoading = true; List services = []; static List? _cachedServices; // Keranjang Layanan List> cartItems = []; int grandTotalPrice = 0; String selectedDeliveryOption = "Ambil di Toko"; final List deliveryOptions = ["Ambil di Toko", "Antar ke Rumah"]; // Hitung delivery_type otomatis: pickup dari pickupData, delivery dari pilihan String get deliveryType { bool hasPickup = widget.pickupData != null; bool isDelivery = selectedDeliveryOption == "Antar ke Rumah"; if (hasPickup && isDelivery) return 'both'; if (hasPickup) return 'pickup'; if (isDelivery) return 'delivery'; return 'none'; } final DatabaseReference _dbRef = FirebaseDatabase.instance.ref(); StreamSubscription? _weightSubscription; bool isScaleConnected = false; Timer? _refreshTimer; String get _devicePath => 'laundry'; bool get _isEstimationActive { final active = widget.userData['shop']?['is_estimation_active']; if (active == null) return false; return active == 1 || active == true; } bool get _shouldShowEstimation { if (selectedServiceId == null) return false; return _isEstimationActive && !isSatuanService() && (_expressExtraPrice > 0 || _kilatExtraPrice > 0); } int get _expressExtraPrice { final price = widget.userData['shop']?['express_extra_price']; return price != null ? int.tryParse(price.toString()) ?? 0 : 0; } int get _kilatExtraPrice { final price = widget.userData['shop']?['kilat_extra_price']; return price != null ? int.tryParse(price.toString()) ?? 0 : 0; } final List> estimations = [ { "id": "1", "name": "Reguler (3 Hari)", "multiplier": 1.0, "desc": "Normal" }, {"id": "2", "name": "Express (24 Jam)", "multiplier": 1.5, "desc": "Cepat"}, {"id": "3", "name": "Kilat (6 Jam)", "multiplier": 2.0, "desc": "Priority"}, ]; String formatHarga(dynamic value) { if (value == null) return "0"; String val = value.toString(); RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'); return val.replaceAllMapped(reg, (Match m) => '${m[1]}.'); } void _checkAndInitScale() { if (_weightSubscription == null) { _dbRef.child('$_devicePath/current_weight').set(0.0); _dbRef.child('$_devicePath/status_hp').set("-|-|-|-|Rp0"); _listenToScale(); } } @override void initState() { super.initState(); if (_cachedServices != null) { services = _cachedServices!; isLoading = false; } fetchServices(showLoading: _cachedServices == null); _checkAndInitScale(); _refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) { if (mounted) fetchServices(showLoading: false); }); } void _listenToScale() { if (_devicePath.isEmpty) return; _weightSubscription = _dbRef.child('$_devicePath/current_weight').onValue.listen((event) { if (event.snapshot.value != null && mounted) { setState(() { isScaleConnected = true; double newWeight = double.tryParse(event.snapshot.value.toString()) ?? 0.0; if (newWeight > 20.0) newWeight = 20.0; if (newWeight < 0.1) newWeight = 0.0; if (!isSatuanService()) { weight = newWeight; _calculateCurrentPrice(); } }); } }); } void _calculateCurrentPrice() { if (selectedServiceId == null) return; final dynamic service; try { service = services.firstWhere((s) => s['id'].toString() == selectedServiceId); } catch (e) { return; } double basePrice = double.tryParse(service['price'].toString()) ?? 0.0; bool isSatuan = isSatuanService(); double amount = isSatuan ? quantity.toDouble() : double.parse(weight.toStringAsFixed(2)); String estId = _shouldShowEstimation ? (selectedEstimationId ?? "1") : "1"; double extraPrice = 0; if (estId == "2") { extraPrice = _expressExtraPrice.toDouble(); } else if (estId == "3") { extraPrice = _kilatExtraPrice.toDouble(); } setState(() { double rawPrice = (basePrice * amount) + (extraPrice * amount); currentItemPrice = _roundTo500(rawPrice.round()); }); //String namaJasa = service['name'].toString(); //if (namaJasa.length > 8) namaJasa = namaJasa.substring(0, 8); //String textUntukOLED = //"$namaJasa|${est['name'].toString().split(' ')[0]}|${isSatuan ? 'Satuan' : 'Beban'}|${isSatuan ? '$quantity p' : '${weight.toStringAsFixed(3)} kg'}|Rp$currentItemPrice"; //_dbRef.child('laundry/status_hp').set(textUntukOLED); _updateOLEDDisplay(); } void _addItemToCart() { if (selectedServiceId == null) return; if (!isSatuanService() && weight <= 0) { _showSnackBar("Berat belum terdeteksi dari timbangan!", Colors.orange); return; } final service = services.firstWhere((s) => s['id'].toString() == selectedServiceId); String estId = _shouldShowEstimation ? (selectedEstimationId ?? "1") : "1"; final est = estimations.firstWhere((e) => e['id'].toString() == estId, orElse: () => estimations.first); String getCategory(String name) { name = name.toLowerCase(); if (name.contains('express') || name.contains('ekspres')) return 'Express'; if (name.contains('kilat')) return 'Kilat'; return 'Reguler'; } String newItemCategory = getCategory(_shouldShowEstimation ? est['name'].toString() : ""); if (cartItems.isNotEmpty) { String firstItemCategory = getCategory(cartItems.first['estimation_name'].toString()); if (newItemCategory != firstItemCategory) { _showSnackBar( "Gagal: Layanan $newItemCategory tidak bisa digabung dengan layanan $firstItemCategory. Mohon buat nota terpisah.", Colors.red); return; } } setState(() { cartItems.add({ 'service_id': selectedServiceId, 'service_name': service['name'], 'estimation_name': _shouldShowEstimation ? est['name'].toString().split(' ')[0] : "", 'unit': service['unit'], 'weight': isSatuanService() ? quantity.toDouble() : weight, 'price': currentItemPrice, }); _calculateGrandTotal(); _resetInputForm(); }); } void _calculateGrandTotal() { int total = 0; for (var item in cartItems) { total += (item['price'] as int); } setState(() { grandTotalPrice = _roundTo500(total); }); } /// Membulatkan harga ke atas kelipatan 500 int _roundTo500(int value) { if (value <= 0) return 0; return ((value / 500).ceil() * 500).toInt(); } void _removeItem(int index) { setState(() { cartItems.removeAt(index); _calculateGrandTotal(); _updateOLEDDisplay(); // <--- Tambahkan baris ini }); } void _resetInputForm() { setState(() { selectedServiceId = null; weight = 0.0; quantity = 1; currentItemPrice = 0; }); // Panggil fungsi update untuk mencerminkan status keranjang saat ini _updateOLEDDisplay(); } void _updateOLEDDisplay() { List listJasa = []; List listBerat = []; int totalHarga = grandTotalPrice; // 1. Ambil SEMUA riwayat pesanan dari keranjang (Cart) for (var item in cartItems) { listJasa.add(item['service_name'].toString()); if (item['unit'] == 'PCS') { listBerat.add("${(item['weight'] as num).toInt()} pcs"); } else { listBerat.add("${(item['weight'] as num).toStringAsFixed(3)} kg"); } } // 2. Tambah jasa yang SEDANG DIPILIH di form (jika ada) // TAPI: Hanya jika user sedang memilih jasa (selectedServiceId != null) if (selectedServiceId != null) { try { final service = services.firstWhere((s) => s['id'].toString() == selectedServiceId); listJasa.add(service['name'].toString()); totalHarga += currentItemPrice; // Tambahkan harga item yang sedang dipilih if (service['unit'] == 'PCS') { listBerat.add("$quantity pcs"); } else { listBerat .add(weight > 0 ? "${weight.toStringAsFixed(3)} kg" : "0 kg"); } } catch (e) { // ignore: empty_catches } } // 3. Gabungkan semua jadi String String strJasa = listJasa.isNotEmpty ? listJasa.join(", ") : "-"; if (strJasa.length > 50) { strJasa = "${strJasa.substring(0, 47)}..."; // Membatasi string agar memori OLED/ESP32 tidak lambat } String strBerat = listBerat.isNotEmpty ? listBerat.join(", ") : "0"; String strEst = "-"; if (selectedServiceId != null || cartItems.isNotEmpty) { strEst = "Reguler"; // Default ke Reguler jika ada barang } if (selectedServiceId != null && _shouldShowEstimation) { String estId = selectedEstimationId ?? "1"; strEst = estimations.firstWhere((e) => e['id'].toString() == estId, orElse: () => estimations.first)['name'].toString().split(' ')[0]; } else if (cartItems.isNotEmpty) { bool hasKilat = cartItems.any((item) => item['estimation_name']?.toString().toUpperCase() == 'KILAT'); bool hasExpress = cartItems.any((item) => item['estimation_name']?.toString().toUpperCase() == 'EXPRESS'); if (hasKilat) { strEst = "Kilat"; } else if (hasExpress) { strEst = "Express"; } } // Menentukan tipe layanan dinamis (Satuan / Beban) String tipeLayanan = "Beban"; if (selectedServiceId != null) { tipeLayanan = isSatuanService() ? "Satuan" : "Beban"; } else if (cartItems.isNotEmpty) { bool hasKiloan = cartItems.any((item) => item['unit'] != 'PCS'); tipeLayanan = hasKiloan ? "Beban" : "Satuan"; } // 4. Kirim ke Firebase String textUntukOLED = "$strJasa|$strEst|$tipeLayanan|$strBerat|Rp${_roundTo500(totalHarga)}"; if (_devicePath.isNotEmpty) { _dbRef.child('$_devicePath/status_hp').set(textUntukOLED); } } @override void dispose() { _refreshTimer?.cancel(); _weightSubscription?.cancel(); super.dispose(); } bool isSatuanService() { if (selectedServiceId == null) return false; try { final service = services.firstWhere((s) => s['id'].toString() == selectedServiceId); return service['unit']?.toString().toUpperCase() == 'PCS'; } catch (e) { return false; } } Future fetchServices({bool showLoading = true}) async { if (!mounted) return; if (showLoading) setState(() => isLoading = true); try { final response = await http.get(Uri.parse("${AppConfig.baseUrl}/services"), headers: { 'Authorization': 'Bearer ${widget.userData['access_token']}', }); if (response.statusCode == 200) { final decoded = json.decode(response.body); _cachedServices = (decoded is Map && decoded.containsKey('data')) ? decoded['data'] : decoded; setState(() { services = _cachedServices!; isLoading = false; }); } } catch (e) { if (mounted && _cachedServices == null) setState(() => isLoading = false); } } @override Widget build(BuildContext context) { _checkAndInitScale(); bool isSatuan = isSatuanService(); Widget content = isLoading ? Center(child: CircularProgressIndicator(color: primary)) : Column( children: [ if (widget.pickupData == null) _buildAppBar(), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB( 20, 24, 20, 100), // added bottom padding child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.pickupData != null ? "Input Pesanan Pick Up" : "Buat Pesanan", style: GoogleFonts.inter( fontSize: 28, fontWeight: FontWeight.w700, color: onSurface, letterSpacing: -0.02)), const SizedBox(height: 4), Text( widget.pickupData != null ? "Selesaikan data pesanan dari layanan penjemputan." : "Pilih layanan laundry untuk pesanan pelanggan.", style: GoogleFonts.inter( fontSize: 16, color: onSurfaceVariant, fontWeight: FontWeight.w400)), const SizedBox(height: 24), if (widget.pickupData != null) ...[ _buildPickupCustomerCard(), const SizedBox(height: 24), ], _buildSectionTitle("Pilih Layanan"), const SizedBox(height: 12), _buildServiceDropdown(), const SizedBox(height: 24), if (_shouldShowEstimation) ...[ _buildSectionTitle("Estimasi Waktu"), const SizedBox(height: 12), _buildEstimationCards(), const SizedBox(height: 24), ], if (selectedServiceId != null) ...[ _buildSectionTitle("Detail Input"), const SizedBox(height: 12), _buildInputDisplay(isSatuan), const SizedBox(height: 16), _buildAddButton(), const SizedBox(height: 32), ], if (cartItems.isNotEmpty) ...[ _buildSectionTitle("Keranjang"), const SizedBox(height: 12), _buildCartList(), const SizedBox(height: 32), ], _buildSectionTitle("Metode Pengiriman"), const SizedBox(height: 12), _buildDeliveryOptions(), const SizedBox(height: 32), // No longer need _buildPaymentSummaryCard here, we will show it dynamically above or floating if needed. Wait, HTML puts summary inside Cart. if (widget.isTab) const SizedBox(height: 100), ], ), ), ), ], ); return Scaffold( backgroundColor: background, body: Stack( children: [ content, if (cartItems.isNotEmpty) Positioned( bottom: (widget.isTab || widget.pickupData != null) ? 20 : 0, // HTML uses fixed bottom-24 floating button left: 20, right: 20, child: _buildFloatingPayButton(), ), ], ), bottomNavigationBar: isLoading || widget.isTab || widget.pickupData != null ? null : _buildModernBottomBar(), ); } // --- WIDGET COMPONENTS --- Widget _buildPickupCustomerCard() { final order = widget.pickupData!; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: surfaceContainerLowest, borderRadius: BorderRadius.circular(16), border: Border.all(color: outlineVariant.withOpacity(0.5)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4), ) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.person_pin_circle_rounded, color: primary, size: 24), const SizedBox(width: 8), Text("Informasi Pelanggan", style: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.w700, color: onSurface)), ], ), const SizedBox(height: 16), _infoRow(Icons.person_outline_rounded, "Nama", order['customer_name'] ?? '-'), const SizedBox(height: 8), _infoRow(Icons.phone_outlined, "Telepon", order['phone'] ?? '-'), const SizedBox(height: 8), _infoRow( Icons.location_on_outlined, "Alamat", order['address'] ?? '-', maxLines: 2), ], ), ); } Widget _infoRow(IconData icon, String label, String value, {int maxLines = 1}) { return Row( crossAxisAlignment: maxLines > 1 ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ Icon(icon, size: 16, color: onSurfaceVariant), const SizedBox(width: 8), SizedBox( width: 70, child: Text(label, style: GoogleFonts.inter(fontSize: 13, color: onSurfaceVariant)), ), const Text(": "), Expanded( child: Text(value, maxLines: maxLines, overflow: maxLines > 1 ? TextOverflow.ellipsis : null, style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: onSurface)), ), ], ); } Widget _buildAppBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: surface.withOpacity(0.9), border: Border( bottom: BorderSide( color: const Color(0xFFE6E8EA) .withOpacity(0.5), // surfaceContainerHigh width: 1.0, ), ), ), child: SafeArea( bottom: false, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 32, height: 32, decoration: const BoxDecoration( color: Color(0xFFD8E2FF), // primaryFixed shape: BoxShape.circle, ), clipBehavior: Clip.hardEdge, child: Image.asset( 'assets/app_icon.png', fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Icon( Icons.local_laundry_service_rounded, color: primary, size: 20), ), ), const SizedBox(width: 8), Text("My Laundry", style: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w600, color: primary, )), ], ), const SizedBox(width: 48), // Empty space for alignment ], ), ), ); } Widget _buildSectionTitle(String title) { return Text(title, style: GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.w600, color: onSurface)); } Widget _buildServiceDropdown() { final dynamic selectedService = selectedServiceId == null ? null : services.firstWhere((s) => s['id'].toString() == selectedServiceId, orElse: () => null); return GestureDetector( onTap: _showServicePicker, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: surfaceContainerLowest, borderRadius: BorderRadius.circular(12), border: Border.all( color: selectedServiceId != null ? primary : outlineVariant.withOpacity(0.5), width: 1.0), ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: primary.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), child: Icon(Icons.local_laundry_service_rounded, color: primary, size: 24), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( selectedService != null ? selectedService['name'] : "Pilih Layanan Laundry", style: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.w600, color: selectedService != null ? onSurface : onSurfaceVariant), ), if (selectedService != null) Text( "Rp ${formatHarga(selectedService['price'])} / ${selectedService['unit']}", style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, color: primary), ) else Text( "Ketuk untuk melihat daftar layanan", style: GoogleFonts.inter( fontSize: 12, color: onSurfaceVariant.withOpacity(0.8), fontWeight: FontWeight.w400), ), ], ), ), Icon(Icons.keyboard_arrow_down_rounded, color: onSurfaceVariant, size: 24), ], ), ), ); } void _showServicePicker() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => Container( height: MediaQuery.of(context).size.height * 0.7, decoration: BoxDecoration( color: background, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: outlineVariant, borderRadius: BorderRadius.circular(2)), ), Padding( padding: const EdgeInsets.all(24), child: Row( children: [ Text("Pilih Layanan", style: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.w700, color: onSurface)), const Spacer(), IconButton( onPressed: () => Navigator.pop(context), icon: Icon(Icons.close_rounded, color: onSurfaceVariant), style: IconButton.styleFrom( backgroundColor: surfaceContainerLowest), ) ], ), ), Expanded( child: ListView.separated( padding: const EdgeInsets.fromLTRB(24, 0, 24, 40), itemCount: services.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { final s = services[index]; bool isSelected = selectedServiceId == s['id'].toString(); Color iconBg = primary.withOpacity(0.1); Color iconColor = primary; IconData iconData = Icons.local_laundry_service_rounded; if (s['unit'].toString().toUpperCase() == 'PCS') { iconBg = tertiary.withOpacity(0.1); iconColor = tertiary; iconData = Icons.dry_cleaning; } else if (s['name'] .toString() .toLowerCase() .contains('setrika')) { iconBg = primary.withOpacity(0.1); iconColor = primary; iconData = Icons.iron; } else { iconBg = secondary.withOpacity(0.1); iconColor = secondary; iconData = Icons.local_laundry_service; } return GestureDetector( onTap: () { setState(() { selectedServiceId = s['id'].toString(); _calculateCurrentPrice(); }); Navigator.pop(context); }, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: surfaceContainerLowest, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? primary : outlineVariant.withOpacity(0.5), width: isSelected ? 1.5 : 1.0), boxShadow: isSelected ? [ BoxShadow( color: primary.withOpacity(0.1), blurRadius: 4) ] : null, ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: isSelected ? iconColor : iconBg, borderRadius: BorderRadius.circular(8), ), child: Icon( iconData, color: isSelected ? onPrimary : iconColor, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( s['name'], style: GoogleFonts.inter( fontWeight: FontWeight.w600, fontSize: 14, color: onSurface), ), Text( s['unit'].toString().toUpperCase() == 'PCS' ? 'Perawatan premium' : 'Bersih & harum', style: GoogleFonts.inter( fontSize: 11, color: onSurfaceVariant), ), ], ), ), Text( "Rp ${formatHarga(s['price'])}/${s['unit'].toString().toLowerCase() == 'kg' ? 'kg' : 'pc'}", style: GoogleFonts.inter( fontWeight: FontWeight.w600, fontSize: 14, color: primary), ), ], ), ), ); }, ), ), ], ), ), ); } Widget _buildEstimationCards() { // Filter estimasi berdasarkan konfigurasi harga tambahan di pengaturan final availableEstimations = estimations.where((est) { if (est['id'].toString() == "1") return true; // Reguler selalu tampil if (est['id'].toString() == "2") return _expressExtraPrice > 0; if (est['id'].toString() == "3") return _kilatExtraPrice > 0; return true; }).toList(); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: availableEstimations.map((est) { bool isSelected = selectedEstimationId == est['id'].toString(); // Memberi warna aksen berbeda untuk tiap estimasi Color estColor; if (est['id'].toString() == "1") { estColor = successGreen; } else if (est['id'].toString() == "2") { estColor = error; } else { estColor = error; } return GestureDetector( onTap: () => setState(() { selectedEstimationId = est['id'].toString(); _calculateCurrentPrice(); }), child: Container( width: 140, margin: const EdgeInsets.only(right: 12, bottom: 4), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isSelected ? estColor.withOpacity(0.08) : surfaceContainerLowest, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? estColor : outlineVariant.withOpacity(0.3), width: 1.0, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon( isSelected ? Icons.bolt_rounded : Icons.schedule_rounded, color: isSelected ? estColor : onSurfaceVariant), Container( width: 22, height: 22, decoration: BoxDecoration( shape: BoxShape.circle, color: isSelected ? estColor : Colors.transparent, border: Border.all( color: isSelected ? estColor : outlineVariant, width: 1.5), ), child: isSelected ? const Icon(Icons.check, size: 14, color: Colors.white) : null, ) ], ), const SizedBox(height: 12), Text(est['name'].toString().split(' ')[0], style: GoogleFonts.inter( fontWeight: FontWeight.w700, fontSize: 15, color: isSelected ? estColor : onSurface)), Text( est['name'] .toString() .replaceAll(est['name'].toString().split(' ')[0], '') .trim(), style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w500, color: isSelected ? estColor : onSurfaceVariant)), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isSelected ? estColor : outlineVariant.withOpacity(0.5), borderRadius: BorderRadius.circular(8), ), child: Text(est['desc'], style: GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.w700, color: isSelected ? Colors.white : onSurfaceVariant, letterSpacing: 0.5)), ) ], ), ), ); }).toList(), ), ); } Widget _buildInputDisplay(bool isSatuan) { // Variasi warna untuk membedakan mode satuan dan mode kiloan Color inputAccentColor = isSatuan ? error : primary; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: surfaceContainerLowest, borderRadius: BorderRadius.circular(12), border: Border.all(color: outlineVariant.withOpacity(0.3), width: 1.0), ), child: Column( children: [ isSatuan ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: inputAccentColor.withOpacity(0.1), border: Border.all( color: inputAccentColor.withOpacity(0.3), width: 1.0), borderRadius: BorderRadius.circular(12)), child: Icon(Icons.checkroom_rounded, color: inputAccentColor), ), const SizedBox(width: 16), Text("Jumlah Pcs", style: GoogleFonts.inter( fontWeight: FontWeight.w700, fontSize: 15, color: onSurface)), ], ), Container( decoration: BoxDecoration( color: background, border: Border.all(color: outlineVariant, width: 1.0), borderRadius: BorderRadius.circular(30)), child: Row( children: [ IconButton( icon: const Icon(Icons.remove, size: 20), color: inputAccentColor, onPressed: () { if (quantity > 1) { setState(() { quantity--; _calculateCurrentPrice(); }); } }, ), SizedBox( width: 24, child: Text("$quantity", textAlign: TextAlign.center, style: GoogleFonts.inter( fontWeight: FontWeight.w600, fontSize: 16))), IconButton( icon: const Icon(Icons.add, size: 20), color: Colors.white, style: IconButton.styleFrom( backgroundColor: inputAccentColor), onPressed: () { setState(() { quantity++; _calculateCurrentPrice(); }); }, ), ], ), ) ], ) : Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: inputAccentColor.withOpacity(0.1), border: Border.all( color: inputAccentColor.withOpacity(0.3), width: 1.0), borderRadius: BorderRadius.circular(12)), child: Icon(Icons.scale_rounded, color: inputAccentColor), ), const SizedBox(width: 16), Expanded( child: Text("Berat Pakaian", style: GoogleFonts.inter( fontWeight: FontWeight.w700, fontSize: 15, color: onSurface)), ), Text("${weight.toStringAsFixed(3)} kg", style: GoogleFonts.inter( fontSize: 24, fontWeight: FontWeight.w600, color: inputAccentColor, letterSpacing: -0.5)), ], ), const SizedBox(height: 16), Divider(color: outlineVariant, height: 1.0), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Subtotal", style: GoogleFonts.inter( color: onSurfaceVariant, fontWeight: FontWeight.w500, fontSize: 14)), Text("Rp ${formatHarga(currentItemPrice)}", style: GoogleFonts.inter( color: primary, fontWeight: FontWeight.w700, fontSize: 18)), ], ) ], ), ); } Widget _buildAddButton() { return SizedBox( width: double.infinity, height: 48, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: primary.withOpacity(0.1), foregroundColor: primary, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: _addItemToCart, icon: const Icon(Icons.add_circle_outline, size: 20), label: Text("Tambahkan Item Ini", style: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 14)), ), ); } Widget _buildCartList() { int subtotal = cartItems.fold(0, (sum, item) => sum + (item['price'] as int)); int totalEstimasi = subtotal; // Delivery fee is set later return Container( decoration: BoxDecoration( color: surfaceContainerLowest, borderRadius: BorderRadius.circular(12), border: Border.all(color: outlineVariant.withOpacity(0.5), width: 1.0), boxShadow: [ BoxShadow( color: onSurface.withOpacity(0.02), blurRadius: 4, offset: const Offset(0, 2)) ], ), child: Column( children: [ ListView.separated( padding: const EdgeInsets.all(12), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: cartItems.length, separatorBuilder: (_, __) => Divider(color: outlineVariant.withOpacity(0.3), height: 24), itemBuilder: (context, index) { final item = cartItems[index]; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item['service_name'], style: GoogleFonts.inter( fontWeight: FontWeight.w600, color: onSurface, fontSize: 14)), Text( item['estimation_name'].toString().isNotEmpty ? "${item['estimation_name']} • ${item['unit'] == 'PCS' ? item['weight'].toInt() : item['weight'].toStringAsFixed(3)} ${item['unit']}" : "${item['unit'] == 'PCS' ? item['weight'].toInt() : item['weight'].toStringAsFixed(3)} ${item['unit']}", style: GoogleFonts.inter( color: onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.w400)), ], ), ), Text("Rp ${formatHarga(item['price'])}", style: GoogleFonts.inter( fontWeight: FontWeight.w600, color: onSurface, fontSize: 14)), const SizedBox(width: 12), InkWell( onTap: () => _removeItem(index), borderRadius: BorderRadius.circular(20), child: Container( width: 28, height: 28, decoration: BoxDecoration( border: Border.all(color: outlineVariant), shape: BoxShape.circle), child: Icon(Icons.remove, color: onSurfaceVariant, size: 16), ), ) ], ); }, ), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: surfaceContainer.withOpacity(0.5), border: Border( top: BorderSide(color: outlineVariant.withOpacity(0.3))), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Total Estimasi", style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: onSurface)), Text("Rp ${formatHarga(_roundTo500(totalEstimasi))}", style: GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.w600, color: primary)), ], ), ], ), ) ], ), ); } Widget _buildDeliveryOptions() { return Row( children: deliveryOptions.map((option) { bool isSelected = selectedDeliveryOption == option; return Expanded( child: GestureDetector( onTap: () => setState(() => selectedDeliveryOption = option), child: Container( margin: EdgeInsets.only( right: option == deliveryOptions.first ? 6 : 0, left: option == deliveryOptions.last ? 6 : 0), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: BoxDecoration( color: isSelected ? primary.withOpacity(0.05) : surfaceContainerLowest, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? primary : outlineVariant.withOpacity(0.3), width: 1.0, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( option == "Ambil di Toko" ? Icons.store : Icons.moped_rounded, color: isSelected ? primary : onSurfaceVariant, size: 24), const SizedBox(height: 4), Text( option, textAlign: TextAlign.center, style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w500, color: isSelected ? primary : onSurface, ), ), ], ), ), ), ); }).toList(), ); } Widget _buildModernBottomBar() { return Container( decoration: BoxDecoration( color: surfaceContainerLowest, border: Border(top: BorderSide(color: surfaceVariant.withOpacity(0.5))), ), child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _navItem(Icons.home_outlined, "Beranda", false, () { if (_devicePath.isNotEmpty) { _dbRef.child('$_devicePath/screen_status').set("OFF"); } Navigator.pop(context); }), _navItem(Icons.add_circle, "Input", true, () {}), _navItem(Icons.person_outline, "Profil", false, () { if (_devicePath.isNotEmpty) { _dbRef.child('$_devicePath/screen_status').set("OFF"); } Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => ProfilePage(userData: widget.userData))); }), ], ), ), ), ); } Widget _navItem( IconData icon, String label, bool isActive, VoidCallback onTap) { if (isActive) { return InkWell( onTap: onTap, splashColor: Colors.transparent, highlightColor: Colors.transparent, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( color: primary.withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: Icon(icon, color: primary, size: 24), ), const SizedBox(height: 4), Text(label, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, color: primary)), ], ), ); } return InkWell( onTap: onTap, splashColor: Colors.transparent, highlightColor: Colors.transparent, child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 24, color: onSurfaceVariant), const SizedBox(height: 4), Text(label, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w500, color: onSurfaceVariant)), ], ), ); } Widget _buildFloatingPayButton() { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: primary.withOpacity(0.2), blurRadius: 10, offset: const Offset(0, 4)) ], ), child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: primary, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0, ), onPressed: () { double totalWeight = cartItems.fold( 0, (sum, item) => sum + (item['weight'] as double)); String estType = "Reguler"; if (cartItems .every((e) => e['estimation_name'].toString().trim().isEmpty)) { estType = "None"; } else if (cartItems.any((e) => e['estimation_name'] .toString() .toLowerCase() .contains('kilat'))) { estType = "Kilat"; } else if (cartItems.any((e) => e['estimation_name'] .toString() .toLowerCase() .contains('ekspres') || e['estimation_name'] .toString() .toLowerCase() .contains('express'))) { estType = "Ekspres"; } int totalBill = _roundTo500( cartItems.fold(0, (sum, item) => sum + (item['price'] as int))); // Bersihkan item yang menggantung (tidak jadi di-add) agar OLED update hanya sesuai isi keranjang valid _resetInputForm(); Navigator.push( context, MaterialPageRoute( builder: (_) => PaymentPage( totalBill: totalBill, cartItems: cartItems, userData: widget.userData, weight: totalWeight, deliveryType: deliveryType, estimationType: estType, pickupData: widget.pickupData, ), )).then((result) { if (result == true) { setState(() { cartItems = []; grandTotalPrice = 0; _resetInputForm(); }); } }); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("Proses Pesanan", style: GoogleFonts.inter( fontWeight: FontWeight.w600, fontSize: 16, color: onPrimary)), ], ), ), ); } void _showSnackBar(String msg, Color bg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Container( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Icon( bg == successGreen ? Icons.check_circle_outline : Icons.error_outline_rounded, color: Colors.white, size: 28, ), const SizedBox(width: 12), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( bg == successGreen ? "Berhasil" : "Peringatan", style: GoogleFonts.inter( fontWeight: FontWeight.w800, fontSize: 14, color: Colors.white), ), Text( msg, style: GoogleFonts.inter( fontSize: 12, color: Colors.white.withOpacity(0.9), fontWeight: FontWeight.w500), ), ], ), ), ], ), ), behavior: SnackBarBehavior.floating, backgroundColor: bg.withOpacity(0.95), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.2), width: 1), ), duration: const Duration(seconds: 3), )); } }