Compare commits

...

2 Commits

Author SHA1 Message Date
efrilm
34555dd789 order load more 2025-10-27 21:57:25 +07:00
efrilm
8bd61eb58e order page 2025-10-27 21:55:19 +07:00
38 changed files with 8827 additions and 89 deletions

View File

@ -0,0 +1,98 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart' hide Order;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart' hide Order;
import '../../../domain/order/order.dart';
part 'order_loader_event.dart';
part 'order_loader_state.dart';
part 'order_loader_bloc.freezed.dart';
@injectable
class OrderLoaderBloc extends Bloc<OrderLoaderEvent, OrderLoaderState> {
final IOrderRepository _repository;
OrderLoaderBloc(this._repository) : super(OrderLoaderState.initial()) {
on<OrderLoaderEvent>(_onOrderLoaderEvent);
}
Future<void> _onOrderLoaderEvent(
OrderLoaderEvent event,
Emitter<OrderLoaderState> emit,
) {
return event.map(
setSelectedOrder: (e) async {
emit(state.copyWith(selectedOrder: e.order));
},
dateTimeRangeChange: (e) async {
emit(state.copyWith(startDate: e.startDate, endDate: e.endDate));
},
searchChange: (e) async {
emit(state.copyWith(search: e.search));
},
fetched: (e) async {
var newState = state;
if (e.isRefresh) {
newState = newState.copyWith(isFetching: true);
emit(newState);
}
newState = await _mapFetchedToState(
newState,
isRefresh: e.isRefresh,
status: e.status,
);
emit(newState);
},
);
}
Future<OrderLoaderState> _mapFetchedToState(
OrderLoaderState state, {
bool isRefresh = false,
String status = 'completed',
}) async {
state = state.copyWith(isFetching: false);
if (state.hasReachedMax && state.orders.isNotEmpty && !isRefresh) {
return state;
}
if (isRefresh) {
state = state.copyWith(
page: 1,
failureOption: none(),
hasReachedMax: false,
orders: [],
);
}
final failureOrTable = await _repository.getOrders(
page: state.page,
status: status,
startDate: state.startDate,
endDate: state.endDate,
search: state.search,
);
state = failureOrTable.fold(
(f) {
if (state.orders.isNotEmpty) {
return state.copyWith(hasReachedMax: true);
}
return state.copyWith(failureOption: optionOf(f));
},
(orders) {
return state.copyWith(
orders: List.from(state.orders)..addAll(orders.orders),
failureOption: none(),
page: state.page + 1,
hasReachedMax: orders.orders.length < 10,
);
},
);
return state;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
part of 'order_loader_bloc.dart';
@freezed
class OrderLoaderEvent with _$OrderLoaderEvent {
const factory OrderLoaderEvent.dateTimeRangeChange(
DateTime startDate,
DateTime endDate,
) = _DateTimeRangeChange;
const factory OrderLoaderEvent.searchChange(String search) = _SearchChange;
const factory OrderLoaderEvent.setSelectedOrder(Order order) =
_SetSelectedOrder;
const factory OrderLoaderEvent.fetched({
@Default(false) bool isRefresh,
required String status,
}) = _Fetched;
}

View File

@ -0,0 +1,23 @@
part of 'order_loader_bloc.dart';
@freezed
class OrderLoaderState with _$OrderLoaderState {
factory OrderLoaderState({
required List<Order> orders,
required Option<OrderFailure> failureOption,
Order? selectedOrder,
String? search,
required DateTime startDate,
required DateTime endDate,
@Default(false) bool isFetching,
@Default(false) bool hasReachedMax,
@Default(1) int page,
}) = _OrderLoaderState;
factory OrderLoaderState.initial() => OrderLoaderState(
orders: [],
failureOption: none(),
startDate: DateTime.now(),
endDate: DateTime.now(),
);
}

View File

@ -0,0 +1,65 @@
part of 'extension.dart';
const List<String> _dayNames = [
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu',
'Minggu',
];
const List<String> _monthNames = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
extension DateTimeExt on DateTime {
String toFormattedDayDate() {
String dayName = _dayNames[weekday - 1];
String day = this.day.toString();
String month = _monthNames[this.month - 1];
String year = this.year.toString();
return '$dayName, $day $month $year';
}
String toFormattedDate() {
String day = this.day.toString();
String month = _monthNames[this.month - 1];
String year = this.year.toString();
return '$day $month $year';
}
String toFormattedDateTime() {
String day = this.day.toString();
String month = _monthNames[this.month - 1];
String year = this.year.toString();
String hour = this.hour.toString().padLeft(
2,
'0',
); // Menambahkan nol di depan jika jam hanya satu digit
String minute = this.minute.toString().padLeft(
2,
'0',
); // Menambahkan nol di depan jika menit hanya satu digit
String second = this.second.toString().padLeft(
2,
'0',
); // Menambahkan nol di depan jika detik hanya satu digit
return '$day $month $year, $hour:$minute:$second';
}
}

View File

@ -7,3 +7,4 @@ part 'build_context_extension.dart';
part 'int_extension.dart';
part 'double_extension.dart';
part 'string_extension.dart';
part 'datetime_extension.dart';

View File

@ -6,4 +6,5 @@ class ApiPath {
static const String tables = '/api/v1/tables';
static const String customers = '/api/v1/customers';
static const String paymentMethods = '/api/v1/payment-methods';
static const String orders = '/api/v1/orders';
}

View File

@ -0,0 +1,154 @@
part of '../order.dart';
@freezed
class ListOrder with _$ListOrder {
const factory ListOrder({
required List<Order> orders,
required int totalCount,
required int page,
required int limit,
required int totalPages,
}) = _ListOrder;
factory ListOrder.empty() =>
ListOrder(orders: [], totalCount: 0, page: 0, limit: 0, totalPages: 0);
}
@freezed
class Order with _$Order {
const factory Order({
required String id,
required String orderNumber,
required String outletId,
required String userId,
required String tableNumber,
required String orderType,
required String status,
required int subtotal,
required int taxAmount,
required int discountAmount,
required int totalAmount,
required num totalCost,
required int remainingAmount,
required String paymentStatus,
required int refundAmount,
required bool isVoid,
required bool isRefund,
required String notes,
required Map<String, dynamic> metadata,
required DateTime createdAt,
required DateTime updatedAt,
required List<OrderItem> orderItems,
required List<PaymentOrder> payments,
required int totalPaid,
required int paymentCount,
required String splitType,
}) = _Order;
factory Order.empty() => Order(
id: '',
orderNumber: '',
outletId: '',
userId: '',
tableNumber: '',
orderType: '',
status: '',
subtotal: 0,
taxAmount: 0,
discountAmount: 0,
totalAmount: 0,
totalCost: 0,
remainingAmount: 0,
paymentStatus: '',
refundAmount: 0,
isVoid: false,
isRefund: false,
notes: '',
metadata: const {},
createdAt: DateTime(1970),
updatedAt: DateTime(1970),
orderItems: const [],
payments: const [],
totalPaid: 0,
paymentCount: 0,
splitType: '',
);
}
@freezed
class OrderItem with _$OrderItem {
const factory OrderItem({
required String id,
required String orderId,
required String productId,
required String productName,
required String productVariantId,
required String productVariantName,
required int quantity,
required int unitPrice,
required int totalPrice,
required List<dynamic> modifiers,
required String notes,
required String status,
required DateTime createdAt,
required DateTime updatedAt,
required String printerType,
required int paidQuantity,
}) = _OrderItem;
factory OrderItem.empty() => OrderItem(
id: '',
orderId: '',
productId: '',
productName: '',
productVariantId: '',
productVariantName: '',
quantity: 0,
unitPrice: 0,
totalPrice: 0,
modifiers: const [],
notes: '',
status: '',
createdAt: DateTime(1970),
updatedAt: DateTime(1970),
printerType: '',
paidQuantity: 0,
);
}
@freezed
class PaymentOrder with _$PaymentOrder {
const factory PaymentOrder({
required String id,
required String orderId,
required String paymentMethodId,
required String paymentMethodName,
required String paymentMethodType,
required int amount,
required String status,
required int splitNumber,
required int splitTotal,
required String splitDescription,
required int refundAmount,
required Map<String, dynamic> metadata,
required DateTime createdAt,
required DateTime updatedAt,
}) = _PaymentOrder;
factory PaymentOrder.empty() => PaymentOrder(
id: '',
orderId: '',
paymentMethodId: '',
paymentMethodName: '',
paymentMethodType: '',
amount: 0,
status: '',
splitNumber: 0,
splitTotal: 0,
splitDescription: '',
refundAmount: 0,
metadata: const {},
createdAt: DateTime(1970),
updatedAt: DateTime(1970),
);
}

View File

@ -0,0 +1,12 @@
part of '../order.dart';
@freezed
sealed class OrderFailure with _$OrderFailure {
const factory OrderFailure.serverError(ApiFailure failure) = _ServerError;
const factory OrderFailure.unexpectedError() = _UnexpectedError;
const factory OrderFailure.empty() = _Empty;
const factory OrderFailure.localStorageError(String erroMessage) =
_LocalStorageError;
const factory OrderFailure.dynamicErrorMessage(String erroMessage) =
_DynamicErrorMessage;
}

View File

@ -0,0 +1,10 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart';
part 'order.freezed.dart';
part 'entities/order_entity.dart';
part 'failures/order_failure.dart';
part 'repositories/i_order_repository.dart';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
part of '../order.dart';
abstract class IOrderRepository {
Future<Either<OrderFailure, ListOrder>> getOrders({
int page = 1,
int limit = 10,
String status = 'completed',
required DateTime startDate,
required DateTime endDate,
String? search,
});
}

View File

@ -0,0 +1,61 @@
import 'dart:developer';
import 'package:data_channel/data_channel.dart';
import 'package:injectable/injectable.dart';
import 'package:intl/intl.dart';
import '../../../common/api/api_client.dart';
import '../../../common/api/api_failure.dart';
import '../../../common/function/app_function.dart';
import '../../../common/url/api_path.dart';
import '../../../domain/order/order.dart';
import '../order_dtos.dart';
@injectable
class OrderRemoteDataProvider {
final ApiClient _apiClient;
final _logName = 'OrderRemoteDataProvider';
OrderRemoteDataProvider(this._apiClient);
Future<DC<OrderFailure, ListOrderDto>> fetchOrders({
int page = 1,
int limit = 10,
String status = 'completed',
required DateTime startDate,
required DateTime endDate,
String? search,
}) async {
try {
Map<String, dynamic> params = {
'page': page,
'limit': limit,
'status': status,
'date_from': DateFormat('dd-MM-yyyy').format(startDate),
'date_to': DateFormat('dd-MM-yyyy').format(endDate),
};
if (search != null && search.isNotEmpty) {
params['search'] = search;
}
final response = await _apiClient.get(
ApiPath.orders,
params: params,
headers: getAuthorizationHeader(),
);
if (response.data['success'] == false) {
return DC.error(OrderFailure.unexpectedError());
}
final orders = ListOrderDto.fromJson(
response.data['data'] as Map<String, dynamic>,
);
return DC.data(orders);
} on ApiFailure catch (e, s) {
log('fetchOrderError', name: _logName, error: e, stackTrace: s);
return DC.error(OrderFailure.serverError(e));
}
}
}

View File

@ -0,0 +1,182 @@
part of '../order_dtos.dart';
@freezed
class ListOrderDto with _$ListOrderDto {
const ListOrderDto._();
const factory ListOrderDto({
@JsonKey(name: "orders") List<OrderDto>? orders,
@JsonKey(name: 'total_count') int? totalCount,
@JsonKey(name: 'page') int? page,
@JsonKey(name: 'limit') int? limit,
@JsonKey(name: 'total_pages') int? totalPages,
}) = _ListOrderDto;
factory ListOrderDto.fromJson(Map<String, dynamic> json) =>
_$ListOrderDtoFromJson(json);
ListOrder toDomain() => ListOrder(
orders: orders?.map((e) => e.toDomain()).toList() ?? [],
totalCount: totalCount ?? 0,
page: page ?? 0,
limit: limit ?? 0,
totalPages: totalPages ?? 0,
);
}
@freezed
class OrderDto with _$OrderDto {
const OrderDto._();
const factory OrderDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "order_number") String? orderNumber,
@JsonKey(name: "outlet_id") String? outletId,
@JsonKey(name: "user_id") String? userId,
@JsonKey(name: "table_number") String? tableNumber,
@JsonKey(name: "order_type") String? orderType,
@JsonKey(name: "status") String? status,
@JsonKey(name: "subtotal") int? subtotal,
@JsonKey(name: "tax_amount") int? taxAmount,
@JsonKey(name: "discount_amount") int? discountAmount,
@JsonKey(name: "total_amount") int? totalAmount,
@JsonKey(name: "total_cost") num? totalCost,
@JsonKey(name: "remaining_amount") int? remainingAmount,
@JsonKey(name: "payment_status") String? paymentStatus,
@JsonKey(name: "refund_amount") int? refundAmount,
@JsonKey(name: "is_void") bool? isVoid,
@JsonKey(name: "is_refund") bool? isRefund,
@JsonKey(name: "notes") String? notes,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
@JsonKey(name: "order_items") List<OrderItemDto>? orderItems,
@JsonKey(name: "payments") List<PaymentOrderDto>? payments,
@JsonKey(name: "total_paid") int? totalPaid,
@JsonKey(name: "payment_count") int? paymentCount,
@JsonKey(name: "split_type") String? splitType,
}) = _OrderDto;
factory OrderDto.fromJson(Map<String, dynamic> json) =>
_$OrderDtoFromJson(json);
// Optional: mapper ke domain entity
Order toDomain() => Order(
id: id ?? '',
orderNumber: orderNumber ?? '',
outletId: outletId ?? '',
userId: userId ?? '',
tableNumber: tableNumber ?? '',
orderType: orderType ?? '',
status: status ?? '',
subtotal: subtotal ?? 0,
taxAmount: taxAmount ?? 0,
discountAmount: discountAmount ?? 0,
totalAmount: totalAmount ?? 0,
totalCost: totalCost ?? 0,
remainingAmount: remainingAmount ?? 0,
paymentStatus: paymentStatus ?? '',
refundAmount: refundAmount ?? 0,
isVoid: isVoid ?? false,
isRefund: isRefund ?? false,
notes: notes ?? '',
metadata: metadata ?? const {},
createdAt: createdAt != null ? DateTime.parse(createdAt!) : DateTime(1970),
updatedAt: updatedAt != null ? DateTime.parse(updatedAt!) : DateTime(1970),
orderItems: orderItems?.map((e) => e.toDomain()).toList() ?? const [],
payments: payments?.map((e) => e.toDomain()).toList() ?? const [],
totalPaid: totalPaid ?? 0,
paymentCount: paymentCount ?? 0,
splitType: splitType ?? '',
);
}
@freezed
class OrderItemDto with _$OrderItemDto {
const OrderItemDto._();
const factory OrderItemDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "order_id") String? orderId,
@JsonKey(name: "product_id") String? productId,
@JsonKey(name: "product_name") String? productName,
@JsonKey(name: "product_variant_id") String? productVariantId,
@JsonKey(name: "product_variant_name") String? productVariantName,
@JsonKey(name: "quantity") int? quantity,
@JsonKey(name: "unit_price") int? unitPrice,
@JsonKey(name: "total_price") int? totalPrice,
@JsonKey(name: "modifiers") List<dynamic>? modifiers,
@JsonKey(name: "notes") String? notes,
@JsonKey(name: "status") String? status,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
@JsonKey(name: "printer_type") String? printerType,
@JsonKey(name: "paid_quantity") int? paidQuantity,
}) = _OrderItemDto;
factory OrderItemDto.fromJson(Map<String, dynamic> json) =>
_$OrderItemDtoFromJson(json);
// Optional mapper to domain entity
OrderItem toDomain() => OrderItem(
id: id ?? '',
orderId: orderId ?? '',
productId: productId ?? '',
productName: productName ?? '',
productVariantId: productVariantId ?? '',
productVariantName: productVariantName ?? '',
quantity: quantity ?? 0,
unitPrice: unitPrice ?? 0,
totalPrice: totalPrice ?? 0,
modifiers: modifiers ?? [],
notes: notes ?? '',
status: status ?? '',
createdAt: createdAt != null ? DateTime.parse(createdAt!) : DateTime(1970),
updatedAt: updatedAt != null ? DateTime.parse(updatedAt!) : DateTime(1970),
printerType: printerType ?? '',
paidQuantity: paidQuantity ?? 0,
);
}
@freezed
class PaymentOrderDto with _$PaymentOrderDto {
const PaymentOrderDto._();
const factory PaymentOrderDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "order_id") String? orderId,
@JsonKey(name: "payment_method_id") String? paymentMethodId,
@JsonKey(name: "payment_method_name") String? paymentMethodName,
@JsonKey(name: "payment_method_type") String? paymentMethodType,
@JsonKey(name: "amount") int? amount,
@JsonKey(name: "status") String? status,
@JsonKey(name: "split_number") int? splitNumber,
@JsonKey(name: "split_total") int? splitTotal,
@JsonKey(name: "split_description") String? splitDescription,
@JsonKey(name: "refund_amount") int? refundAmount,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
}) = _PaymentOrderDto;
factory PaymentOrderDto.fromJson(Map<String, dynamic> json) =>
_$PaymentOrderDtoFromJson(json);
// Optional mapper ke domain entity
PaymentOrder toDomain() => PaymentOrder(
id: id ?? '',
orderId: orderId ?? '',
paymentMethodId: paymentMethodId ?? '',
paymentMethodName: paymentMethodName ?? '',
paymentMethodType: paymentMethodType ?? '',
amount: amount ?? 0,
status: status ?? '',
splitNumber: splitNumber ?? 0,
splitTotal: splitTotal ?? 0,
splitDescription: splitDescription ?? '',
refundAmount: refundAmount ?? 0,
metadata: metadata ?? const {},
createdAt: createdAt != null ? DateTime.parse(createdAt!) : DateTime(1970),
updatedAt: updatedAt != null ? DateTime.parse(updatedAt!) : DateTime(1970),
);
}

