TKK_E32230801/laundry_mobile/lib/staf/beranda.dart

1034 lines
41 KiB
Dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../config.dart';
import 'dart:async';
import 'package:google_fonts/google_fonts.dart';
import 'package:firebase_database/firebase_database.dart';
import 'home/order_list.dart';
import 'home/payments.dart';
import 'laundry.dart';
import 'profile.dart';
import 'request_pickup.dart';
class BerandaPage extends StatefulWidget {
final Map<String, dynamic> userData;
const BerandaPage({super.key, required this.userData});
@override
State<BerandaPage> createState() => _BerandaPageState();
}
class _BerandaPageState extends State<BerandaPage> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F9FB), // bg-surface
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _selectedIndex == 0
? HomeTab(key: const ValueKey("home"), userData: widget.userData)
: _selectedIndex == 1
? LaundryPage(
key: const ValueKey("laundry"),
userData: widget.userData,
isTab: true)
: ProfilePage(
key: const ValueKey("profile"),
userData: widget.userData,
isTab: true),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
),
bottomNavigationBar: _buildPersistentBottomNav(),
);
}
Widget _buildPersistentBottomNav() {
return Container(
height: 64 + MediaQuery.of(context).padding.bottom,
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF).withOpacity(0.95),
border:
const Border(top: BorderSide(color: Color(0xFFE6E8EA), width: 1.0)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, -1))
],
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_navItem(Icons.home, "Beranda", _selectedIndex == 0, () {
setState(() => _selectedIndex = 0);
}),
_navItem(Icons.add_circle, "Input", _selectedIndex == 1,
() async {
setState(() => _selectedIndex = 1);
try {
await FirebaseDatabase.instance
.ref('laundry/screen_status')
.set("RESTART");
// Otomatis kembalikan ke ON dari sisi HP agar tidak nyangkut
await Future.delayed(const Duration(milliseconds: 500));
await FirebaseDatabase.instance
.ref('laundry/screen_status')
.set("ON");
// ignore: empty_catches
} catch (e) {}
}),
_navItem(Icons.person, "Profil", _selectedIndex == 2, () {
setState(() => _selectedIndex = 2);
}),
],
),
),
),
);
}
Widget _navItem(
IconData icon, String label, bool isActive, VoidCallback onTap) {
return InkWell(
onTap: onTap,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
child: Container(
width: 64,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 24,
color: isActive
? const Color(0xFF0058BE)
: const Color(0xFF424754).withOpacity(0.6),
),
const SizedBox(height: 2),
Text(
label,
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
color: isActive
? const Color(0xFF0058BE)
: const Color(0xFF424754).withOpacity(0.6),
),
),
],
),
),
);
}
}
class HomeTab extends StatefulWidget {
final Map<String, dynamic> userData;
const HomeTab({super.key, required this.userData});
@override
State<HomeTab> createState() => _HomeTabState();
}
class _HomeTabState extends State<HomeTab> {
// Tailwind Colors
final Color primary = const Color(0xFF0058BE);
final Color surface = const Color(0xFFF7F9FB);
final Color surfaceContainerHigh = const Color(0xFFE6E8EA);
final Color onSurface = const Color(0xFF191C1E);
final Color onSurfaceVariant = const Color(0xFF424754);
final Color primaryFixed = const Color(0xFFD8E2FF);
final Color surfaceContainerLowest = const Color(0xFFFFFFFF);
final Color surfaceContainerLow = const Color(0xFFF2F4F6);
final Color secondaryContainer = const Color(0xFFC3ECD7);
final Color onSecondaryContainer = const Color(0xFF294E3F);
final Color outline = const Color(0xFF727785);
int countAntrian = 0;
int countProses = 0;
int countSelesai = 0;
List<dynamic> aktivitasTerbaru = [];
List<dynamic> pesananTiba = [];
bool isLoading = true;
Timer? _refreshTimer;
String _selectedTab = 'antrian';
static Map<String, dynamic>? _cachedStats;
static Map<String, dynamic>? _cachedActs;
static Map<String, dynamic>? _cachedTiba;
@override
void initState() {
super.initState();
if (_cachedStats != null && _cachedActs != null) {
_loadFromCache();
}
fetchAllData(showLoading: _cachedStats == null);
_startRefreshTimer();
}
void _loadFromCache() {
countAntrian = _cachedStats!['data']['antrian'] ?? 0;
countProses = _cachedStats!['data']['proses'] ?? 0;
countSelesai = _cachedStats!['data']['selesai'] ?? 0;
aktivitasTerbaru = _cachedActs!['data'] ?? [];
pesananTiba = _cachedTiba!['data'] ?? [];
isLoading = false;
}
void _startRefreshTimer() {
_refreshTimer?.cancel();
_refreshTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (mounted) fetchAllData(showLoading: false);
});
}
@override
void dispose() {
_refreshTimer?.cancel();
super.dispose();
}
Future<void> fetchAllData({bool showLoading = true}) async {
if (showLoading) setState(() => isLoading = true);
String baseUrl = AppConfig.baseUrl;
try {
final String? token = widget.userData['access_token'];
final responses = await Future.wait([
http.get(Uri.parse('$baseUrl/dashboard/stats'), headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
http.get(Uri.parse('$baseUrl/dashboard/activities'), headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
http.get(Uri.parse('$baseUrl/orders/status/TIBA%20DI%20TOKO'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
http.get(Uri.parse('$baseUrl/shop/settings'), headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
]);
if (responses[0].statusCode == 200 && responses[1].statusCode == 200) {
final dataStats = json.decode(responses[0].body);
final dataActs = json.decode(responses[1].body);
final dataTiba = responses.length > 2 && responses[2].statusCode == 200
? json.decode(responses[2].body)
: {'data': []};
if (responses.length > 3 && responses[3].statusCode == 200) {
final dataSettings = json.decode(responses[3].body);
if (dataSettings['data'] != null) {
widget.userData['shop'] = dataSettings['data'];
}
}
_cachedStats = dataStats;
_cachedActs = dataActs;
_cachedTiba = dataTiba;
setState(() {
countAntrian = dataStats['data']['antrian'] ?? 0;
countProses = dataStats['data']['proses'] ?? 0;
countSelesai = dataStats['data']['selesai'] ?? 0;
aktivitasTerbaru = dataActs['data'] ?? [];
pesananTiba = dataTiba['data'] ?? [];
isLoading = false;
});
} else {
if (mounted && _cachedStats == null) setState(() => isLoading = false);
}
} catch (e) {
if (mounted && _cachedStats == null) setState(() => isLoading = false);
}
}
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]}.');
}
String formatWeightInString(String input) {
return input.replaceAllMapped(RegExp(r'\((\d+\.?\d*)\s*([a-zA-Z]+)\)'),
(match) {
double val = double.tryParse(match.group(1) ?? "0") ?? 0;
String unit = match.group(2) ?? "Kg";
if (unit.toUpperCase() == 'PCS') return "(${val.toInt()} $unit)";
// Format the display string exactly with 3 decimal places (e.g. 2.500 instead of 2.5)
String valStr = val.toStringAsFixed(3);
return "($valStr $unit)";
});
}
List<dynamic> get _filteredActivities {
List<dynamic> filtered = aktivitasTerbaru.where((item) {
String status = item['status']?.toString().toUpperCase() ?? "";
if (_selectedTab == 'antrian' &&
(status == 'ANTRIAN' || status == 'MENUNGGU JEMPUT')) return true;
if (_selectedTab == 'proses' &&
(status == 'PROSES' ||
status == 'JEMPUTAN TIBA' ||
status == 'MENCUCI' ||
status == 'MENYETRIKA')) return true;
if (_selectedTab == 'selesai' && status == 'SELESAI') return true;
return false;
}).toList();
filtered.sort((a, b) {
int priority(String type) {
String t = type.toLowerCase();
if (t.contains('kilat')) return 1;
if (t.contains('ekspres') || t.contains('express')) return 2;
return 3;
}
int pA = priority((a['estimation_type'] ?? '').toString());
int pB = priority((b['estimation_type'] ?? '').toString());
if (pA != pB) return pA.compareTo(pB);
final fallbackTime = DateTime(2100, 1, 1);
DateTime parseTime(dynamic timeStr) {
if (timeStr == null || timeStr.toString().isEmpty) {
return fallbackTime;
}
return DateTime.tryParse(timeStr.toString()) ?? fallbackTime;
}
DateTime timeA = parseTime(a['estimation_time']);
DateTime timeB = parseTime(b['estimation_time']);
int comp = timeA.compareTo(timeB);
if (comp != 0) return comp;
return (a['id'] as int)
.compareTo(b['id'] as int); // tertiary sort oldest first
});
return filtered;
}
@override
Widget build(BuildContext context) {
return isLoading
? Center(child: CircularProgressIndicator(color: primary))
: Column(
children: [
_buildAppBar(),
Expanded(
child: RefreshIndicator(
onRefresh: () => fetchAllData(),
color: primary,
backgroundColor: Colors.white,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// HERO BANNER
GestureDetector(
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RequestPickupPage(
userData: widget.userData)));
if (result == true) fetchAllData();
},
child: Container(
width: double.infinity,
height: 160,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: surfaceContainerHigh),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 4,
offset: const Offset(0, 2))
],
image: const DecorationImage(
image: AssetImage('assets/beranda.jpg'),
fit: BoxFit.cover,
),
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
primary.withOpacity(0.8),
primary.withOpacity(0.4),
Colors.transparent
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 24),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Halo, ${(widget.userData['user']?['name'] ?? 'Staf').toString().split(' ')[0]}",
style: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w700,
color: Colors.white,
height: 1.1,
),
),
const SizedBox(height: 4),
Text(
"Siap mengelola pesanan hari ini?",
style: GoogleFonts.inter(
fontSize: 12,
color: Colors.white
.withOpacity(0.9),
),
),
const SizedBox(height: 12),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(999),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.05),
blurRadius: 4,
offset:
const Offset(0, 2))
],
),
child: Text(
"Buat Jadwal Pickup",
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w700,
color: primary,
),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
// ORDER TABS
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("Status Pesanan",
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: onSurface)),
GestureDetector(
onTap: () {
String initialFilter = "Semua";
if (_selectedTab == 'antrian') {
initialFilter = "Antrian";
}
if (_selectedTab == 'proses') {
initialFilter = "Proses";
}
if (_selectedTab == 'selesai') {
initialFilter = "Selesai";
}
Navigator.push(
context,
MaterialPageRoute(
builder: (c) => OrderListPage(
initialFilter: initialFilter,
userData: widget.userData)));
},
child: Text("Lihat Semua",
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primary)),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
_buildTabItem(
'antrian', 'Antrian ($countAntrian)'),
_buildTabItem(
'proses', 'Proses ($countProses)'),
_buildTabItem(
'selesai', 'Selesai ($countSelesai)'),
],
),
),
const SizedBox(height: 12),
// TAB CONTENT (Filtered Orders)
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _buildFilteredList(
key: ValueKey(_selectedTab)),
),
const SizedBox(height: 24),
// PESANAN TIBA DI TOKO
if (pesananTiba.isNotEmpty) ...[
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("Tiba di Toko (Belum Diinput)",
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w700,
color: onSurface)),
if (pesananTiba.length > 3)
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (c) => OrderListPage(
initialFilter: "Proses",
userData:
widget.userData)));
},
child: Text("Lihat Semua",
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primary)),
),
],
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: primary.withOpacity(0.2)),
),
child: Column(
children: pesananTiba
.take(3)
.toList()
.asMap()
.entries
.map((entry) {
return _buildTibaRow(
entry.value,
entry.key ==
(pesananTiba.take(3).length - 1));
}).toList(),
),
),
const SizedBox(height: 24),
],
// LAST ACTIVITY
Text("Aktivitas Terakhir",
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: onSurface)),
const SizedBox(height: 12),
_buildRecentActivityList(),
],
),
),
),
],
),
), // End of RefreshIndicator
), // End of Expanded
], // End of Column children
); // End of Column
}
Widget _buildTabItem(String tabId, String label) {
bool isActive = _selectedTab == tabId;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedTab = tabId),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: isActive ? surfaceContainerLowest : Colors.transparent,
borderRadius: BorderRadius.circular(8),
boxShadow: isActive
? [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1))
]
: null,
),
alignment: Alignment.center,
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isActive ? primary : onSurfaceVariant,
),
),
),
),
);
}
Widget _buildFilteredList({Key? key}) {
List<dynamic> items = _filteredActivities;
if (items.isEmpty) {
return Container(
key: key,
width: double.infinity,
padding: const EdgeInsets.all(24),
alignment: Alignment.center,
child: Text("Belum ada pesanan",
style: GoogleFonts.inter(fontSize: 13, color: outline)),
);
}
return Column(
key: key,
children:
items.take(2).map<Widget>((item) => _buildOrderCard(item)).toList(),
);
}
Widget _buildRecentActivityList() {
if (aktivitasTerbaru.isEmpty) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
alignment: Alignment.center,
child: Text("Belum ada aktivitas",
style: GoogleFonts.inter(fontSize: 13, color: outline)),
);
}
return Column(
children: aktivitasTerbaru
.take(5)
.map<Widget>((item) => _buildOrderCard(item, isClickable: false))
.toList(),
);
}
Widget _buildOrderCard(dynamic item, {bool isClickable = true}) {
String title = item['title']?.toString() ?? "Pesanan";
String rawSubtitle = item['subtitle']?.toString() ?? "-";
String subtitle = formatWeightInString(rawSubtitle.split('||').first);
String status = item['status']?.toString().toUpperCase() ?? "PROSES";
String orderId = "#${item['order_number'] ?? item['id'] ?? 'ACT'}";
String price = "";
if (item['total'] != null) {
price = "Rp ${formatHarga(item['total'])}";
}
if (status == 'MENUNGGU JEMPUT') status = 'PICKUP';
if (status == 'JEMPUTAN TIBA') status = 'DI TOKO';
IconData icon = Icons.local_laundry_service;
if (status == 'ANTRIAN' || status == 'PICKUP') icon = Icons.dry_cleaning;
if (status == 'SELESAI') icon = Icons.checkroom;
if (status == 'DI DRIVER') icon = Icons.delivery_dining;
return InkWell(
onTap: isClickable
? () {
if (item['status']?.toString().toUpperCase() == 'ANTRIAN' ||
item['status']?.toString().toUpperCase() == 'PROSES' ||
item['status']?.toString().toUpperCase() == 'SELESAI') {
_showUpdateStatusDialog(item);
}
}
: null,
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: surfaceContainerLowest,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: surfaceContainerHigh),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: primary.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: primary.withOpacity(0.1)),
),
alignment: Alignment.center,
child: Icon(icon, color: primary, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(orderId,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: onSurfaceVariant.withOpacity(0.7),
letterSpacing: 0.5)),
Text(title,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: onSurface,
height: 1.2),
maxLines: 1,
overflow: TextOverflow.ellipsis),
if (subtitle.isNotEmpty && subtitle != "-") ...[
const SizedBox(height: 2),
Text(subtitle,
style:
GoogleFonts.inter(fontSize: 11, color: outline),
maxLines: 1,
overflow: TextOverflow.ellipsis),
]
],
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: secondaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
status,
style: GoogleFonts.inter(
fontSize: 9,
fontWeight: FontWeight.w700,
color: onSecondaryContainer,
letterSpacing: 0.5),
),
),
if (price.isNotEmpty && price != "Rp 0") ...[
const SizedBox(height: 4),
Text(price,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
color: primary)),
]
],
),
],
),
));
}
void _showUpdateStatusDialog(dynamic item) {
String currentStatus = item['status']?.toString().toUpperCase() ?? "";
String orderIdStr =
item['order_number']?.toString() ?? item['id']?.toString() ?? "";
int id = int.tryParse(item['id']?.toString() ?? "0") ?? 0;
if (id == 0) return;
String promptText = "";
bool isUnpaid = false;
if (currentStatus == 'ANTRIAN') {
promptText =
"Mulai proses cucian (Mencuci/Menyetrika) untuk pesanan #$orderIdStr?";
} else if (currentStatus == 'PROSES') {
promptText = "Tandai pesanan #$orderIdStr sebagai SELESAI?";
} else if (currentStatus == 'SELESAI') {
String deliveryType =
item['delivery_type']?.toString().toLowerCase() ?? 'none';
if (deliveryType == 'delivery' || deliveryType == 'both') {
promptText =
"Tugaskan pesanan #$orderIdStr ke Driver untuk pengiriman (DELIVERY)?";
} else {
String paymentStatus =
item['payment_status']?.toString().toUpperCase() ?? "UNPAID";
if (paymentStatus != 'PAID') {
isUnpaid = true;
promptText =
"Pesanan #$orderIdStr belum lunas. Lanjutkan ke halaman pelunasan?";
} else {
promptText =
"Selesaikan pesanan #$orderIdStr dan ubah menjadi DIAMBIL?";
}
}
} else {
return;
}
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text("Lanjutkan Pesanan?",
style: GoogleFonts.inter(fontWeight: FontWeight.w700)),
content: Text(promptText, style: GoogleFonts.inter(fontSize: 14)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text("Batal",
style: GoogleFonts.inter(color: Colors.grey)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: primary, foregroundColor: Colors.white),
onPressed: () {
Navigator.pop(ctx);
if (isUnpaid) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePaymentPage(
order: item,
userData: widget.userData,
),
),
).then((_) => fetchAllData());
} else {
_updateOrderStatus(id);
}
},
child: Text("Ya, Lanjutkan",
style: GoogleFonts.inter(fontWeight: FontWeight.w600)),
),
],
));
}
Future<void> _updateOrderStatus(int id) async {
setState(() => isLoading = true);
try {
final String? token = widget.userData['access_token'];
final response = await http.put(
Uri.parse('${AppConfig.baseUrl}/orders/$id/update-status'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
fetchAllData();
} else {
setState(() => isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Gagal memperbarui status")));
}
}
} catch (e) {
if (mounted) setState(() => isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Terjadi kesalahan jaringan")));
}
}
}
Widget _buildTibaRow(dynamic order, bool isLast) {
String orderId = "#${order['order_number'] ?? order['id'] ?? 'ACT'}";
String name = order['customer_name'] ?? "Pelanggan";
return InkWell(
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (c) => Scaffold(
appBar: AppBar(
title: Text("Input Layanan Pickup",
style: GoogleFonts.inter(
fontWeight: FontWeight.w700, fontSize: 16)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded,
size: 20),
onPressed: () => Navigator.pop(c)),
),
body: LaundryPage(
userData: widget.userData,
isTab: false,
pickupData: order))));
if (result == true) fetchAllData();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: isLast
? null
: Border(
bottom:
BorderSide(color: surfaceContainerHigh.withOpacity(0.5))),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.inventory_2_rounded, color: primary, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name,
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
fontSize: 14,
color: onSurface)),
const SizedBox(height: 2),
Text(orderId,
style: GoogleFonts.inter(fontSize: 11, color: outline)),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: primary, borderRadius: BorderRadius.circular(6)),
child: Text("Input",
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.white)),
)
],
),
),
);
}
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), // Padding equivalent to empty action area to align center if needed, or just leave it empty.
],
),
),
);
}
}