feat: order detail

This commit is contained in:
efrilm
2025-08-29 19:08:18 +07:00
parent dcf76b5fed
commit 2d596a25b9
4 changed files with 1076 additions and 132 deletions
@@ -1,7 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../router/app_router.gr.dart';
import '../order_page.dart';
class OrderCard extends StatelessWidget {
@@ -10,22 +12,21 @@ class OrderCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.06),
blurRadius: 16,
offset: const Offset(0, 3),
),
],
),
child: InkWell(
onTap: () => _showOrderDetail(order),
borderRadius: BorderRadius.circular(16),
return GestureDetector(
onTap: () => context.router.push(OrderDetailRoute(order: order)),
child: Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.06),
blurRadius: 16,
offset: const Offset(0, 3),
),
],
),
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
@@ -325,8 +326,4 @@ class OrderCard extends StatelessWidget {
final formatter = NumberFormat('#,###');
return formatter.format(amount);
}
void _showOrderDetail(Order order) {
// Implementation for showing order details
}
}
@@ -0,0 +1,905 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../common/theme/theme.dart';
import '../../main/pages/order/order_page.dart';
@RoutePage()
class OrderDetailPage extends StatefulWidget {
final Order order;
const OrderDetailPage({super.key, required this.order});
@override
State<OrderDetailPage> createState() => _OrderDetailPageState();
}
class _OrderDetailPageState extends State<OrderDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.white,
appBar: _buildAppBar(),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildOrderHeader(),
const SizedBox(height: 24),
_buildCustomerInfo(),
const SizedBox(height: 24),
_buildOrderItems(),
const SizedBox(height: 24),
_buildOrderSummary(),
if (widget.order.notes != null) ...[
const SizedBox(height: 24),
_buildOrderNotes(),
],
const SizedBox(height: 32),
_buildActionButtons(),
const SizedBox(height: 16),
],
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
elevation: 0,
backgroundColor: AppColor.white,
leading: IconButton(
onPressed: () => context.router.back(),
icon: const Icon(Icons.arrow_back, color: AppColor.textPrimary),
),
title: Text(
'Detail Pesanan',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
actions: [
IconButton(
onPressed: _shareOrder,
icon: const Icon(Icons.share, color: AppColor.textSecondary),
),
PopupMenuButton<String>(
onSelected: _handleMenuAction,
icon: const Icon(Icons.more_vert, color: AppColor.textSecondary),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 20),
SizedBox(width: 12),
Text('Edit Pesanan'),
],
),
),
const PopupMenuItem(
value: 'duplicate',
child: Row(
children: [
Icon(Icons.content_copy, size: 20),
SizedBox(width: 12),
Text('Duplikat Pesanan'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 20, color: Colors.red),
SizedBox(width: 12),
Text('Hapus Pesanan', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
);
}
Widget _buildOrderHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.order.id,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
_buildStatusBadge(widget.order.status),
],
),
const SizedBox(height: 12),
Row(
children: [
Icon(Icons.access_time, size: 16, color: AppColor.textSecondary),
const SizedBox(width: 8),
Text(
_formatDateTime(widget.order.orderDate),
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
],
),
);
}
Widget _buildStatusBadge(OrderStatus status) {
Color backgroundColor;
Color textColor;
String text;
switch (status) {
case OrderStatus.pending:
backgroundColor = Colors.orange.withOpacity(0.1);
textColor = Colors.orange;
text = 'Menunggu';
break;
case OrderStatus.processing:
backgroundColor = Colors.blue.withOpacity(0.1);
textColor = Colors.blue;
text = 'Diproses';
break;
case OrderStatus.completed:
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green;
text = 'Selesai';
break;
case OrderStatus.cancelled:
backgroundColor = Colors.red.withOpacity(0.1);
textColor = Colors.red;
text = 'Dibatalkan';
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
text,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: textColor,
),
),
);
}
Widget _buildCustomerInfo() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Informasi Pelanggan',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 16),
_buildInfoRow(
icon: Icons.person,
label: 'Nama',
value: widget.order.customerName,
),
if (widget.order.phoneNumber != null) ...[
const SizedBox(height: 12),
_buildInfoRow(
icon: Icons.phone,
label: 'Telepon',
value: widget.order.phoneNumber!,
isClickable: true,
),
],
if (widget.order.address != null) ...[
const SizedBox(height: 12),
_buildInfoRow(
icon: Icons.location_on,
label: 'Alamat',
value: widget.order.address!,
isMultiline: true,
),
],
],
),
);
}
Widget _buildInfoRow({
required IconData icon,
required String label,
required String value,
bool isClickable = false,
bool isMultiline = false,
}) {
return Row(
crossAxisAlignment: isMultiline
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 18, color: AppColor.primary),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
GestureDetector(
onTap: isClickable ? () => _callCustomer(value) : null,
child: Text(
value,
style: AppStyle.sm.copyWith(
color: isClickable
? AppColor.primary
: AppColor.textPrimary,
fontWeight: FontWeight.w500,
decoration: isClickable ? TextDecoration.underline : null,
),
),
),
],
),
),
],
);
}
Widget _buildOrderItems() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Item Pesanan',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 16),
...widget.order.items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Column(
children: [
if (index > 0)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Divider(color: AppColor.border, height: 1),
),
_buildOrderItem(item),
],
);
}).toList(),
],
),
);
}
Widget _buildOrderItem(OrderItem item) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: item.imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
item.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.restaurant,
color: AppColor.primary,
size: 24,
);
},
),
)
: Icon(Icons.restaurant, color: AppColor.primary, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Rp ${_formatCurrency(item.price)} x ${item.quantity}',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
if (item.notes != null) ...[
const SizedBox(height: 4),
Text(
'Catatan: ${item.notes}',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontStyle: FontStyle.italic,
),
),
],
],
),
),
Text(
'Rp ${_formatCurrency(item.price * item.quantity)}',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
],
);
}
Widget _buildOrderSummary() {
final subtotal = widget.order.items.fold<double>(
0,
(sum, item) => sum + (item.price * item.quantity),
);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Pesanan',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 16),
_buildSummaryRow('Subtotal', subtotal),
const SizedBox(height: 8),
_buildSummaryRow('Ongkos Kirim', 10000),
const SizedBox(height: 8),
_buildSummaryRow('Pajak', subtotal * 0.1),
const SizedBox(height: 12),
Divider(color: AppColor.border, height: 1),
const SizedBox(height: 12),
_buildSummaryRow('Total', widget.order.totalAmount, isBold: true),
],
),
);
}
Widget _buildSummaryRow(String label, double amount, {bool isBold = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
),
),
Text(
'Rp ${_formatCurrency(amount)}',
style: AppStyle.sm.copyWith(
color: isBold ? AppColor.textPrimary : AppColor.textSecondary,
fontWeight: isBold ? FontWeight.bold : FontWeight.w500,
),
),
],
);
}
Widget _buildOrderNotes() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.note, size: 18, color: AppColor.primary),
const SizedBox(width: 8),
Text(
'Catatan Pesanan',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.border),
),
child: Text(
widget.order.notes!,
style: AppStyle.sm.copyWith(
color: AppColor.textPrimary,
height: 1.5,
),
),
),
],
),
);
}
Widget _buildActionButtons() {
return Column(
children: [
if (widget.order.status == OrderStatus.pending) ...[
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _cancelOrder,
icon: const Icon(Icons.cancel, size: 20),
label: const Text('Batalkan'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _processOrder,
icon: const Icon(Icons.check, size: 20),
label: const Text('Proses'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
foregroundColor: AppColor.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
] else if (widget.order.status == OrderStatus.processing) ...[
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _completeOrder,
icon: const Icon(Icons.check_circle, size: 20),
label: const Text('Selesaikan Pesanan'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: AppColor.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _contactCustomer,
icon: const Icon(Icons.chat, size: 20),
label: const Text('Hubungi Pelanggan'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColor.primary,
side: BorderSide(color: AppColor.primary),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
);
}
String _formatDateTime(DateTime dateTime) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Ags',
'Sep',
'Okt',
'Nov',
'Des',
];
final day = dateTime.day;
final month = months[dateTime.month - 1];
final year = dateTime.year;
final hour = dateTime.hour.toString().padLeft(2, '0');
final minute = dateTime.minute.toString().padLeft(2, '0');
return '$day $month $year, $hour:$minute WIB';
}
String _formatCurrency(double amount) {
return amount
.toStringAsFixed(0)
.replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
void _shareOrder() {
// Implement share functionality
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Fitur berbagi akan segera hadir'),
backgroundColor: AppColor.primary,
),
);
}
void _handleMenuAction(String action) {
switch (action) {
case 'edit':
_editOrder();
break;
case 'duplicate':
_duplicateOrder();
break;
case 'delete':
_deleteOrder();
break;
}
}
void _editOrder() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Fitur edit pesanan akan segera hadir'),
backgroundColor: AppColor.primary,
),
);
}
void _duplicateOrder() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pesanan berhasil diduplikat'),
backgroundColor: Colors.green,
),
);
}
void _deleteOrder() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Hapus Pesanan',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
content: Text(
'Apakah Anda yakin ingin menghapus pesanan ${widget.order.id}? Tindakan ini tidak dapat dibatalkan.',
style: AppStyle.md,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Batal',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
ElevatedButton(
onPressed: () {
context.router.back();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pesanan berhasil dihapus'),
backgroundColor: Colors.red,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Hapus'),
),
],
),
);
}
void _processOrder() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Proses Pesanan',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
content: Text(
'Apakah Anda yakin ingin memproses pesanan ${widget.order.id}?',
style: AppStyle.md,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Batal',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pesanan sedang diproses'),
backgroundColor: Colors.blue,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
foregroundColor: Colors.white,
),
child: const Text('Proses'),
),
],
),
);
}
void _completeOrder() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Selesaikan Pesanan',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
content: Text(
'Apakah pesanan ${widget.order.id} sudah selesai dan siap dikirim?',
style: AppStyle.md,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Belum',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pesanan berhasil diselesaikan'),
backgroundColor: Colors.green,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('Selesai'),
),
],
),
);
}
void _cancelOrder() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Batalkan Pesanan',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
content: Text(
'Apakah Anda yakin ingin membatalkan pesanan ${widget.order.id}?',
style: AppStyle.md,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Tidak',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pesanan berhasil dibatalkan'),
backgroundColor: Colors.red,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Batalkan'),
),
],
),
);
}
void _contactCustomer() {
showModalBottomSheet(
context: context,
backgroundColor: AppColor.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.border,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
Text(
'Hubungi ${widget.order.customerName}',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 24),
if (widget.order.phoneNumber != null)
ListTile(
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.phone, color: Colors.green),
),
title: const Text('Telepon'),
subtitle: Text(widget.order.phoneNumber!),
onTap: () => _callCustomer(widget.order.phoneNumber!),
),
ListTile(
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.chat, color: Colors.blue),
),
title: const Text('Kirim Pesan'),
subtitle: const Text('Via WhatsApp'),
onTap: () => _sendMessage(),
),
const SizedBox(height: 16),
],
),
),
);
}
void _callCustomer(String phoneNumber) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Menghubungi $phoneNumber...'),
backgroundColor: Colors.green,
),
);
}
void _sendMessage() {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Membuka WhatsApp...'),
backgroundColor: Colors.blue,
),
);
}
}