TKK_E32230801/laundry_mobile/lib/staf/laundry.dart

1504 lines
53 KiB
Dart

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<String, dynamic> userData;
final bool isTab;
final Map<String, dynamic>? pickupData;
const LaundryPage(
{super.key, required this.userData, this.isTab = false, this.pickupData});
@override
State<LaundryPage> createState() => _LaundryPageState();
}
class _LaundryPageState extends State<LaundryPage> {
// --- 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<dynamic> services = [];
static List<dynamic>? _cachedServices;
// Keranjang Layanan
List<Map<String, dynamic>> cartItems = [];
int grandTotalPrice = 0;
String selectedDeliveryOption = "Ambil di Toko";
final List<String> 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<DatabaseEvent>? _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<Map<String, dynamic>> 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<String> listJasa = [];
List<String> 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<void> 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),
));
}
}