View File

@ -0,0 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/order/order.dart';
part 'order_dtos.freezed.dart';
part 'order_dtos.g.dart';
part 'dtos/order_dto.dart';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,169 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_dtos.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ListOrderDtoImpl _$$ListOrderDtoImplFromJson(Map<String, dynamic> json) =>
_$ListOrderDtoImpl(
orders: (json['orders'] as List<dynamic>?)
?.map((e) => OrderDto.fromJson(e as Map<String, dynamic>))
.toList(),
totalCount: (json['total_count'] as num?)?.toInt(),
page: (json['page'] as num?)?.toInt(),
limit: (json['limit'] as num?)?.toInt(),
totalPages: (json['total_pages'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ListOrderDtoImplToJson(_$ListOrderDtoImpl instance) =>
<String, dynamic>{
'orders': instance.orders,
'total_count': instance.totalCount,
'page': instance.page,
'limit': instance.limit,
'total_pages': instance.totalPages,
};
_$OrderDtoImpl _$$OrderDtoImplFromJson(Map<String, dynamic> json) =>
_$OrderDtoImpl(
id: json['id'] as String?,
orderNumber: json['order_number'] as String?,
outletId: json['outlet_id'] as String?,
userId: json['user_id'] as String?,
tableNumber: json['table_number'] as String?,
orderType: json['order_type'] as String?,
status: json['status'] as String?,
subtotal: (json['subtotal'] as num?)?.toInt(),
taxAmount: (json['tax_amount'] as num?)?.toInt(),
discountAmount: (json['discount_amount'] as num?)?.toInt(),
totalAmount: (json['total_amount'] as num?)?.toInt(),
totalCost: json['total_cost'] as num?,
remainingAmount: (json['remaining_amount'] as num?)?.toInt(),
paymentStatus: json['payment_status'] as String?,
refundAmount: (json['refund_amount'] as num?)?.toInt(),
isVoid: json['is_void'] as bool?,
isRefund: json['is_refund'] as bool?,
notes: json['notes'] as String?,
metadata: json['metadata'] as Map<String, dynamic>?,
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
orderItems: (json['order_items'] as List<dynamic>?)
?.map((e) => OrderItemDto.fromJson(e as Map<String, dynamic>))
.toList(),
payments: (json['payments'] as List<dynamic>?)
?.map((e) => PaymentOrderDto.fromJson(e as Map<String, dynamic>))
.toList(),
totalPaid: (json['total_paid'] as num?)?.toInt(),
paymentCount: (json['payment_count'] as num?)?.toInt(),
splitType: json['split_type'] as String?,
);
Map<String, dynamic> _$$OrderDtoImplToJson(_$OrderDtoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'order_number': instance.orderNumber,
'outlet_id': instance.outletId,
'user_id': instance.userId,
'table_number': instance.tableNumber,
'order_type': instance.orderType,
'status': instance.status,
'subtotal': instance.subtotal,
'tax_amount': instance.taxAmount,
'discount_amount': instance.discountAmount,
'total_amount': instance.totalAmount,
'total_cost': instance.totalCost,
'remaining_amount': instance.remainingAmount,
'payment_status': instance.paymentStatus,
'refund_amount': instance.refundAmount,
'is_void': instance.isVoid,
'is_refund': instance.isRefund,
'notes': instance.notes,
'metadata': instance.metadata,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
'order_items': instance.orderItems,
'payments': instance.payments,
'total_paid': instance.totalPaid,
'payment_count': instance.paymentCount,
'split_type': instance.splitType,
};
_$OrderItemDtoImpl _$$OrderItemDtoImplFromJson(Map<String, dynamic> json) =>
_$OrderItemDtoImpl(
id: json['id'] as String?,
orderId: json['order_id'] as String?,
productId: json['product_id'] as String?,
productName: json['product_name'] as String?,
productVariantId: json['product_variant_id'] as String?,
productVariantName: json['product_variant_name'] as String?,
quantity: (json['quantity'] as num?)?.toInt(),
unitPrice: (json['unit_price'] as num?)?.toInt(),
totalPrice: (json['total_price'] as num?)?.toInt(),
modifiers: json['modifiers'] as List<dynamic>?,
notes: json['notes'] as String?,
status: json['status'] as String?,
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
printerType: json['printer_type'] as String?,
paidQuantity: (json['paid_quantity'] as num?)?.toInt(),
);
Map<String, dynamic> _$$OrderItemDtoImplToJson(_$OrderItemDtoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'order_id': instance.orderId,
'product_id': instance.productId,
'product_name': instance.productName,
'product_variant_id': instance.productVariantId,
'product_variant_name': instance.productVariantName,
'quantity': instance.quantity,
'unit_price': instance.unitPrice,
'total_price': instance.totalPrice,
'modifiers': instance.modifiers,
'notes': instance.notes,
'status': instance.status,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
'printer_type': instance.printerType,
'paid_quantity': instance.paidQuantity,
};
_$PaymentOrderDtoImpl _$$PaymentOrderDtoImplFromJson(
Map<String, dynamic> json,
) => _$PaymentOrderDtoImpl(
id: json['id'] as String?,
orderId: json['order_id'] as String?,
paymentMethodId: json['payment_method_id'] as String?,
paymentMethodName: json['payment_method_name'] as String?,
paymentMethodType: json['payment_method_type'] as String?,
amount: (json['amount'] as num?)?.toInt(),
status: json['status'] as String?,
splitNumber: (json['split_number'] as num?)?.toInt(),
splitTotal: (json['split_total'] as num?)?.toInt(),
splitDescription: json['split_description'] as String?,
refundAmount: (json['refund_amount'] as num?)?.toInt(),
metadata: json['metadata'] as Map<String, dynamic>?,
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
);
Map<String, dynamic> _$$PaymentOrderDtoImplToJson(
_$PaymentOrderDtoImpl instance,
) => <String, dynamic>{
'id': instance.id,
'order_id': instance.orderId,
'payment_method_id': instance.paymentMethodId,
'payment_method_name': instance.paymentMethodName,
'payment_method_type': instance.paymentMethodType,
'amount': instance.amount,
'status': instance.status,
'split_number': instance.splitNumber,
'split_total': instance.splitTotal,
'split_description': instance.splitDescription,
'refund_amount': instance.refundAmount,
'metadata': instance.metadata,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
};

View File

@ -0,0 +1,46 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/order/order.dart';
import '../datasources/remote_data_provider.dart';
@Injectable(as: IOrderRepository)
class OrderRepository implements IOrderRepository {
final OrderRemoteDataProvider _dataProvider;
final _logName = 'OrderRepository';
OrderRepository(this._dataProvider);
@override
Future<Either<OrderFailure, ListOrder>> getOrders({
int page = 1,
int limit = 10,
String status = 'completed',
required DateTime startDate,
required DateTime endDate,
String? search,
}) async {
try {
final result = await _dataProvider.fetchOrders(
page: page,
limit: limit,
status: status,
startDate: startDate,
endDate: endDate,
search: search,
);
if (result.hasError) {
return left(result.error!);
}
final orders = result.data!.toDomain();
return right(orders);
} catch (e) {
log('getOrdersError', name: _logName, error: e);
return left(const OrderFailure.unexpectedError());
}
}
}

View File

@ -20,6 +20,8 @@ import 'package:apskel_pos_flutter_v2/application/customer/customer_loader/custo
as _i683;
import 'package:apskel_pos_flutter_v2/application/order/order_form/order_form_bloc.dart'
as _i702;
import 'package:apskel_pos_flutter_v2/application/order/order_loader/order_loader_bloc.dart'
as _i94;
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
as _i76;
import 'package:apskel_pos_flutter_v2/application/payment_method/payment_method_loader/payment_method_loader_bloc.dart'
@ -45,6 +47,7 @@ import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
import 'package:apskel_pos_flutter_v2/domain/customer/customer.dart' as _i143;
import 'package:apskel_pos_flutter_v2/domain/order/order.dart' as _i299;
import 'package:apskel_pos_flutter_v2/domain/outlet/outlet.dart' as _i552;
import 'package:apskel_pos_flutter_v2/domain/payment_method/payment_method.dart'
as _i297;
@ -67,6 +70,10 @@ import 'package:apskel_pos_flutter_v2/infrastructure/customer/datasources/remote
as _i841;
import 'package:apskel_pos_flutter_v2/infrastructure/customer/repositories/customer_repository.dart'
as _i385;
import 'package:apskel_pos_flutter_v2/infrastructure/order/datasources/remote_data_provider.dart'
as _i360;
import 'package:apskel_pos_flutter_v2/infrastructure/order/repositories/order_repository.dart'
as _i851;
import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/local_data_provider.dart'
as _i693;
import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/remote_data_provider.dart'
@ -161,6 +168,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i833.PaymentMethodRemoteDataProvider>(
() => _i833.PaymentMethodRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i360.OrderRemoteDataProvider>(
() => _i360.OrderRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i776.IAuthRepository>(
() => _i941.AuthRepository(
gh<_i370.AuthRemoteDataProvider>(),
@ -179,6 +189,9 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i693.OutletLocalDatasource>(),
),
);
gh.factory<_i299.IOrderRepository>(
() => _i851.OrderRepository(gh<_i360.OrderRemoteDataProvider>()),
);
gh.factory<_i248.TableFormBloc>(
() => _i248.TableFormBloc(gh<_i983.ITableRepository>()),
);
@ -211,6 +224,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i143.ICustomerRepository>(
() => _i385.CustomerRepository(gh<_i841.CustomerRemoteDataProvider>()),
);
gh.factory<_i94.OrderLoaderBloc>(
() => _i94.OrderLoaderBloc(gh<_i299.IOrderRepository>()),
);
gh.factory<_i683.CustomerLoaderBloc>(
() => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()),
);

View File

@ -6,6 +6,7 @@ import '../application/category/category_loader/category_loader_bloc.dart';
import '../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../application/customer/customer_loader/customer_loader_bloc.dart';
import '../application/order/order_form/order_form_bloc.dart';
import '../application/order/order_loader/order_loader_bloc.dart';
import '../application/outlet/outlet_loader/outlet_loader_bloc.dart';
import '../application/payment_method/payment_method_loader/payment_method_loader_bloc.dart';
import '../application/product/product_loader/product_loader_bloc.dart';
@ -40,6 +41,7 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<TableFormBloc>()),
BlocProvider(create: (context) => getIt<PaymentMethodLoaderBloc>()),
BlocProvider(create: (context) => getIt<OrderFormBloc>()),
BlocProvider(create: (context) => getIt<OrderLoaderBloc>()),
BlocProvider(create: (context) => getIt<CustomerLoaderBloc>()),
],
child: MaterialApp.router(

View File

@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/order/order.dart';
class OrderCard extends StatelessWidget {
final Order order;
final bool isActive;
const OrderCard({super.key, required this.order, required this.isActive});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: isActive ? AppColor.primary.withOpacity(0.1) : AppColor.white,
border: Border.all(
color: isActive ? AppColor.primary : AppColor.border,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
order.orderNumber,
style: AppStyle.sm.copyWith(fontWeight: FontWeight.w600),
),
),
if (order.isRefund == true)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Refund',
style: AppStyle.xs.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
fontSize: 10,
letterSpacing: 0.5,
),
),
),
if (order.isVoid == true)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Void',
style: AppStyle.xs.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
fontSize: 10,
letterSpacing: 0.5,
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
CircleAvatar(
radius: 22,
backgroundColor: AppColor.primary,
child: Icon(Icons.person, color: Colors.white),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
order.metadata['customer_name'] == ""
? "Anonim"
: order.metadata['customer_name'],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
if (order.orderType == "dineIn") ...[
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.table_bar,
size: 16,
color: AppColor.textSecondary,
),
const SizedBox(width: 4),
Text(
'Meja ${order.tableNumber}',
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
],
),
),
_buildStatus(),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
order.status == 'pending'
? ((order.totalAmount) - (order.totalPaid))
.currencyFormatRpV2
: (order.totalAmount).currencyFormatRpV2,
style: AppStyle.xl.copyWith(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
Text(
(order.createdAt).toFormattedDateTime(),
style: TextStyle(color: AppColor.black),
),
],
),
],
),
),
);
}
Widget _buildStatus() {
switch (order.status) {
case 'pending':
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.warning.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Text(
(order.status).toUpperCase(),
style: AppStyle.sm.copyWith(
color: AppColor.warning,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
);
case 'completed':
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Text(
(order.status).toUpperCase(),
style: TextStyle(
color: AppColor.success,
fontWeight: FontWeight.w600,
fontSize: 12,
letterSpacing: 0.5,
),
),
);
default:
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.textSecondary.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Text(
(order.status).toUpperCase(),
style: TextStyle(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
fontSize: 12,
letterSpacing: 0.5,
),
),
);
}
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../domain/order/order.dart';
import '../card/error_card.dart';
class OrderLoaderErrorStateWidget extends StatelessWidget {
final OrderFailure failure;
final String status;
const OrderLoaderErrorStateWidget({
super.key,
required this.failure,
required this.status,
});
@override
Widget build(BuildContext context) {
return failure.maybeMap(
orElse: () => ErrorCard(
title: 'Pesanan',
message: 'Terjadi kesalahan saat memuat pesanan',
onTap: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
dynamicErrorMessage: (value) => ErrorCard(
title: 'Pesanan',
message: value.erroMessage,
onTap: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
empty: (value) => ErrorCard(
title: 'Pesanan',
message: 'Data Pesanan Kosong',
onTap: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
serverError: (value) => ErrorCard(
title: 'Pesanan',
message: 'Terjadi kesalahan saat memuat pesanan',
onTap: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
unexpectedError: (value) => ErrorCard(
title: 'Pesanan',
message: 'Terjadi kesalahan saat memuat pesanan',
onTap: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
);
}
}

View File

@ -52,6 +52,7 @@ class AppTextFormField extends StatelessWidget {
maxLines: maxLines,
validator: validator,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 12),
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
hintText: label,

View File

@ -10,29 +10,40 @@ class PageTitle extends StatelessWidget {
final String? subtitle;
final bool isBack;
final List<Widget>? actionWidget;
final Widget? bottom;
const PageTitle({
super.key,
required this.title,
this.subtitle,
this.isBack = true,
this.actionWidget,
this.bottom,
});
@override
Widget build(BuildContext context) {
return Container(
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: context.deviceHeight * 0.123,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: AppColor.white,
border: Border(bottom: BorderSide(color: AppColor.border, width: 1.0)),
border: Border(
bottom: BorderSide(color: AppColor.border, width: 1.0),
),
),
child: Row(
children: [
if (isBack) ...[
GestureDetector(
onTap: () => context.router.maybePop(),
child: Icon(Icons.arrow_back, color: AppColor.primary, size: 24),
child: Icon(
Icons.arrow_back,
color: AppColor.primary,
size: 24,
),
),
SpaceWidth(16),
],
@ -49,7 +60,9 @@ class PageTitle extends StatelessWidget {
const SizedBox(height: 4.0),
Text(
subtitle!,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
],
],
@ -58,6 +71,9 @@ class PageTitle extends StatelessWidget {
if (actionWidget != null) ...actionWidget!,
],
),
),
bottom ?? const SizedBox.shrink(),
],
);
}
}

View File

@ -0,0 +1,411 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datepicker/datepicker.dart';
import '../../../common/theme/theme.dart';
class DateRangePickerModal {
static Future<DateRangePickerSelectionChangedArgs?> show({
required BuildContext context,
String title = 'Pilih Rentang Tanggal',
DateTime? initialStartDate,
DateTime? initialEndDate,
DateTime? minDate,
DateTime? maxDate,
String confirmText = 'Pilih',
String cancelText = 'Batal',
Color primaryColor = AppColor.primary,
Function(DateTime? startDate, DateTime? endDate)? onChanged,
}) async {
return await showDialog<DateRangePickerSelectionChangedArgs?>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => _DateRangePickerDialog(
title: title,
initialStartDate: initialStartDate,
initialEndDate: initialEndDate,
minDate: minDate,
maxDate: maxDate,
confirmText: confirmText,
cancelText: cancelText,
primaryColor: primaryColor,
onChanged: onChanged,
),
);
}
}
class _DateRangePickerDialog extends StatefulWidget {
final String title;
final DateTime? initialStartDate;
final DateTime? initialEndDate;
final DateTime? minDate;
final DateTime? maxDate;
final String confirmText;
final String cancelText;
final Color primaryColor;
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
const _DateRangePickerDialog({
required this.title,
this.initialStartDate,
this.initialEndDate,
this.minDate,
this.maxDate,
required this.confirmText,
required this.cancelText,
required this.primaryColor,
this.onChanged,
});
@override
State<_DateRangePickerDialog> createState() => _DateRangePickerDialogState();
}
class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
with TickerProviderStateMixin {
DateRangePickerSelectionChangedArgs? _selectionChangedArgs;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) {
setState(() {
_selectionChangedArgs = args;
});
// Note: onChanged callback is now called only when confirm button is pressed
// This allows users to see real-time selection without triggering callbacks
}
String _getSelectionText() {
if (_selectionChangedArgs?.value is PickerDateRange) {
final PickerDateRange range = _selectionChangedArgs!.value;
if (range.startDate != null && range.endDate != null) {
return '${_formatDate(range.startDate!)} - ${_formatDate(range.endDate!)}';
} else if (range.startDate != null) {
return _formatDate(range.startDate!);
}
}
return 'Belum ada tanggal dipilih';
}
String _formatDate(DateTime date) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
bool get _isValidSelection {
if (_selectionChangedArgs?.value is PickerDateRange) {
final PickerDateRange range = _selectionChangedArgs!.value;
return range.startDate != null && range.endDate != null;
}
return false;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
insetPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 24,
),
child: Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxWidth: 400,
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
widget.primaryColor,
widget.primaryColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
Icon(
Icons.calendar_today_rounded,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.title,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
// Scrollable Content
Flexible(
child: SingleChildScrollView(
child: Column(
children: [
// Selection Info
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: widget.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tanggal Terpilih:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: widget.primaryColor,
),
),
const SizedBox(height: 4),
Text(
_getSelectionText(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
),
// Date Picker
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Container(
height: 320,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: SfDateRangePicker(
onSelectionChanged: _onSelectionChanged,
selectionMode:
DateRangePickerSelectionMode.range,
initialSelectedRange:
(widget.initialStartDate != null &&
widget.initialEndDate != null)
? PickerDateRange(
widget.initialStartDate,
widget.initialEndDate,
)
: null,
minDate: widget.minDate,
maxDate: widget.maxDate,
startRangeSelectionColor: widget.primaryColor,
endRangeSelectionColor: widget.primaryColor,
rangeSelectionColor: widget.primaryColor
.withOpacity(0.2),
todayHighlightColor: widget.primaryColor,
headerStyle: DateRangePickerHeaderStyle(
backgroundColor: Colors.transparent,
textAlign: TextAlign.center,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
monthViewSettings:
DateRangePickerMonthViewSettings(
viewHeaderStyle:
DateRangePickerViewHeaderStyle(
backgroundColor: Colors.grey
.withOpacity(0.1),
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: widget.primaryColor,
),
),
),
selectionTextStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
rangeTextStyle: TextStyle(
color: widget.primaryColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
),
),
],
),
),
),
// Action Buttons
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
),
side: BorderSide(color: Colors.grey.shade400),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
widget.cancelText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isValidSelection
? () {
// Call onChanged when confirm button is pressed
if (widget.onChanged != null &&
_selectionChangedArgs?.value
is PickerDateRange) {
final PickerDateRange range =
_selectionChangedArgs!.value;
widget.onChanged!(
range.startDate,
range.endDate,
);
}
Navigator.of(
context,
).pop(_selectionChangedArgs);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: widget.primaryColor,
padding: const EdgeInsets.symmetric(
vertical: 14,
),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: Colors.grey.shade300,
),
child: Text(
widget.confirmText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _isValidSelection
? Colors.white
: Colors.grey.shade600,
),
),
),
),
],
),
),
],
),
),
),
),
);
},
);
}
}

