1034 lines
41 KiB
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.
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|