1504 lines
53 KiB
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),
|
|
));
|
|
}
|
|
}
|