View File

@ -30,7 +30,11 @@ class HomeRightTitle extends StatelessWidget {
children: [
Expanded(
child: Row(
children: [_buildButton('Daftar Pesanan', Icons.list, () {})],
children: [
_buildButton('Daftar Pesanan', Icons.list, () {
context.router.push(OrderRoute(status: 'pending'));
}),
],
),
),
Expanded(

View File

@ -0,0 +1,71 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../../injection.dart';
import 'widgets/order_left_panel.dart';
import 'widgets/order_right_panel.dart';
@RoutePage()
class OrderPage extends StatelessWidget implements AutoRouteWrapper {
final String status;
const OrderPage({super.key, required this.status});
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
BlocListener<OrderLoaderBloc, OrderLoaderState>(
listenWhen: (previous, current) =>
previous.startDate != current.startDate ||
previous.endDate != current.endDate,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
BlocListener<OrderLoaderBloc, OrderLoaderState>(
listenWhen: (previous, current) => previous.search != current.search,
listener: (context, state) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: status, isRefresh: true),
);
},
),
],
child: SafeArea(
child: Scaffold(
backgroundColor: AppColor.background,
body: BlocBuilder<OrderLoaderBloc, OrderLoaderState>(
builder: (context, state) {
return Row(
children: [
Expanded(
flex: 2,
child: Material(
color: AppColor.white,
child: OrderLeftPanel(state: state, status: status),
),
),
Expanded(flex: 4, child: OrderRightPanel(state: state)),
],
);
},
),
),
),
);
}
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<OrderLoaderBloc>()
..add(OrderLoaderEvent.fetched(status: status, isRefresh: true)),
child: this,
);
}

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/order/order.dart';
import '../../../components/spaces/space.dart';
class OrderInformation extends StatelessWidget {
final Order? order;
const OrderInformation({super.key, this.order});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.primary,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
order?.orderNumber ?? "",
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
_buildStatus(),
],
),
const SizedBox(height: 8),
Row(
children: [
if (order?.orderType == 'dineIn') ...[
_buildRowItem(
Icons.table_restaurant_outlined,
'Meja ${order?.tableNumber}',
),
const SizedBox(width: 16),
],
_buildRowItem(Icons.restaurant_outlined, '${order?.orderType}'),
],
),
const SizedBox(height: 8),
Text(
'Pelanggan: ${order?.metadata['customer_name'] ?? ""}',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
Text(
'Dibuat: ${order?.createdAt.toFormattedDateTime() ?? ""}',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
);
}
Row _buildRowItem(IconData icon, String title) {
return Row(
children: [
Icon(icon, color: Colors.white70, size: 16),
const SpaceWidth(4),
Text(
title,
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
],
);
}
Container _buildStatus() {
switch (order?.status) {
case 'pending':
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(12),
),
child: Text(
(order?.status ?? "").toUpperCase(),
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
case 'completed':
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.greenAccent,
borderRadius: BorderRadius.circular(12),
),
child: Text(
(order?.status ?? "").toUpperCase(),
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
default:
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(12),
),
child: Text(
(order?.status ?? "").toUpperCase(),
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
}
}
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/card/order_card.dart';
import '../../../components/error/order_loader_error_state_widget.dart';
import '../../../components/loader/loader_with_text.dart';
import 'order_title.dart';
class OrderLeftPanel extends StatefulWidget {
final String status;
final OrderLoaderState state;
const OrderLeftPanel({super.key, required this.state, required this.status});
@override
State<OrderLeftPanel> createState() => _OrderLeftPanelState();
}
class _OrderLeftPanelState extends State<OrderLeftPanel> {
ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollEndNotification &&
scrollController.position.extentAfter == 0) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.fetched(status: widget.status),
);
return true;
}
return true;
},
child: Column(
children: [
OrderTitle(
startDate: widget.state.startDate,
endDate: widget.state.endDate,
title: widget.status == 'pending'
? "Pending Pesanan"
: "Daftar Pesanan",
onChanged: (value) {
Future.delayed(const Duration(milliseconds: 800), () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.searchChange(value),
);
});
},
onDateRangeChanged: (start, end) {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.dateTimeRangeChange(start!, end!),
);
},
),
Expanded(
child: widget.state.failureOption.fold(
() {
if (widget.state.isFetching) {
return Center(child: LoaderWithText());
}
if (widget.state.orders.isEmpty) {
return Center(
child: Text(
"Belum ada transaksi saat ini. ",
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
);
}
return ListView.builder(
itemCount: widget.state.orders.length,
controller: scrollController,
itemBuilder: (context, index) => GestureDetector(
onTap: () {
context.read<OrderLoaderBloc>().add(
OrderLoaderEvent.setSelectedOrder(
widget.state.orders[index],
),
);
},
child: OrderCard(
order: widget.state.orders[index],
isActive:
widget.state.orders[index] ==
widget.state.selectedOrder,
),
),
);
},
(f) => OrderLoaderErrorStateWidget(
failure: f,
status: widget.status,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,357 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/order/order.dart';
import '../../../components/spaces/space.dart';
class OrderList extends StatelessWidget {
final Order? order;
const OrderList({super.key, this.order});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SpaceHeight(8),
_buildItemsList(context),
const SpaceHeight(8),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.primary.withOpacity(0.1),
AppColor.primary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
border: Border(
bottom: BorderSide(
color: AppColor.primary.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Daftar Pembelian',
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
const SizedBox(height: 4),
Text(
'${order?.orderItems.length ?? 0} item',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.primary.withOpacity(0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.shopping_cart_outlined,
size: 16,
color: AppColor.primary,
),
const SizedBox(width: 4),
Text(
'Order',
style: AppStyle.sm.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
Widget _buildItemsList(BuildContext context) {
return Column(
children: List.generate(order?.orderItems.length ?? 0, (index) {
final item = order!.orderItems[index];
return _buildItem(context, item, index);
}).toList(),
);
}
Widget _buildItem(BuildContext context, OrderItem product, int index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200, width: 1),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
product.productName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
),
),
),
_buildStatusBadge(product.status),
],
),
if (product.productVariantName != '') ...[
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Text(
product.productVariantName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
),
],
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Harga Satuan',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
(product.unitPrice)
.toString()
.currencyFormatRpV2,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'x${product.quantity}',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Total',
style: AppStyle.sm.copyWith(
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
(product.totalPrice)
.toString()
.currencyFormatRpV2,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.primary,
),
),
],
),
],
),
],
),
),
],
),
if (order?.splitType == 'ITEM' && order?.status == 'pending') ...[
SpaceHeight(6),
Align(
alignment: Alignment.centerRight,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.primary),
),
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '${product.paidQuantity} ',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
TextSpan(
text: 'dari ',
style: AppStyle.sm.copyWith(color: AppColor.primary),
),
TextSpan(
text: '${product.quantity} ',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
TextSpan(
text: 'kuantiti telah dibayar.',
style: AppStyle.sm.copyWith(color: AppColor.primary),
),
],
),
),
),
),
],
],
),
),
);
}
Widget _buildStatusBadge(String? status) {
Color backgroundColor;
Color textColor;
String displayText;
IconData icon;
switch (status) {
case "pending":
backgroundColor = Colors.white;
textColor = Colors.white;
displayText = "Pending";
icon = Icons.access_time;
break;
case "cancelled":
backgroundColor = Colors.red.withOpacity(0.1);
textColor = Colors.red.shade700;
displayText = "Batal";
icon = Icons.cancel_outlined;
break;
case "refund":
backgroundColor = Colors.purple.withOpacity(0.1);
textColor = Colors.purple.shade700;
displayText = "Refund";
icon = Icons.undo;
break;
default:
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green.shade700;
displayText = "Selesai";
icon = Icons.check_circle_outline;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: textColor.withOpacity(0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: textColor),
const SizedBox(width: 4),
Text(
displayText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: textColor,
),
),
],
),
);
}
}

