From 09d8f6af69a0d76393cf65a409ced15af3fd7fc9 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 27 Aug 2025 20:02:49 +0700 Subject: [PATCH] feat: order page --- .../pages/main/pages/order/order_page.dart | 351 +++++++++++++++++- .../main/pages/order/widgets/order_card.dart | 332 +++++++++++++++++ 2 files changed, 678 insertions(+), 5 deletions(-) create mode 100644 lib/presentation/pages/main/pages/order/widgets/order_card.dart diff --git a/lib/presentation/pages/main/pages/order/order_page.dart b/lib/presentation/pages/main/pages/order/order_page.dart index 9dd5178..fe30fe6 100644 --- a/lib/presentation/pages/main/pages/order/order_page.dart +++ b/lib/presentation/pages/main/pages/order/order_page.dart @@ -1,12 +1,353 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -@RoutePage() -class OrderPage extends StatelessWidget { - const OrderPage({super.key}); +import '../../../../../common/theme/theme.dart'; +import 'widgets/order_card.dart'; + +// Model untuk Order +class Order { + final String id; + final String customerName; + final DateTime orderDate; + final List items; + final double totalAmount; + final OrderStatus status; + final String? notes; + final String? phoneNumber; + final String? address; + + Order({ + required this.id, + required this.customerName, + required this.orderDate, + required this.items, + required this.totalAmount, + required this.status, + this.notes, + this.phoneNumber, + this.address, + }); +} + +class OrderItem { + final String name; + final int quantity; + final double price; + final String? imageUrl; + final String? notes; + + OrderItem({ + required this.name, + required this.quantity, + required this.price, + this.imageUrl, + this.notes, + }); +} + +enum OrderStatus { pending, processing, completed, cancelled } + +class OrderPage extends StatefulWidget { + const OrderPage({Key? key}) : super(key: key); + + @override + State createState() => _OrderPageState(); +} + +class _OrderPageState extends State with TickerProviderStateMixin { + late TabController _tabController; + bool _isLoading = true; + List _orders = []; + + // Filter states + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 5, vsync: this); + _loadOrders(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + void _loadOrders() { + // Simulate loading + Future.delayed(const Duration(seconds: 2), () { + setState(() { + _isLoading = false; + // Uncomment untuk testing dengan data + _orders = _generateSampleOrders(); + }); + }); + } + + List _generateSampleOrders() { + return [ + Order( + id: "ORD-001", + customerName: "John Doe", + orderDate: DateTime.now().subtract(const Duration(hours: 2)), + address: "Jl. Malioboro No. 123, Yogyakarta", + items: [ + OrderItem( + name: "Nasi Gudeg", + quantity: 2, + price: 25000, + notes: "Pedas sedang", + ), + OrderItem(name: "Es Teh Manis", quantity: 2, price: 8000), + OrderItem(name: "Kerupuk", quantity: 1, price: 5000), + ], + totalAmount: 71000, + status: OrderStatus.pending, + notes: "Tolong diantar sebelum jam 2 siang", + ), + Order( + id: "ORD-002", + customerName: "Jane Smith", + orderDate: DateTime.now().subtract(const Duration(hours: 1)), + address: "Jl. Sultan Agung No. 45, Yogyakarta", + items: [ + OrderItem( + name: "Ayam Bakar", + quantity: 1, + price: 35000, + notes: "Tidak pedas", + ), + OrderItem(name: "Nasi Putih", quantity: 1, price: 5000), + OrderItem(name: "Lalapan", quantity: 1, price: 8000), + ], + totalAmount: 48000, + status: OrderStatus.processing, + ), + Order( + id: "ORD-003", + customerName: "Bob Wilson", + orderDate: DateTime.now().subtract(const Duration(minutes: 30)), + phoneNumber: "+62 811-2345-6789", + items: [ + OrderItem(name: "Gado-gado", quantity: 2, price: 20000), + OrderItem(name: "Lontong", quantity: 2, price: 3000), + OrderItem(name: "Es Jeruk", quantity: 2, price: 10000), + ], + totalAmount: 66000, + status: OrderStatus.completed, + ), + ]; + } @override Widget build(BuildContext context) { - return Center(child: Text('Order Page')); + return Scaffold( + backgroundColor: AppColor.background, + appBar: _buildAppBar(), + body: Column( + children: [ + _buildTabBar(), + Expanded( + child: _isLoading ? _buildLoadingState() : _buildOrderContent(), + ), + ], + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + elevation: 0, + backgroundColor: AppColor.white, + title: Text('Pesanan'), + actions: [ + IconButton( + onPressed: _showFilterDialog, + icon: const Icon(Icons.filter_list, color: AppColor.textSecondary), + ), + IconButton( + onPressed: _refreshOrders, + icon: const Icon(Icons.refresh, color: AppColor.textSecondary), + ), + ], + ); + } + + Widget _buildTabBar() { + return Container( + color: AppColor.white, + child: TabBar( + controller: _tabController, + isScrollable: true, + labelColor: AppColor.primary, + unselectedLabelColor: AppColor.textSecondary, + indicatorColor: AppColor.primary, + indicatorWeight: 3, + labelStyle: AppStyle.md.copyWith(fontWeight: FontWeight.w600), + tabAlignment: TabAlignment.start, + unselectedLabelStyle: AppStyle.md, + tabs: const [ + Tab(text: 'Semua'), + Tab(text: 'Menunggu'), + Tab(text: 'Diproses'), + Tab(text: 'Selesai'), + Tab(text: 'Dibatalkan'), + ], + ), + ); + } + + Widget _buildOrderContent() { + if (_orders.isEmpty) { + return _buildEmptyState(); + } + + return TabBarView( + controller: _tabController, + children: [ + _buildOrderList(_orders), + _buildOrderList( + _orders.where((o) => o.status == OrderStatus.pending).toList(), + ), + _buildOrderList( + _orders.where((o) => o.status == OrderStatus.processing).toList(), + ), + _buildOrderList( + _orders.where((o) => o.status == OrderStatus.completed).toList(), + ), + _buildOrderList( + _orders.where((o) => o.status == OrderStatus.cancelled).toList(), + ), + ], + ); + } + + Widget _buildOrderList(List orders) { + if (orders.isEmpty) { + return _buildEmptyState(); + } + + return RefreshIndicator( + onRefresh: _refreshOrders, + color: AppColor.primary, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: orders.length, + itemBuilder: (context, index) { + return OrderCard(order: orders[index]); + }, + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(60), + ), + child: Icon( + Icons.receipt_long, + size: 60, + color: AppColor.primary.withOpacity(0.5), + ), + ), + const SizedBox(height: 24), + Text( + 'Belum Ada Pesanan', + style: AppStyle.h6.copyWith( + fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + ), + const SizedBox(height: 8), + Text( + 'Pesanan akan muncul di sini setelah\npelanggan mulai memesan.', + style: AppStyle.md.copyWith(color: AppColor.textSecondary), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: _refreshOrders, + icon: const Icon(Icons.refresh, size: 20), + label: const Text('Muat Ulang'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primary, + foregroundColor: AppColor.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ); + } + + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(AppColor.primary), + ), + const SizedBox(height: 16), + Text( + 'Memuat pesanan...', + style: AppStyle.md.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ); + } + + void _showFilterDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Filter Pesanan', + style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Add filter options here + Text('Opsi filter akan segera hadir...', style: AppStyle.md), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Tutup', + style: AppStyle.md.copyWith(color: AppColor.primary), + ), + ), + ], + ); + }, + ); + } + + Future _refreshOrders() async { + setState(() { + _isLoading = true; + }); + await Future.delayed(const Duration(seconds: 1)); + setState(() { + _isLoading = false; + // Uncomment untuk testing dengan data + _orders = _generateSampleOrders(); + }); } } diff --git a/lib/presentation/pages/main/pages/order/widgets/order_card.dart b/lib/presentation/pages/main/pages/order/widgets/order_card.dart new file mode 100644 index 0000000..15efd3c --- /dev/null +++ b/lib/presentation/pages/main/pages/order/widgets/order_card.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../../../../common/theme/theme.dart'; +import '../order_page.dart'; + +class OrderCard extends StatelessWidget { + final Order order; + const OrderCard({super.key, required this.order}); + + @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), + child: Padding( + padding: const EdgeInsets.all(18), + child: Column( + children: [ + _buildHeader(), + const SizedBox(height: 16), + _buildContent(), + const SizedBox(height: 16), + _buildFooter(), + ], + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + order.id, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.primary, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + DateFormat('dd MMM yyyy • HH:mm').format(order.orderDate), + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ), + _buildStatusChip(), + ], + ); + } + + Widget _buildStatusChip() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: _getStatusColor().withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _getStatusColor().withOpacity(0.2), width: 1), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(_getStatusIcon(), size: 12, color: _getStatusColor()), + const SizedBox(width: 6), + Text( + _getStatusText(), + style: AppStyle.sm.copyWith( + color: _getStatusColor(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _buildContent() { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColor.background, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.restaurant_menu_outlined, + size: 16, + color: AppColor.textSecondary, + ), + const SizedBox(width: 6), + Text( + '${order.items.length} item pesanan', + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w500, + color: AppColor.textSecondary, + ), + ), + ], + ), + const SizedBox(height: 10), + ...order.items + .take(3) + .map( + (item) => Container( + margin: const EdgeInsets.only(bottom: 6), + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + '${item.quantity}', + style: AppStyle.xs.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.primary, + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + item.name, + style: AppStyle.sm.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + 'Rp ${_formatCurrency(item.price * item.quantity)}', + style: AppStyle.sm.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + if (order.items.length > 3) ...[ + Container( + margin: const EdgeInsets.only(top: 4), + child: Text( + '+${order.items.length - 3} item lainnya', + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + if (order.notes != null) ...[ + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColor.warning.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColor.warning.withOpacity(0.2)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.sticky_note_2_outlined, + size: 14, + color: AppColor.warning, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + order.notes!, + style: AppStyle.xs.copyWith( + color: AppColor.textPrimary, + height: 1.3, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ); + } + + Widget _buildFooter() { + return Column( + children: [ + Container( + height: 1, + width: double.infinity, + color: AppColor.border.withOpacity(0.3), + ), + const SizedBox(height: 12), + Row( + children: [ + Icon( + order.address != null + ? Icons.location_on_outlined + : Icons.store_outlined, + size: 16, + color: AppColor.textSecondary, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + order.address ?? 'Ambil di tempat', + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Total', + style: AppStyle.xs.copyWith(color: AppColor.textSecondary), + ), + Text( + 'Rp ${_formatCurrency(order.totalAmount)}', + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.primary, + ), + ), + ], + ), + ], + ), + ], + ); + } + + Color _getStatusColor() { + switch (order.status) { + case OrderStatus.pending: + return AppColor.warning; + case OrderStatus.processing: + return AppColor.info; + case OrderStatus.completed: + return AppColor.success; + case OrderStatus.cancelled: + return AppColor.error; + } + } + + String _getStatusText() { + switch (order.status) { + case OrderStatus.pending: + return 'Menunggu'; + case OrderStatus.processing: + return 'Diproses'; + case OrderStatus.completed: + return 'Selesai'; + case OrderStatus.cancelled: + return 'Dibatalkan'; + } + } + + IconData _getStatusIcon() { + switch (order.status) { + case OrderStatus.pending: + return Icons.schedule; + case OrderStatus.processing: + return Icons.hourglass_empty; + case OrderStatus.completed: + return Icons.check_circle; + case OrderStatus.cancelled: + return Icons.cancel; + } + } + + String _formatCurrency(double amount) { + final formatter = NumberFormat('#,###'); + return formatter.format(amount); + } + + void _showOrderDetail(Order order) { + // Implementation for showing order details + } +}