View File

@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/order/order.dart';
import '../../../components/spaces/space.dart';
class OrderListPayment extends StatelessWidget {
final Order? order;
const OrderListPayment({super.key, this.order});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Informasi Pembayaran',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
_buildPaymentStatus(),
],
),
const SpaceHeight(12),
...List.generate(
order?.payments.length ?? 0,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: _buildPaymentItem(
order?.payments[index] ?? PaymentOrder.empty(),
),
),
),
const SpaceHeight(4),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Jumlah yang Dibayar',
style: AppStyle.md.copyWith(color: Colors.grey.shade700),
),
Text(
(order?.totalPaid ?? 0).currencyFormatRpV2,
style: TextStyle(fontWeight: FontWeight.w500),
),
],
),
if (((order?.totalAmount ?? 0) - (order?.totalPaid ?? 0)) != 0) ...[
const SpaceHeight(4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sisa Tagihan',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w500,
),
),
Text(
((order?.totalAmount ?? 0) - (order?.totalPaid ?? 0))
.currencyFormatRpV2,
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
],
],
),
);
}
Container _buildPaymentStatus() {
switch (order?.paymentStatus) {
case 'completed':
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
(order?.paymentStatus ?? "").toTitleCase(),
style: AppStyle.xs.copyWith(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColor.success,
),
),
);
default:
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
(order?.paymentStatus ?? "").toTitleCase(),
style: AppStyle.xs.copyWith(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.amber.shade800,
),
),
);
}
}
Row _buildPaymentItem(PaymentOrder payment) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Icon(Icons.payments, color: Colors.green.shade700, size: 16),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
payment.paymentMethodName,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w500),
),
if ((payment.splitTotal) > 1)
Text(
'Split ${payment.splitNumber} of ${payment.splitTotal}',
style: AppStyle.md.copyWith(color: Colors.grey.shade600),
),
],
),
),
Text(
(payment.amount).currencyFormatRpV2,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.success,
),
),
],
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/order/order.dart';
import '../../../components/border/dashed_border.dart';
import '../../../components/spaces/space.dart';
class OrderPaymentSummary extends StatelessWidget {
final Order? order;
const OrderPaymentSummary({super.key, this.order});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Pembayaran',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
const SpaceHeight(12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Subtotal',
style: AppStyle.md.copyWith(color: Colors.grey.shade700),
),
Text((order?.subtotal ?? 0).currencyFormatRpV2),
],
),
const SpaceHeight(4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Tax',
style: AppStyle.md.copyWith(color: Colors.grey.shade700),
),
Text((order?.taxAmount ?? 0).currencyFormatRpV2),
],
),
const SpaceHeight(4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Discount',
style: AppStyle.md.copyWith(color: Colors.grey.shade700),
),
Text((order?.discountAmount ?? 0).currencyFormatRpV2),
],
),
const SpaceHeight(8),
const DashedDivider(color: AppColor.border),
const SpaceHeight(8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
(order?.totalAmount ?? 0).currencyFormatRpV2,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import '../../../../application/order/order_loader/order_loader_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/button/button.dart';
import '../../../components/spaces/space.dart';
import 'order_information.dart';
import 'order_list.dart';
import 'order_list_payment.dart';
import 'order_payment_summary.dart';
class OrderRightPanel extends StatelessWidget {
final OrderLoaderState state;
const OrderRightPanel({super.key, required this.state});
@override
Widget build(BuildContext context) {
if (state.selectedOrder == null) {
return Center(
child: Text(
"Belum ada order yang dipilih.",
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
);
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OrderInformation(order: state.selectedOrder),
OrderList(order: state.selectedOrder),
const SpaceHeight(16),
OrderPaymentSummary(order: state.selectedOrder),
const SpaceHeight(16),
if (state.selectedOrder?.payments != null &&
state.selectedOrder?.payments.isNotEmpty == true) ...[
OrderListPayment(order: state.selectedOrder),
const SpaceHeight(20),
],
],
),
),
),
),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(color: AppColor.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AppElevatedButton.outlined(
onPressed: () {
if (state.selectedOrder?.status == 'completed') {
// onPrintRecipt(
// context,
// order: orderDetail!,
// paymentMethod:
// orderDetail!.payments
// ?.map((p) => p.paymentMethodName)
// .join(', ') ??
// "",
// nominalBayar: orderDetail?.totalPaid ?? 0,
// kembalian: 0,
// productQuantity: orderDetail!.orderItems!
// .toProductQuantities(),
// );
} else {
// onPrintBill(
// context,
// productQuantity: orderDetail!.orderItems!
// .toProductQuantities(),
// order: orderDetail!,
// );
}
},
label: 'Print Bill',
icon: Icon(Icons.print),
),
SpaceWidth(8),
if (state.selectedOrder?.status == 'pending') ...[
AppElevatedButton.outlined(
onPressed: () {},
label: 'Void',
icon: Icon(Icons.undo),
),
SpaceWidth(8),
AppElevatedButton.outlined(
onPressed: () {
// context.push(SplitBillPage(order: orderDetail!));
},
label: 'Split Bill',
icon: Icon(Icons.calculate_outlined),
),
SpaceWidth(8),
AppElevatedButton.filled(
width: 120,
onPressed: () {},
label: 'Bayar',
icon: Icon(Icons.payment, color: Colors.white),
),
],
],
),
),
],
);
}
}

View File

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/field/field.dart';
import '../../../components/page/page_title.dart';
import '../../../components/picker/date_range_picker.dart';
import '../../../components/spaces/space.dart';
class OrderTitle extends StatelessWidget {
final String title;
final DateTime startDate;
final DateTime endDate;
final Function(String) onChanged;
final void Function(DateTime? start, DateTime? end) onDateRangeChanged;
const OrderTitle({
super.key,
required this.title,
required this.startDate,
required this.endDate,
required this.onChanged,
required this.onDateRangeChanged,
});
@override
Widget build(BuildContext context) {
return PageTitle(
title: title,
bottom: Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: AppColor.white,
border: Border(
bottom: BorderSide(color: AppColor.border, width: 1.0),
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
startDate.toFormattedDate() == endDate.toFormattedDate()
? startDate.toFormattedDate()
: '${startDate.toFormattedDate()} - ${endDate.toFormattedDate()}',
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
Text(
'0 Pesanan',
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
],
),
SpaceHeight(16),
Row(
children: [
Expanded(
child: AppTextFormField(
onChanged: onChanged,
label: 'Cari Pesanan',
showLabel: false,
),
),
SpaceWidth(12),
GestureDetector(
onTap: () => DateRangePickerModal.show(
context: context,
initialStartDate: startDate,
initialEndDate: endDate,
primaryColor: AppColor.primary,
onChanged: onDateRangeChanged,
),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.primary,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.filter_list_outlined,
color: AppColor.white,
size: 24,
),
),
),
],
),
],
),
),
);
}
}

View File

@ -28,5 +28,8 @@ class AppRouter extends RootStackRouter {
// Checkout
AutoRoute(page: CheckoutRoute.page),
// Order
AutoRoute(page: OrderRoute.page),
];
}

View File

@ -20,90 +20,93 @@ import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/customer/cus
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/home/home_page.dart'
as _i3;
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/report/report_page.dart'
as _i6;
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/setting/setting_page.dart'
as _i7;
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/table/table_page.dart'
as _i10;
import 'package:apskel_pos_flutter_v2/presentation/pages/splash/splash_page.dart'
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/setting/setting_page.dart'
as _i8;
import 'package:apskel_pos_flutter_v2/presentation/pages/sync/sync_page.dart'
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/table/table_page.dart'
as _i11;
import 'package:apskel_pos_flutter_v2/presentation/pages/order/order_page.dart'
as _i6;
import 'package:apskel_pos_flutter_v2/presentation/pages/splash/splash_page.dart'
as _i9;
import 'package:auto_route/auto_route.dart' as _i11;
import 'package:apskel_pos_flutter_v2/presentation/pages/sync/sync_page.dart'
as _i10;
import 'package:auto_route/auto_route.dart' as _i12;
import 'package:flutter/widgets.dart' as _i13;
/// generated route for
/// [_i1.CheckoutPage]
class CheckoutRoute extends _i11.PageRouteInfo<void> {
const CheckoutRoute({List<_i11.PageRouteInfo>? children})
class CheckoutRoute extends _i12.PageRouteInfo<void> {
const CheckoutRoute({List<_i12.PageRouteInfo>? children})
: super(CheckoutRoute.name, initialChildren: children);
static const String name = 'CheckoutRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return _i11.WrappedRoute(child: const _i1.CheckoutPage());
return _i12.WrappedRoute(child: const _i1.CheckoutPage());
},
);
}
/// generated route for
/// [_i2.CustomerPage]
class CustomerRoute extends _i11.PageRouteInfo<void> {
const CustomerRoute({List<_i11.PageRouteInfo>? children})
class CustomerRoute extends _i12.PageRouteInfo<void> {
const CustomerRoute({List<_i12.PageRouteInfo>? children})
: super(CustomerRoute.name, initialChildren: children);
static const String name = 'CustomerRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return _i11.WrappedRoute(child: const _i2.CustomerPage());
return _i12.WrappedRoute(child: const _i2.CustomerPage());
},
);
}
/// generated route for
/// [_i3.HomePage]
class HomeRoute extends _i11.PageRouteInfo<void> {
const HomeRoute({List<_i11.PageRouteInfo>? children})
class HomeRoute extends _i12.PageRouteInfo<void> {
const HomeRoute({List<_i12.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return _i11.WrappedRoute(child: const _i3.HomePage());
return _i12.WrappedRoute(child: const _i3.HomePage());
},
);
}
/// generated route for
/// [_i4.LoginPage]
class LoginRoute extends _i11.PageRouteInfo<void> {
const LoginRoute({List<_i11.PageRouteInfo>? children})
class LoginRoute extends _i12.PageRouteInfo<void> {
const LoginRoute({List<_i12.PageRouteInfo>? children})
: super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return _i11.WrappedRoute(child: const _i4.LoginPage());
return _i12.WrappedRoute(child: const _i4.LoginPage());
},
);
}
/// generated route for
/// [_i5.MainPage]
class MainRoute extends _i11.PageRouteInfo<void> {
const MainRoute({List<_i11.PageRouteInfo>? children})
class MainRoute extends _i12.PageRouteInfo<void> {
const MainRoute({List<_i12.PageRouteInfo>? children})
: super(MainRoute.name, initialChildren: children);
static const String name = 'MainRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i5.MainPage();
@ -112,81 +115,118 @@ class MainRoute extends _i11.PageRouteInfo<void> {
}
/// generated route for
/// [_i6.ReportPage]
class ReportRoute extends _i11.PageRouteInfo<void> {
const ReportRoute({List<_i11.PageRouteInfo>? children})
/// [_i6.OrderPage]
class OrderRoute extends _i12.PageRouteInfo<OrderRouteArgs> {
OrderRoute({
_i13.Key? key,
required String status,
List<_i12.PageRouteInfo>? children,
}) : super(
OrderRoute.name,
args: OrderRouteArgs(key: key, status: status),
initialChildren: children,
);
static const String name = 'OrderRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
final args = data.argsAs<OrderRouteArgs>();
return _i6.OrderPage(key: args.key, status: args.status);
},
);
}
class OrderRouteArgs {
const OrderRouteArgs({this.key, required this.status});
final _i13.Key? key;
final String status;
@override
String toString() {
return 'OrderRouteArgs{key: $key, status: $status}';
}
}
/// generated route for
/// [_i7.ReportPage]
class ReportRoute extends _i12.PageRouteInfo<void> {
const ReportRoute({List<_i12.PageRouteInfo>? children})
: super(ReportRoute.name, initialChildren: children);
static const String name = 'ReportRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i6.ReportPage();
return const _i7.ReportPage();
},
);
}
/// generated route for
/// [_i7.SettingPage]
class SettingRoute extends _i11.PageRouteInfo<void> {
const SettingRoute({List<_i11.PageRouteInfo>? children})
/// [_i8.SettingPage]
class SettingRoute extends _i12.PageRouteInfo<void> {
const SettingRoute({List<_i12.PageRouteInfo>? children})
: super(SettingRoute.name, initialChildren: children);
static const String name = 'SettingRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i7.SettingPage();
return const _i8.SettingPage();
},
);
}
/// generated route for
/// [_i8.SplashPage]
class SplashRoute extends _i11.PageRouteInfo<void> {
const SplashRoute({List<_i11.PageRouteInfo>? children})
/// [_i9.SplashPage]
class SplashRoute extends _i12.PageRouteInfo<void> {
const SplashRoute({List<_i12.PageRouteInfo>? children})
: super(SplashRoute.name, initialChildren: children);
static const String name = 'SplashRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i8.SplashPage();
return const _i9.SplashPage();
},
);
}
/// generated route for
/// [_i9.SyncPage]
class SyncRoute extends _i11.PageRouteInfo<void> {
const SyncRoute({List<_i11.PageRouteInfo>? children})
/// [_i10.SyncPage]
class SyncRoute extends _i12.PageRouteInfo<void> {
const SyncRoute({List<_i12.PageRouteInfo>? children})
: super(SyncRoute.name, initialChildren: children);
static const String name = 'SyncRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return _i11.WrappedRoute(child: const _i9.SyncPage());
return _i12.WrappedRoute(child: const _i10.SyncPage());
},
);
}
/// generated route for
/// [_i10.TablePage]
class TableRoute extends _i11.PageRouteInfo<void> {
const TableRoute({List<_i11.PageRouteInfo>? children})
/// [_i11.TablePage]
class TableRoute extends _i12.PageRouteInfo<void> {
const TableRoute({List<_i12.PageRouteInfo>? children})
: super(TableRoute.name, initialChildren: children);
static const String name = 'TableRoute';
static _i11.PageInfo page = _i11.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return _i11.WrappedRoute(child: const _i10.TablePage());
return _i12.WrappedRoute(child: const _i11.TablePage());
},
);
}

View File

@ -1093,6 +1093,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: d03c43f577cdbe020d1632bece00cbf8bec4a7d0ab123923b69141b5fec35420
url: "https://pub.dev"
source: hosted
version: "31.2.3"
syncfusion_flutter_datepicker:
dependency: "direct main"
description:
name: syncfusion_flutter_datepicker
sha256: f6277bd71a6d04785d7359c8caf373acae07132f1cc453b835173261dbd5ddb6
url: "https://pub.dev"
source: hosted
version: "31.2.3"
synchronized:
dependency: transitive
description:
@ -1247,4 +1263,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
flutter: ">=3.35.1"

View File

@ -37,6 +37,7 @@ dependencies:
cached_network_image: ^3.4.1
shimmer: ^3.0.0
dropdown_search: ^5.0.6
syncfusion_flutter_datepicker: ^31.2.3
dev_dependencies:
flutter_test: