From 7678b4791db2422f781bd8f44b5a8966c290a099 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sun, 3 Aug 2025 00:35:00 +0700 Subject: [PATCH] feat: syn product --- lib/data/dataoutputs/print_dataoutputs.dart | 102 ++- .../datasources/product_local_datasource.dart | 4 +- .../product_remote_datasource.dart | 45 +- .../models/request/product_request_model.dart | 6 +- .../response/product_response_model.dart | 317 ++++--- lib/main.dart | 4 + .../add_order_items/add_order_items_bloc.dart | 23 +- .../home/bloc/order/order_bloc.dart | 4 +- .../product_loader/product_loader_bloc.dart | 23 + .../product_loader_bloc.freezed.dart | 790 +++++++++++++++++ .../product_loader/product_loader_event.dart | 6 + .../product_loader/product_loader_state.dart | 9 + .../home/models/product_quantity.dart | 6 +- .../home/pages/confirm_payment_page-old.dart | 24 +- .../home/pages/confirm_payment_page.dart | 9 +- lib/presentation/home/pages/home_page.dart | 793 +++++++++--------- lib/presentation/home/widgets/order_menu.dart | 28 +- .../home/widgets/product_card.dart | 13 +- .../sales/widgets/sales_list_order.dart | 4 +- .../bloc/add_product/add_product_bloc.dart | 8 +- .../bloc/get_products/get_products_bloc.dart | 3 +- .../update_product/update_product_bloc.dart | 43 +- .../dialogs/detail_product_dialog.dart | 32 +- .../setting/dialogs/form_product_dialog.dart | 308 +++---- .../setting/pages/sync_data_page.dart | 2 +- .../setting/widgets/menu_product_item.dart | 18 +- .../table/models/draft_order_item.dart | 2 +- .../table/pages/payment_table_page.dart | 244 ++++-- 28 files changed, 1922 insertions(+), 948 deletions(-) create mode 100644 lib/presentation/home/bloc/product_loader/product_loader_bloc.dart create mode 100644 lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart create mode 100644 lib/presentation/home/bloc/product_loader/product_loader_event.dart create mode 100644 lib/presentation/home/bloc/product_loader/product_loader_state.dart diff --git a/lib/data/dataoutputs/print_dataoutputs.dart b/lib/data/dataoutputs/print_dataoutputs.dart index 3c68bce..ad4be8d 100644 --- a/lib/data/dataoutputs/print_dataoutputs.dart +++ b/lib/data/dataoutputs/print_dataoutputs.dart @@ -60,12 +60,12 @@ class PrintDataoutputs { bytes += generator.row([ PosColumn( text: - '${product.product.price!.toIntegerFromText.currencyFormatRp} x ${product.quantity}', + '${product.product.price!.currencyFormatRp} x ${product.quantity}', width: 8, styles: const PosStyles(align: PosAlign.left), ), PosColumn( - text: '${product.product.price!.toIntegerFromText * product.quantity}' + text: '${product.product.price! * product.quantity}' .toIntegerFromText .currencyFormatRp, width: 4, @@ -328,8 +328,7 @@ class PrintDataoutputs { styles: const PosStyles(align: PosAlign.left), ), PosColumn( - text: (product.product.price!.toIntegerFromText * product.quantity) - .currencyFormatRp, + text: (product.product.price! * product.quantity).currencyFormatRp, width: 4, styles: const PosStyles(align: PosAlign.right), ), @@ -398,8 +397,7 @@ class PrintDataoutputs { styles: const PosStyles(align: PosAlign.left), ), PosColumn( - text: (products[0].product.price!.toIntegerFromText * - products[0].quantity) + text: (products[0].product.price! * products[0].quantity) .currencyFormatRp, width: 4, styles: const PosStyles(align: PosAlign.right), @@ -430,8 +428,7 @@ class PrintDataoutputs { styles: const PosStyles(align: PosAlign.left), ), PosColumn( - text: (products[0].product.price!.toIntegerFromText * - products[0].quantity) + text: (products[0].product.price! * products[0].quantity) .currencyFormatRp, width: 4, styles: const PosStyles(align: PosAlign.right), @@ -610,8 +607,8 @@ class PrintDataoutputs { styles: const PosStyles(bold: true, align: PosAlign.left), ), PosColumn( - text: '${product.product.price!.toIntegerFromText * product.quantity}' - .currencyFormatRpV2, + text: + '${product.product.price! * product.quantity}'.currencyFormatRpV2, width: 4, styles: const PosStyles(bold: true, align: PosAlign.right), ), @@ -626,8 +623,7 @@ class PrintDataoutputs { final subTotalPrice = products.fold( 0, (previousValue, element) => - previousValue + - (element.product.price!.toIntegerFromText * element.quantity)); + previousValue + (element.product.price! * element.quantity)); bytes += generator.row([ PosColumn( text: 'Subtotal $totalQuantity Product', @@ -995,27 +991,27 @@ class PrintDataoutputs { : '--------------------------------', styles: const PosStyles(bold: false, align: PosAlign.center)); bytes += generator.feed(1); - final kitchenProducts = - products.where((p) => p.product.printerType == 'kitchen'); - for (final product in kitchenProducts) { - bytes += generator.text('${product.quantity} x ${product.product.name}', - styles: const PosStyles( - align: PosAlign.left, - bold: false, - height: PosTextSize.size2, - width: PosTextSize.size1, - )); - if (product.notes.isNotEmpty) { - bytes += generator.text(' Notes: ${product.notes}', - styles: const PosStyles( - align: PosAlign.left, - bold: false, - height: PosTextSize.size1, - width: PosTextSize.size1, - fontType: PosFontType.fontA, - )); - } - } + // final kitchenProducts = + // products.where((p) => p.product.printerType == 'kitchen'); + // for (final product in kitchenProducts) { + // bytes += generator.text('${product.quantity} x ${product.product.name}', + // styles: const PosStyles( + // align: PosAlign.left, + // bold: false, + // height: PosTextSize.size2, + // width: PosTextSize.size1, + // )); + // if (product.notes.isNotEmpty) { + // bytes += generator.text(' Notes: ${product.notes}', + // styles: const PosStyles( + // align: PosAlign.left, + // bold: false, + // height: PosTextSize.size1, + // width: PosTextSize.size1, + // fontType: PosFontType.fontA, + // )); + // } + // } bytes += generator.feed(1); bytes += generator.text( @@ -1109,26 +1105,26 @@ class PrintDataoutputs { : '--------------------------------', styles: const PosStyles(bold: false, align: PosAlign.center)); bytes += generator.feed(1); - final barProducts = products.where((p) => p.product.printerType == 'bar'); - for (final product in barProducts) { - bytes += generator.text('${product.quantity} x ${product.product.name}', - styles: const PosStyles( - align: PosAlign.left, - bold: false, - height: PosTextSize.size2, - width: PosTextSize.size1, - )); - if (product.notes.isNotEmpty) { - bytes += generator.text(' Notes: ${product.notes}', - styles: const PosStyles( - align: PosAlign.left, - bold: false, - height: PosTextSize.size1, - width: PosTextSize.size1, - fontType: PosFontType.fontA, - )); - } - } + // final barProducts = products.where((p) => p.product.printerType == 'bar'); + // for (final product in barProducts) { + // bytes += generator.text('${product.quantity} x ${product.product.name}', + // styles: const PosStyles( + // align: PosAlign.left, + // bold: false, + // height: PosTextSize.size2, + // width: PosTextSize.size1, + // )); + // if (product.notes.isNotEmpty) { + // bytes += generator.text(' Notes: ${product.notes}', + // styles: const PosStyles( + // align: PosAlign.left, + // bold: false, + // height: PosTextSize.size1, + // width: PosTextSize.size1, + // fontType: PosFontType.fontA, + // )); + // } + // } bytes += generator.feed(1); bytes += generator.text( diff --git a/lib/data/datasources/product_local_datasource.dart b/lib/data/datasources/product_local_datasource.dart index 1ac0e68..271e907 100644 --- a/lib/data/datasources/product_local_datasource.dart +++ b/lib/data/datasources/product_local_datasource.dart @@ -308,7 +308,7 @@ class ProductLocalDatasource { tableProduct, product.toLocalMap(), where: 'product_id = ?', - whereArgs: [product.productId], + whereArgs: [product.id], ); } @@ -319,7 +319,7 @@ class ProductLocalDatasource { for (var product in products) { await db.insert(tableProduct, product.toLocalMap(), conflictAlgorithm: ConflictAlgorithm.replace); - log('inserted success id: ${product.productId} | name: ${product.name} | price: ${product.price} | Printer Type ${product.printerType}'); + log('inserted success id: ${product.id} | name: ${product.name} | price: ${product.price} '); } } diff --git a/lib/data/datasources/product_remote_datasource.dart b/lib/data/datasources/product_remote_datasource.dart index dfc543d..5b2584f 100644 --- a/lib/data/datasources/product_remote_datasource.dart +++ b/lib/data/datasources/product_remote_datasource.dart @@ -1,6 +1,8 @@ import 'dart:developer'; import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/data/models/request/product_request_model.dart'; import 'package:enaklo_pos/data/models/response/add_product_response_model.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; @@ -10,19 +12,34 @@ import '../../core/constants/variables.dart'; import 'auth_local_datasource.dart'; class ProductRemoteDatasource { + final Dio dio = DioClient.instance; + Future> getProducts() async { - final url = Uri.parse('${Variables.baseUrl}/api/products'); - final authData = await AuthLocalDataSource().getAuthData(); - final response = await http.get(url, headers: { - 'Authorization': 'Bearer ${authData.token}', - 'Accept': 'application/json', - }); - log("Status Code: ${response.statusCode}"); - log("Body: ${response.body}"); - if (response.statusCode == 200) { - return Right(ProductResponseModel.fromJson(response.body)); - } else { - return const Left('Failed to get products'); + try { + final authData = await AuthLocalDataSource().getAuthData(); + final url = '${Variables.baseUrl}/api/v1/products'; + + final response = await dio.get( + url, + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + }, + ), + ); + + if (response.statusCode == 200) { + return Right(ProductResponseModel.fromMap(response.data)); + } else { + return const Left('Failed to get products'); + } + } on DioException catch (e) { + log("Dio error: ${e.message}"); + return Left(e.response?.data['message'] ?? 'Gagal mengambil produk'); + } catch (e) { + log("Unexpected error: $e"); + return const Left('Unexpected error occurred'); } } @@ -57,7 +74,7 @@ class ProductRemoteDatasource { final Map headers = { 'Authorization': 'Bearer ${authData.token}', }; - + log("Update Product Request Data: ${productRequestModel.toMap()}"); log("Update Product ID: ${productRequestModel.id}"); log("Update Product Name: ${productRequestModel.name}"); @@ -67,7 +84,7 @@ class ProductRemoteDatasource { log("Update Product Is Best Seller: ${productRequestModel.isBestSeller}"); log("Update Product Printer Type: ${productRequestModel.printerType}"); log("Update Product Has Image: ${productRequestModel.image != null}"); - + var request = http.MultipartRequest( 'POST', Uri.parse('${Variables.baseUrl}/api/products/edit')); request.fields.addAll(productRequestModel.toMap()); diff --git a/lib/data/models/request/product_request_model.dart b/lib/data/models/request/product_request_model.dart index 1ea57dd..c0cfb9c 100644 --- a/lib/data/models/request/product_request_model.dart +++ b/lib/data/models/request/product_request_model.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:image_picker/image_picker.dart'; class ProductRequestModel { - final int? id; + final String? id; final String name; final int price; final int stock; @@ -32,11 +32,11 @@ class ProductRequestModel { 'is_best_seller': isBestSeller.toString(), 'printer_type': printerType ?? '', }; - + if (id != null) { map['id'] = id.toString(); } - + return map; } } diff --git a/lib/data/models/response/product_response_model.dart b/lib/data/models/response/product_response_model.dart index 07330d6..b8527b3 100644 --- a/lib/data/models/response/product_response_model.dart +++ b/lib/data/models/response/product_response_model.dart @@ -1,17 +1,15 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -import 'package:flutter/foundation.dart'; - -import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart'; - class ProductResponseModel { - final String? status; - final List? data; + final bool? success; + final ProductData? data; + final dynamic errors; ProductResponseModel({ - this.status, + this.success, this.data, + this.errors, }); factory ProductResponseModel.fromJson(String str) => @@ -21,50 +19,86 @@ class ProductResponseModel { factory ProductResponseModel.fromMap(Map json) => ProductResponseModel( - status: json["status"], - data: json["data"] == null - ? [] - : List.from(json["data"]!.map((x) => Product.fromMap(x))), + success: json["success"], + data: json["data"] == null ? null : ProductData.fromMap(json["data"]), + errors: json["errors"], ); Map toMap() => { - "status": status, - "data": - data == null ? [] : List.from(data!.map((x) => x.toMap())), + "success": success, + "data": data?.toMap(), + "errors": errors, + }; +} + +class ProductData { + final List? products; + final int? totalCount; + final int? page; + final int? limit; + final int? totalPages; + + ProductData({ + this.products, + this.totalCount, + this.page, + this.limit, + this.totalPages, + }); + + factory ProductData.fromMap(Map json) => ProductData( + products: json["products"] == null + ? [] + : List.from( + json["products"].map((x) => Product.fromMap(x))), + totalCount: json["total_count"], + page: json["page"], + limit: json["limit"], + totalPages: json["total_pages"], + ); + + Map toMap() => { + "products": products == null + ? [] + : List.from(products!.map((x) => x.toMap())), + "total_count": totalCount, + "page": page, + "limit": limit, + "total_pages": totalPages, }; } class Product { - final int? id; - final int? productId; - final int? categoryId; + final String? id; + final String? organizationId; + final String? categoryId; + final String? sku; final String? name; final String? description; - final String? image; - final String? price; - final int? stock; - final int? status; - final int? isFavorite; + final int? price; + final int? cost; + final String? businessType; + final Map? metadata; + final bool? isActive; final DateTime? createdAt; final DateTime? updatedAt; - final Category? category; - final String? printerType; + final List? variants; Product({ this.id, - this.productId, + this.organizationId, this.categoryId, + this.sku, this.name, this.description, - this.image, this.price, - this.stock, - this.status, - this.isFavorite, + this.cost, + this.businessType, + this.metadata, + this.isActive, this.createdAt, this.updatedAt, - this.category, - this.printerType, + this.variants, }); factory Product.fromJson(String str) => Product.fromMap(json.decode(str)); @@ -72,91 +106,94 @@ class Product { String toJson() => json.encode(toMap()); factory Product.fromMap(Map json) => Product( - id: json["id"] is String ? int.tryParse(json["id"]) : json["id"], - productId: json["product_id"] is String ? int.tryParse(json["product_id"]) : json["product_id"], - categoryId: json["category_id"] is String - ? int.tryParse(json["category_id"]) - : json["category_id"], + id: json["id"], + organizationId: json["organization_id"], + categoryId: json["category_id"], + sku: json["sku"], name: json["name"], description: json["description"], - image: json["image"], - // price: json["price"].substring(0, json["price"].length - 3), - price: json["price"].toString().replaceAll('.00', ''), - stock: json["stock"] is String ? int.tryParse(json["stock"]) : json["stock"], - status: json["status"] is String ? int.tryParse(json["status"]) : json["status"], - isFavorite: json["is_favorite"] is String ? int.tryParse(json["is_favorite"]) : json["is_favorite"], + price: json["price"], + cost: json["cost"], + businessType: json["business_type"], + metadata: json["metadata"] ?? {}, + isActive: json["is_active"], createdAt: json["created_at"] == null ? null : DateTime.parse(json["created_at"]), updatedAt: json["updated_at"] == null ? null : DateTime.parse(json["updated_at"]), - category: json["category"] == null - ? null - : Category.fromMap(json["category"]), - printerType: json["printer_type"] ?? 'bar', + variants: json["variants"] == null + ? [] + : List.from( + json["variants"].map((x) => ProductVariant.fromMap(x))), ); factory Product.fromOrderMap(Map json) => Product( id: json["id_product"], - price: json["price"].toString(), + price: json["price"], ); factory Product.fromLocalMap(Map json) => Product( id: json["id"], - productId: json["product_id"], - categoryId: json["categoryId"], - category: Category( - id: json["categoryId"], - name: json["categoryName"], - ), + organizationId: json["organization_id"], + categoryId: json["category_id"], + sku: json["sku"], name: json["name"], description: json["description"], - image: json["image"], price: json["price"], - stock: json["stock"], - status: json["status"], - isFavorite: json["isFavorite"], - createdAt: json["createdAt"] == null + cost: json["cost"], + businessType: json["business_type"], + metadata: json["metadata"] ?? {}, + isActive: json["is_active"], + createdAt: json["created_at"] == null ? null - : DateTime.parse(json["createdAt"]), - updatedAt: json["updatedAt"] == null + : DateTime.parse(json["created_at"]), + updatedAt: json["updated_at"] == null ? null - : DateTime.parse(json["updatedAt"]), - printerType: json["printer_type"] ?? 'bar', + : DateTime.parse(json["updated_at"]), + variants: json["variants"] == null + ? [] + : List.from( + json["variants"].map((x) => ProductVariant.fromMap(x))), ); Map toLocalMap() => { - "product_id": id, - "categoryId": categoryId, - "categoryName": category?.name, + "id": id, + "organization_id": organizationId, + "category_id": categoryId, + "sku": sku, "name": name, "description": description, - "image": image, - "price": price?.replaceAll(RegExp(r'\.0+$'), ''), - "stock": stock, - "status": status, - "isFavorite": isFavorite, - "createdAt": createdAt?.toIso8601String(), - "updatedAt": updatedAt?.toIso8601String(), - "printer_type": printerType, + "price": price, + "cost": cost, + "business_type": businessType, + "metadata": metadata, + "is_active": isActive, + "created_at": createdAt?.toIso8601String(), + "updated_at": updatedAt?.toIso8601String(), + "variants": variants == null + ? [] + : List.from(variants!.map((x) => x.toMap())), }; Map toMap() => { "id": id, - "product_id": productId, + "organization_id": organizationId, "category_id": categoryId, + "sku": sku, "name": name, "description": description, - "image": image, "price": price, - "stock": stock, - "status": status, - "is_favorite": isFavorite, + "cost": cost, + "business_type": businessType, + "metadata": metadata, + "is_active": isActive, "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), - "category": category?.toMap(), - "printer_type": printerType, + "variants": variants == null + ? [] + : List.from(variants!.map((x) => x.toMap())), }; @override @@ -164,70 +201,80 @@ class Product { if (identical(this, other)) return true; return other.id == id && - other.productId == productId && + other.organizationId == organizationId && other.categoryId == categoryId && + other.sku == sku && other.name == name && other.description == description && - other.image == image && other.price == price && - other.stock == stock && - other.status == status && - other.isFavorite == isFavorite && + other.cost == cost && + other.businessType == businessType && + other.metadata == metadata && + other.isActive == isActive && other.createdAt == createdAt && other.updatedAt == updatedAt && - other.category == category && - other.printerType == printerType; + _listEquals(other.variants, variants); + } + + bool _listEquals(List? a, List? b) { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; } @override int get hashCode { return id.hashCode ^ - productId.hashCode ^ + organizationId.hashCode ^ categoryId.hashCode ^ + sku.hashCode ^ name.hashCode ^ description.hashCode ^ - image.hashCode ^ price.hashCode ^ - stock.hashCode ^ - status.hashCode ^ - isFavorite.hashCode ^ + cost.hashCode ^ + businessType.hashCode ^ + metadata.hashCode ^ + isActive.hashCode ^ createdAt.hashCode ^ updatedAt.hashCode ^ - category.hashCode ^ - printerType.hashCode; + variants.hashCode; } Product copyWith({ - int? id, - int? productId, - int? categoryId, + String? id, + String? organizationId, + String? categoryId, + String? sku, String? name, String? description, - String? image, - String? price, - int? stock, - int? status, - int? isFavorite, + int? price, + int? cost, + String? businessType, + Map? metadata, + bool? isActive, DateTime? createdAt, DateTime? updatedAt, - Category? category, - String? printerType, + List? variants, }) { return Product( id: id ?? this.id, - productId: productId ?? this.productId, + organizationId: organizationId ?? this.organizationId, categoryId: categoryId ?? this.categoryId, + sku: sku ?? this.sku, name: name ?? this.name, description: description ?? this.description, - image: image ?? this.image, price: price ?? this.price, - stock: stock ?? this.stock, - status: status ?? this.status, - isFavorite: isFavorite ?? this.isFavorite, + cost: cost ?? this.cost, + businessType: businessType ?? this.businessType, + metadata: metadata ?? this.metadata, + isActive: isActive ?? this.isActive, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, - category: category ?? this.category, - printerType: printerType ?? this.printerType, + variants: variants ?? this.variants, ); } } @@ -297,3 +344,51 @@ class Category { updatedAt.hashCode; } } + +class ProductVariant { + final String? id; + final String? productId; + final String? name; + final int? priceModifier; + final int? cost; + final Map? metadata; + final DateTime? createdAt; + final DateTime? updatedAt; + + ProductVariant({ + this.id, + this.productId, + this.name, + this.priceModifier, + this.cost, + this.metadata, + this.createdAt, + this.updatedAt, + }); + + factory ProductVariant.fromMap(Map json) => ProductVariant( + id: json["id"], + productId: json["product_id"], + name: json["name"], + priceModifier: json["price_modifier"], + cost: json["cost"], + metadata: json["metadata"] ?? {}, + createdAt: json["created_at"] == null + ? null + : DateTime.parse(json["created_at"]), + updatedAt: json["updated_at"] == null + ? null + : DateTime.parse(json["updated_at"]), + ); + + Map toMap() => { + "id": id, + "product_id": productId, + "name": name, + "price_modifier": priceModifier, + "cost": cost, + "metadata": metadata, + "created_at": createdAt?.toIso8601String(), + "updated_at": updatedAt?.toIso8601String(), + }; +} diff --git a/lib/main.dart b/lib/main.dart index 91401d6..e597a80 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:developer'; import 'package:enaklo_pos/core/constants/theme.dart'; +import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart'; @@ -217,6 +218,9 @@ class _MyAppState extends State { BlocProvider( create: (context) => AddOrderItemsBloc(OrderRemoteDatasource()), ), + BlocProvider( + create: (context) => ProductLoaderBloc(ProductRemoteDatasource()), + ), ], child: MaterialApp( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/home/bloc/add_order_items/add_order_items_bloc.dart b/lib/presentation/home/bloc/add_order_items/add_order_items_bloc.dart index 0b5c9ad..7f5982e 100644 --- a/lib/presentation/home/bloc/add_order_items/add_order_items_bloc.dart +++ b/lib/presentation/home/bloc/add_order_items/add_order_items_bloc.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; -import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'add_order_items_event.dart'; @@ -12,24 +11,26 @@ part 'add_order_items_bloc.freezed.dart'; class AddOrderItemsBloc extends Bloc { final OrderRemoteDatasource orderRemoteDatasource; - + AddOrderItemsBloc( this.orderRemoteDatasource, ) : super(const _Initial()) { on<_AddOrderItems>((event, emit) async { emit(const _Loading()); - + try { // Convert ProductQuantity list to the format expected by the API - final orderItems = event.items.map((item) => { - 'id_product': item.product.productId, - 'quantity': item.quantity, - 'price': item.product.price!.toIntegerFromText, - 'notes': item.notes, - }).toList(); + final orderItems = event.items + .map((item) => { + 'id_product': item.product.id, + 'quantity': item.quantity, + 'price': item.product.price, + 'notes': item.notes, + }) + .toList(); log("Adding order items: ${orderItems.toString()}"); - + final result = await orderRemoteDatasource.addOrderItems( event.orderId, orderItems, @@ -45,4 +46,4 @@ class AddOrderItemsBloc extends Bloc { } }); } -} \ No newline at end of file +} diff --git a/lib/presentation/home/bloc/order/order_bloc.dart b/lib/presentation/home/bloc/order/order_bloc.dart index 76f6b54..9aaa63f 100644 --- a/lib/presentation/home/bloc/order/order_bloc.dart +++ b/lib/presentation/home/bloc/order/order_bloc.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; @@ -33,8 +32,7 @@ class OrderBloc extends Bloc { final subTotal = event.items.fold( 0, (previousValue, element) => - previousValue + - (element.product.price!.toIntegerFromText * element.quantity)); + previousValue + (element.product.price! * element.quantity)); // final total = subTotal + event.tax + event.serviceCharge - event.discount; final totalItem = event.items.fold( diff --git a/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart b/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart new file mode 100644 index 0000000..60daaf4 --- /dev/null +++ b/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart @@ -0,0 +1,23 @@ +import 'package:bloc/bloc.dart'; +import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; +import 'package:enaklo_pos/data/models/response/product_response_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'product_loader_event.dart'; +part 'product_loader_state.dart'; +part 'product_loader_bloc.freezed.dart'; + +class ProductLoaderBloc extends Bloc { + final ProductRemoteDatasource _productRemoteDatasource; + ProductLoaderBloc(this._productRemoteDatasource) + : super(ProductLoaderState.initial()) { + on<_GetProduct>((event, emit) async { + emit(const _Loading()); + final result = await _productRemoteDatasource.getProducts(); + result.fold( + (l) => emit(_Error(l)), + (r) => emit(_Loaded(r.data?.products ?? [])), + ); + }); + } +} diff --git a/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart b/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart new file mode 100644 index 0000000..9c2f0d8 --- /dev/null +++ b/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart @@ -0,0 +1,790 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'product_loader_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ProductLoaderEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() getProduct, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getProduct, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getProduct, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_GetProduct value) getProduct, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetProduct value)? getProduct, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetProduct value)? getProduct, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductLoaderEventCopyWith<$Res> { + factory $ProductLoaderEventCopyWith( + ProductLoaderEvent value, $Res Function(ProductLoaderEvent) then) = + _$ProductLoaderEventCopyWithImpl<$Res, ProductLoaderEvent>; +} + +/// @nodoc +class _$ProductLoaderEventCopyWithImpl<$Res, $Val extends ProductLoaderEvent> + implements $ProductLoaderEventCopyWith<$Res> { + _$ProductLoaderEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$GetProductImplCopyWith<$Res> { + factory _$$GetProductImplCopyWith( + _$GetProductImpl value, $Res Function(_$GetProductImpl) then) = + __$$GetProductImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$GetProductImplCopyWithImpl<$Res> + extends _$ProductLoaderEventCopyWithImpl<$Res, _$GetProductImpl> + implements _$$GetProductImplCopyWith<$Res> { + __$$GetProductImplCopyWithImpl( + _$GetProductImpl _value, $Res Function(_$GetProductImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$GetProductImpl implements _GetProduct { + const _$GetProductImpl(); + + @override + String toString() { + return 'ProductLoaderEvent.getProduct()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$GetProductImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getProduct, + }) { + return getProduct(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getProduct, + }) { + return getProduct?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getProduct, + required TResult orElse(), + }) { + if (getProduct != null) { + return getProduct(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GetProduct value) getProduct, + }) { + return getProduct(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetProduct value)? getProduct, + }) { + return getProduct?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetProduct value)? getProduct, + required TResult orElse(), + }) { + if (getProduct != null) { + return getProduct(this); + } + return orElse(); + } +} + +abstract class _GetProduct implements ProductLoaderEvent { + const factory _GetProduct() = _$GetProductImpl; +} + +/// @nodoc +mixin _$ProductLoaderState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List products) loaded, + required TResult Function(String message) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List products)? loaded, + TResult? Function(String message)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List products)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductLoaderStateCopyWith<$Res> { + factory $ProductLoaderStateCopyWith( + ProductLoaderState value, $Res Function(ProductLoaderState) then) = + _$ProductLoaderStateCopyWithImpl<$Res, ProductLoaderState>; +} + +/// @nodoc +class _$ProductLoaderStateCopyWithImpl<$Res, $Val extends ProductLoaderState> + implements $ProductLoaderStateCopyWith<$Res> { + _$ProductLoaderStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$ProductLoaderStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'ProductLoaderState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List products) loaded, + required TResult Function(String message) error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List products)? loaded, + TResult? Function(String message)? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List products)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements ProductLoaderState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$ProductLoaderStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'ProductLoaderState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List products) loaded, + required TResult Function(String message) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List products)? loaded, + TResult? Function(String message)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List products)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements ProductLoaderState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$LoadedImplCopyWith<$Res> { + factory _$$LoadedImplCopyWith( + _$LoadedImpl value, $Res Function(_$LoadedImpl) then) = + __$$LoadedImplCopyWithImpl<$Res>; + @useResult + $Res call({List products}); +} + +/// @nodoc +class __$$LoadedImplCopyWithImpl<$Res> + extends _$ProductLoaderStateCopyWithImpl<$Res, _$LoadedImpl> + implements _$$LoadedImplCopyWith<$Res> { + __$$LoadedImplCopyWithImpl( + _$LoadedImpl _value, $Res Function(_$LoadedImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? products = null, + }) { + return _then(_$LoadedImpl( + null == products + ? _value._products + : products // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$LoadedImpl implements _Loaded { + const _$LoadedImpl(final List products) : _products = products; + + final List _products; + @override + List get products { + if (_products is EqualUnmodifiableListView) return _products; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_products); + } + + @override + String toString() { + return 'ProductLoaderState.loaded(products: $products)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadedImpl && + const DeepCollectionEquality().equals(other._products, _products)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_products)); + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoadedImplCopyWith<_$LoadedImpl> get copyWith => + __$$LoadedImplCopyWithImpl<_$LoadedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List products) loaded, + required TResult Function(String message) error, + }) { + return loaded(products); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List products)? loaded, + TResult? Function(String message)? error, + }) { + return loaded?.call(products); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List products)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(products); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class _Loaded implements ProductLoaderState { + const factory _Loaded(final List products) = _$LoadedImpl; + + List get products; + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoadedImplCopyWith<_$LoadedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ErrorImplCopyWith<$Res> { + factory _$$ErrorImplCopyWith( + _$ErrorImpl value, $Res Function(_$ErrorImpl) then) = + __$$ErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$ErrorImplCopyWithImpl<$Res> + extends _$ProductLoaderStateCopyWithImpl<$Res, _$ErrorImpl> + implements _$$ErrorImplCopyWith<$Res> { + __$$ErrorImplCopyWithImpl( + _$ErrorImpl _value, $Res Function(_$ErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$ErrorImpl( + null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ErrorImpl implements _Error { + const _$ErrorImpl(this.message); + + @override + final String message; + + @override + String toString() { + return 'ProductLoaderState.error(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + __$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List products) loaded, + required TResult Function(String message) error, + }) { + return error(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List products)? loaded, + TResult? Function(String message)? error, + }) { + return error?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List products)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class _Error implements ProductLoaderState { + const factory _Error(final String message) = _$ErrorImpl; + + String get message; + + /// Create a copy of ProductLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/home/bloc/product_loader/product_loader_event.dart b/lib/presentation/home/bloc/product_loader/product_loader_event.dart new file mode 100644 index 0000000..33f9ad3 --- /dev/null +++ b/lib/presentation/home/bloc/product_loader/product_loader_event.dart @@ -0,0 +1,6 @@ +part of 'product_loader_bloc.dart'; + +@freezed +class ProductLoaderEvent with _$ProductLoaderEvent { + const factory ProductLoaderEvent.getProduct() = _GetProduct; +} diff --git a/lib/presentation/home/bloc/product_loader/product_loader_state.dart b/lib/presentation/home/bloc/product_loader/product_loader_state.dart new file mode 100644 index 0000000..c2a3f2c --- /dev/null +++ b/lib/presentation/home/bloc/product_loader/product_loader_state.dart @@ -0,0 +1,9 @@ +part of 'product_loader_bloc.dart'; + +@freezed +class ProductLoaderState with _$ProductLoaderState { + const factory ProductLoaderState.initial() = _Initial; + const factory ProductLoaderState.loading() = _Loading; + const factory ProductLoaderState.loaded(List products) = _Loaded; + const factory ProductLoaderState.error(String message) = _Error; +} diff --git a/lib/presentation/home/models/product_quantity.dart b/lib/presentation/home/models/product_quantity.dart index 335ccdb..69cc40b 100644 --- a/lib/presentation/home/models/product_quantity.dart +++ b/lib/presentation/home/models/product_quantity.dart @@ -39,7 +39,7 @@ class ProductQuantity { return { 'id_order': orderId, - 'id_product': product.productId, + 'id_product': product.id, 'quantity': quantity, 'price': product.price, 'notes': notes, @@ -47,11 +47,11 @@ class ProductQuantity { } Map toServerMap(int? orderId) { - log("toServerMap: ${product.productId}"); + log("toServerMap: ${product.id}"); return { 'id_order': orderId ?? 0, - 'id_product': product.productId, + 'id_product': product.id, 'quantity': quantity, 'price': product.price, 'notes': notes, diff --git a/lib/presentation/home/pages/confirm_payment_page-old.dart b/lib/presentation/home/pages/confirm_payment_page-old.dart index 424ef94..4d6b24a 100644 --- a/lib/presentation/home/pages/confirm_payment_page-old.dart +++ b/lib/presentation/home/pages/confirm_payment_page-old.dart @@ -254,8 +254,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), )); return Text( @@ -315,8 +314,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), )); @@ -373,8 +371,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -457,8 +454,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -509,8 +505,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -1262,8 +1257,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -1566,10 +1560,8 @@ class _ConfirmPaymentPageState extends State { 0, (sum, item) => sum + - (int.tryParse(item - .product - .price ?? - '0') ?? + (item.product + .price ?? 0) * item.quantity), ); diff --git a/lib/presentation/home/pages/confirm_payment_page.dart b/lib/presentation/home/pages/confirm_payment_page.dart index 3ea0ead..ee63f99 100644 --- a/lib/presentation/home/pages/confirm_payment_page.dart +++ b/lib/presentation/home/pages/confirm_payment_page.dart @@ -282,8 +282,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), )); return Text( @@ -340,8 +339,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -417,8 +415,7 @@ class _ConfirmPaymentPageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); diff --git a/lib/presentation/home/pages/home_page.dart b/lib/presentation/home/pages/home_page.dart index 9e2f0ce..619ed30 100644 --- a/lib/presentation/home/pages/home_page.dart +++ b/lib/presentation/home/pages/home_page.dart @@ -1,16 +1,13 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:enaklo_pos/presentation/home/widgets/home_right_title.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/core/extensions/int_ext.dart'; -import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; -import 'package:enaklo_pos/presentation/home/bloc/local_product/local_product_bloc.dart'; import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart'; -import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; -import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import '../../../core/assets/assets.gen.dart'; @@ -49,12 +46,15 @@ class _HomePageState extends State { void _syncAndLoadProducts() { // Trigger sync from API first - context.read().add(const SyncProductEvent.syncProduct()); + // context.read().add(const SyncProductEvent.syncProduct()); // Also load local products initially in case sync fails or takes time + // context + // .read() + // .add(const LocalProductEvent.getLocalProduct()); context - .read() - .add(const LocalProductEvent.getLocalProduct()); + .read() + .add(const ProductLoaderEvent.getProduct()); // Initialize checkout with tax and service charge settings context.read().add(const CheckoutEvent.started()); @@ -83,7 +83,7 @@ class _HomePageState extends State { List products, int categoryId) { final filteredBySearch = _filterProducts(products); return filteredBySearch - .where((element) => element.category?.id == categoryId) + .where((element) => element.price == categoryId) .toList(); } @@ -93,451 +93,420 @@ class _HomePageState extends State { tag: 'confirmation_screen', child: Scaffold( backgroundColor: AppColors.white, - body: BlocListener( - listener: (context, state) { - state.maybeWhen( - orElse: () {}, - error: (message) { - // If sync fails, still try to load local products - context - .read() - .add(const LocalProductEvent.getLocalProduct()); - }, - loaded: (productResponseModel) async { - // Store context reference before async operations - final localProductBloc = context.read(); - - // Save synced products to local database - await ProductLocalDatasource.instance.deleteAllProducts(); - await ProductLocalDatasource.instance.insertProducts( - productResponseModel.data!, - ); - // Then load local products to display - localProductBloc.add(const LocalProductEvent.getLocalProduct()); - }, - ); - }, - child: Row( - children: [ - Expanded( - flex: 3, - child: Align( - alignment: AlignmentDirectional.topStart, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - HomeTitle( - controller: searchController, - onChanged: (value) { - setState(() { - searchQuery = value; - }); - }, - ), - BlocBuilder( - builder: (context, state) { - return Expanded( - child: CustomTabBarV2( - tabTitles: const [ - 'Semua', - 'Makanan', - 'Minuman', - 'Paket' - ], - tabViews: [ - // All Products Tab - SizedBox( - child: state.maybeWhen(orElse: () { + body: Row( + children: [ + Expanded( + flex: 3, + child: Align( + alignment: AlignmentDirectional.topStart, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + HomeTitle( + controller: searchController, + onChanged: (value) { + setState(() { + searchQuery = value; + }); + }, + ), + BlocBuilder( + builder: (context, state) { + return Expanded( + child: CustomTabBarV2( + tabTitles: const [ + 'Semua', + 'Makanan', + 'Minuman', + 'Paket' + ], + tabViews: [ + // All Products Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + final filteredProducts = + _filterProducts(products); + if (filteredProducts.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items Found'), ); - }, loading: () { + } + return GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 180, + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 180 / 240, + ), + itemBuilder: (context, index) => + ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ), + ); + }), + ), + // Makanan Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + if (products.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items'), ); - }, loaded: (products) { - final filteredProducts = - _filterProducts(products); - if (filteredProducts.isEmpty) { - return const Center( - child: Text('No Items Found'), - ); - } - return GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 180, - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 180 / 240, - ), - itemBuilder: (context, index) => - ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ), - ); - }), - ), - // Makanan Tab - SizedBox( - child: state.maybeWhen(orElse: () { + } + final filteredProducts = + _filterProductsByCategory(products, 1); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) => + ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ), + ); + }), + ), + // Minuman Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + if (products.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items'), ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - final filteredProducts = - _filterProductsByCategory(products, 1); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - 200, // Lebar maksimal tiap item (bisa kamu ubah) - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemBuilder: (context, index) => - ProductCard( + } + final filteredProducts = + _filterProductsByCategory(products, 2); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) { + return ProductCard( data: filteredProducts[index], onCartButton: () {}, - ), - ); - }), - ), - // Minuman Tab - SizedBox( - child: state.maybeWhen(orElse: () { + ); + }, + ); + }), + ), + // Snack Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + if (products.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items'), ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - final filteredProducts = - _filterProductsByCategory(products, 2); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - 200, // Lebar maksimal tiap item (bisa kamu ubah) - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemBuilder: (context, index) { - return ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ); - }, - ); - }), - ), - // Snack Tab - SizedBox( - child: state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - final filteredProducts = - _filterProductsByCategory(products, 3); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - 200, // Lebar maksimal tiap item (bisa kamu ubah) - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemBuilder: (context, index) { - return ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ); - }, - ); - }), - ), - ], - ), - ); - }, - ), - ], - ), + } + final filteredProducts = + _filterProductsByCategory(products, 3); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) { + return ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ); + }, + ); + }), + ), + ], + ), + ); + }, + ), + ], ), ), - Expanded( - flex: 2, - child: Align( - alignment: Alignment.topCenter, - child: Material( - color: Colors.white, - child: Column( - children: [ - HomeRightTitle( - table: widget.table, - ), - Padding( - padding: const EdgeInsets.all(16.0) - .copyWith(bottom: 0, top: 27), - child: Column( - children: [ - const Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Item', + ), + Expanded( + flex: 2, + child: Align( + alignment: Alignment.topCenter, + child: Material( + color: Colors.white, + child: Column( + children: [ + HomeRightTitle( + table: widget.table, + ), + Padding( + padding: const EdgeInsets.all(16.0) + .copyWith(bottom: 0, top: 27), + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Item', + style: TextStyle( + color: AppColors.primary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + SizedBox( + width: 130, + ), + SizedBox( + width: 50.0, + child: Text( + 'Qty', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.w600, ), ), - SizedBox( - width: 130, - ), - SizedBox( - width: 50.0, - child: Text( - 'Qty', - style: TextStyle( - color: AppColors.primary, - fontSize: 16, - fontWeight: FontWeight.w600, - ), + ), + SizedBox( + child: Text( + 'Price', + style: TextStyle( + color: AppColors.primary, + fontSize: 16, + fontWeight: FontWeight.w600, ), ), - SizedBox( - child: Text( - 'Price', - style: TextStyle( - color: AppColors.primary, - fontSize: 16, - fontWeight: FontWeight.w600, - ), + ), + ], + ), + const SpaceHeight(8), + const Divider(), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0).copyWith(top: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const Center( + child: Text('No Items'), ), - ), - ], + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return const Center( + child: Text('No Items'), + ); + } + return ListView.separated( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => + OrderMenu(data: products[index]), + separatorBuilder: (context, index) => + const SpaceHeight(1.0), + itemCount: products.length, + ); + }, + ); + }, ), - const SpaceHeight(8), - const Divider(), + const SpaceHeight(8.0), ], ), ), - Expanded( - child: SingleChildScrollView( - padding: - const EdgeInsets.all(16.0).copyWith(top: 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + Padding( + padding: const EdgeInsets.all(16.0).copyWith(top: 0), + child: Column( + children: [ + const Divider(), + const SpaceHeight(16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + const Text( + 'Pajak', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), + ), BlocBuilder( builder: (context, state) { - return state.maybeWhen( - orElse: () => const Center( - child: Text('No Items'), + final tax = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return 0; + } + return tax; + }); + return Text( + '$tax %', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, ), - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - return ListView.separated( - shrinkWrap: true, - physics: - const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => - OrderMenu(data: products[index]), - separatorBuilder: (context, index) => - const SpaceHeight(1.0), - itemCount: products.length, - ); - }, ); }, ), - const SpaceHeight(8.0), ], ), - ), - ), - Padding( - padding: const EdgeInsets.all(16.0).copyWith(top: 0), - child: Column( - children: [ - const Divider(), - const SpaceHeight(16.0), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Pajak', - style: TextStyle( - color: AppColors.black, - fontWeight: FontWeight.bold, - ), - ), - BlocBuilder( - builder: (context, state) { - final tax = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - if (products.isEmpty) { - return 0; - } - return tax; - }); - return Text( - '$tax %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SpaceHeight(16.0), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Sub total', - style: TextStyle( - color: AppColors.black, - fontWeight: FontWeight.bold, - ), - ), - BlocBuilder( - builder: (context, state) { - final price = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - if (products.isEmpty) { - return 0; - } - return products - .map((e) => - e.product.price! - .toIntegerFromText * - e.quantity) - .reduce((value, element) => - value + element); - }); - - return Text( - price.currencyFormatRp, - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w900, - ), - ); - }, - ), - ], - ), - SpaceHeight(16.0), - Align( - alignment: Alignment.bottomCenter, - child: Expanded( - child: Button.filled( - borderRadius: 12, - elevation: 1, - onPressed: () { - context.push(ConfirmPaymentPage( - isTable: widget.isTable, - table: widget.table, - )); - }, - label: 'Lanjutkan Pembayaran', + const SpaceHeight(16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Sub total', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, ), ), + BlocBuilder( + builder: (context, state) { + final price = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return 0; + } + return products + .map((e) => + e.product.price! * e.quantity) + .reduce((value, element) => + value + element); + }); + + return Text( + price.currencyFormatRp, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w900, + ), + ); + }, + ), + ], + ), + SpaceHeight(16.0), + Align( + alignment: Alignment.bottomCenter, + child: Expanded( + child: Button.filled( + borderRadius: 12, + elevation: 1, + onPressed: () { + context.push(ConfirmPaymentPage( + isTable: widget.isTable, + table: widget.table, + )); + }, + label: 'Lanjutkan Pembayaran', + ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/presentation/home/widgets/order_menu.dart b/lib/presentation/home/widgets/order_menu.dart index 94ccca9..d7e2e0e 100644 --- a/lib/presentation/home/widgets/order_menu.dart +++ b/lib/presentation/home/widgets/order_menu.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; import 'package:enaklo_pos/core/extensions/int_ext.dart'; -import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; @@ -52,7 +51,7 @@ class _OrderMenuState extends State { child: ListTile( contentPadding: EdgeInsets.zero, leading: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(50.0)), + borderRadius: BorderRadius.all(Radius.circular(8.0)), child: // Icon( // Icons.fastfood, @@ -60,14 +59,23 @@ class _OrderMenuState extends State { // color: AppColors.primary, // ), CachedNetworkImage( - imageUrl: widget.data.product.image!.contains('http') - ? widget.data.product.image! - : '${Variables.baseUrl}/${widget.data.product.image}', + imageUrl: widget.data.product.name!.contains('http') + ? widget.data.product.name! + : '${Variables.baseUrl}/${widget.data.product.name}', width: 50.0, height: 50.0, fit: BoxFit.cover, - errorWidget: (context, url, error) => - const Icon(Icons.error), + errorWidget: (context, url, error) => Container( + width: 50.0, + height: 50.0, + decoration: BoxDecoration( + color: AppColors.disabled.withOpacity(0.4), + ), + child: const Icon( + Icons.image, + color: AppColors.grey, + ), + ), ), ), title: Row( @@ -86,8 +94,7 @@ class _OrderMenuState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.data.product.price!.toIntegerFromText - .currencyFormatRp), + Text(widget.data.product.price!.currencyFormatRp), ], ), ), @@ -139,8 +146,7 @@ class _OrderMenuState extends State { SizedBox( width: 80.0, child: Text( - (widget.data.product.price!.toIntegerFromText * - widget.data.quantity) + (widget.data.product.price! * widget.data.quantity) .currencyFormatRp, textAlign: TextAlign.right, style: const TextStyle( diff --git a/lib/presentation/home/widgets/product_card.dart b/lib/presentation/home/widgets/product_card.dart index 8e45252..4298aab 100644 --- a/lib/presentation/home/widgets/product_card.dart +++ b/lib/presentation/home/widgets/product_card.dart @@ -1,9 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; -import 'package:enaklo_pos/core/extensions/int_ext.dart'; -import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; @@ -43,9 +42,9 @@ class ProductCard extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.all(Radius.circular(8.0)), child: CachedNetworkImage( - imageUrl: data.image!.contains('http') - ? data.image! - : '${Variables.baseUrl}/${data.image}', + imageUrl: data.name!.contains('http') + ? data.name! + : '${Variables.baseUrl}/${data.name}', fit: BoxFit.cover, width: double.infinity, height: 120, @@ -76,7 +75,7 @@ class ProductCard extends StatelessWidget { Align( alignment: Alignment.center, child: Text( - data.category?.name ?? '-', + '-', style: const TextStyle( fontSize: 12, color: AppColors.grey, @@ -90,7 +89,7 @@ class ProductCard extends StatelessWidget { Align( alignment: Alignment.center, child: Text( - data.price!.toIntegerFromText.currencyFormatRp, + data.price!.currencyFormatRp, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 12, diff --git a/lib/presentation/sales/widgets/sales_list_order.dart b/lib/presentation/sales/widgets/sales_list_order.dart index b8ff4ec..4c01460 100644 --- a/lib/presentation/sales/widgets/sales_list_order.dart +++ b/lib/presentation/sales/widgets/sales_list_order.dart @@ -62,7 +62,7 @@ class SalesListOrder extends StatelessWidget { ), ), Text( - product.product.price ?? '', + (product.product.price ?? 0) as String, style: const TextStyle( fontSize: 14, ), @@ -76,7 +76,7 @@ class SalesListOrder extends StatelessWidget { ), ), Text( - product.product.price ?? '', + (product.product.price ?? 0) as String, style: const TextStyle( fontSize: 14, ), diff --git a/lib/presentation/setting/bloc/add_product/add_product_bloc.dart b/lib/presentation/setting/bloc/add_product/add_product_bloc.dart index 5efe3f9..29bf3ef 100644 --- a/lib/presentation/setting/bloc/add_product/add_product_bloc.dart +++ b/lib/presentation/setting/bloc/add_product/add_product_bloc.dart @@ -20,10 +20,10 @@ class AddProductBloc extends Bloc { emit(const _Loading()); final requestData = ProductRequestModel( name: event.product.name!, - price: int.parse(event.product.price!), - stock: event.product.stock!, - categoryId: event.product.categoryId!, - isBestSeller: event.product.isFavorite!, + price: event.product.price!, + stock: 0, + categoryId: 0, + isBestSeller: 0, image: event.image, ); log("requestData: ${requestData.toString()}"); diff --git a/lib/presentation/setting/bloc/get_products/get_products_bloc.dart b/lib/presentation/setting/bloc/get_products/get_products_bloc.dart index bf492ce..c7e05f8 100644 --- a/lib/presentation/setting/bloc/get_products/get_products_bloc.dart +++ b/lib/presentation/setting/bloc/get_products/get_products_bloc.dart @@ -1,4 +1,3 @@ - import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; @@ -19,7 +18,7 @@ class GetProductsBloc extends Bloc { response.fold( (l) => emit(_Error(l)), (r) { - emit(_Success(r.data!)); + emit(_Success(r.data!.products!)); }, ); }); diff --git a/lib/presentation/setting/bloc/update_product/update_product_bloc.dart b/lib/presentation/setting/bloc/update_product/update_product_bloc.dart index ace0614..83ad348 100644 --- a/lib/presentation/setting/bloc/update_product/update_product_bloc.dart +++ b/lib/presentation/setting/bloc/update_product/update_product_bloc.dart @@ -19,57 +19,58 @@ class UpdateProductBloc extends Bloc { ) : super(const _Initial()) { on<_UpdateProduct>((event, emit) async { emit(const _Loading()); - + try { // Validate required fields if (event.product.name == null || event.product.name!.isEmpty) { emit(_Error('Product name is required')); return; } - - if (event.product.price == null || event.product.price!.isEmpty) { + + if (event.product.price == null || event.product.price == 0) { emit(_Error('Product price is required')); return; } - - if (event.product.stock == null) { - emit(_Error('Product stock is required')); - return; - } - + + // if (event.product.stock == null) { + // emit(_Error('Product stock is required')); + // return; + // } + if (event.product.categoryId == null) { emit(_Error('Product category is required')); return; } - + // Parse price safely - final price = int.tryParse(event.product.price!); - if (price == null) { + final price = event.product.price!; + if (price == 0) { emit(_Error('Invalid price format')); return; } - + final requestData = ProductRequestModel( id: event.product.id, name: event.product.name!, price: price, - stock: event.product.stock!, - categoryId: event.product.categoryId!, - isBestSeller: event.product.isFavorite ?? 0, // Default to 0 if null + stock: 0, + categoryId: 0, + isBestSeller: 0, // Default to 0 if null image: event.image, - printerType: event.product.printerType ?? 'kitchen', // Default to kitchen if null + printerType: 'kitchen', // Default to kitchen if null ); - + log("Update requestData: ${requestData.toString()}"); log("Request map: ${requestData.toMap()}"); - + final response = await datasource.updateProduct(requestData); response.fold( (l) => emit(_Error(l)), (r) async { // Update local database after successful API update try { - await ProductLocalDatasource.instance.updateProduct(event.product); + await ProductLocalDatasource.instance + .updateProduct(event.product); log("Local product updated successfully"); } catch (e) { log("Error updating local product: $e"); @@ -83,4 +84,4 @@ class UpdateProductBloc extends Bloc { } }); } -} \ No newline at end of file +} diff --git a/lib/presentation/setting/dialogs/detail_product_dialog.dart b/lib/presentation/setting/dialogs/detail_product_dialog.dart index 445019b..4b96426 100644 --- a/lib/presentation/setting/dialogs/detail_product_dialog.dart +++ b/lib/presentation/setting/dialogs/detail_product_dialog.dart @@ -28,9 +28,9 @@ class DetailProductDialog extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( - imageUrl: product.image!.contains('http') - ? product.image! - : '${Variables.baseUrl}/${product.image}', + imageUrl: product.name!.contains('http') + ? product.name! + : '${Variables.baseUrl}/${product.name}', fit: BoxFit.cover, width: 120, height: 120, @@ -88,20 +88,20 @@ class DetailProductDialog extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildItem( - product.category?.name ?? "-", + "-", "Kategori", ), + // _buildItem( + // "${product.stock}", + // "Stok", + // valueColor: product.stock! < 50 + // ? AppColors.red + // : product.stock! < 100 + // ? Colors.yellow + // : AppColors.green, + // ), _buildItem( - "${product.stock}", - "Stok", - valueColor: product.stock! < 50 - ? AppColors.red - : product.stock! < 100 - ? Colors.yellow - : AppColors.green, - ), - _buildItem( - (product.price ?? "0").currencyFormatRpV2, + (product.price ?? 0).toString().currencyFormatRpV2, "Harga", valueColor: AppColors.primary, ), @@ -142,11 +142,11 @@ class DetailProductDialog extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: product.status == 1 ? AppColors.green : AppColors.red, + color: product.isActive == true ? AppColors.green : AppColors.red, borderRadius: BorderRadius.circular(8), ), child: Text( - product.status == 1 ? 'Aktif' : 'Tidak Aktif', + product.isActive == true ? 'Aktif' : 'Tidak Aktif', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w700), ), diff --git a/lib/presentation/setting/dialogs/form_product_dialog.dart b/lib/presentation/setting/dialogs/form_product_dialog.dart index 2f1fa5b..779376b 100644 --- a/lib/presentation/setting/dialogs/form_product_dialog.dart +++ b/lib/presentation/setting/dialogs/form_product_dialog.dart @@ -57,12 +57,12 @@ class _FormProductDialogState extends State { // Pre-fill the form with existing product data final product = widget.product!; nameController!.text = product.name ?? ''; - priceValue = int.tryParse(product.price ?? '0') ?? 0; + priceValue = product.price ?? 0; priceController!.text = priceValue.currencyFormatRp; - stockController!.text = (product.stock ?? 0).toString(); - isBestSeller = product.isFavorite == 1; - printType = product.printerType ?? 'kitchen'; - imageUrl = product.image; + stockController!.text = ''; + isBestSeller = false; + printType = 'kitchen'; + imageUrl = ''; } super.initState(); @@ -129,72 +129,72 @@ class _FormProductDialogState extends State { keyboardType: TextInputType.number, ), const SpaceHeight(20.0), - const Text( - "Kategori", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - const SpaceHeight(12.0), - BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - success: (categories) { - // Set the selected category if in edit mode and not already set - if (isEditMode && - selectCategory == null && - widget.product?.category != null) { - try { - selectCategory = categories.firstWhere( - (cat) => cat.id == widget.product!.category!.id, - ); - } catch (e) { - // If no exact match found, leave selectCategory as null - // This will show the hint text instead - log("No matching category found for product category ID: ${widget.product!.category!.id}"); - } - } + // const Text( + // "Kategori", + // style: TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w700, + // ), + // ), + // const SpaceHeight(12.0), + // BlocBuilder( + // builder: (context, state) { + // return state.maybeWhen( + // orElse: () { + // return const Center( + // child: CircularProgressIndicator(), + // ); + // }, + // success: (categories) { + // // Set the selected category if in edit mode and not already set + // if (isEditMode && + // selectCategory == null && + // widget.product?.category != null) { + // try { + // selectCategory = categories.firstWhere( + // (cat) => cat.id == widget.product!.category!.id, + // ); + // } catch (e) { + // // If no exact match found, leave selectCategory as null + // // This will show the hint text instead + // log("No matching category found for product category ID: ${widget.product!.category!.id}"); + // } + // } - return DropdownButtonHideUnderline( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - child: DropdownButton( - value: selectCategory, - hint: const Text("Pilih Kategori"), - isExpanded: true, // Untuk mengisi lebar container - onChanged: (newValue) { - if (newValue != null) { - selectCategory = newValue; - setState(() {}); - log("selectCategory: ${selectCategory!.name}"); - } - }, - items: categories - .map>( - (CategoryModel category) { - return DropdownMenuItem( - value: category, - child: Text(category.name!), - ); - }).toList(), - ), - ), - ); - }, - ); - }, - ), + // return DropdownButtonHideUnderline( + // child: Container( + // decoration: BoxDecoration( + // border: Border.all(color: Colors.grey), + // borderRadius: BorderRadius.circular(12), + // ), + // padding: const EdgeInsets.symmetric( + // horizontal: 10, vertical: 5), + // child: DropdownButton( + // value: selectCategory, + // hint: const Text("Pilih Kategori"), + // isExpanded: true, // Untuk mengisi lebar container + // onChanged: (newValue) { + // if (newValue != null) { + // selectCategory = newValue; + // setState(() {}); + // log("selectCategory: ${selectCategory!.name}"); + // } + // }, + // items: categories + // .map>( + // (CategoryModel category) { + // return DropdownMenuItem( + // value: category, + // child: Text(category.name!), + // ); + // }).toList(), + // ), + // ), + // ); + // }, + // ); + // }, + // ), const SpaceHeight(12.0), const Text( "Tipe Print", @@ -296,23 +296,23 @@ class _FormProductDialogState extends State { return; } - log("isBestSeller: $isBestSeller"); - final String name = nameController!.text; - final int stock = - stockController!.text.toIntegerFromText; + // log("isBestSeller: $isBestSeller"); + // final String name = nameController!.text; + // final int stock = + // stockController!.text.toIntegerFromText; - final Product product = widget.product!.copyWith( - name: name, - price: priceValue.toString(), - stock: stock, - categoryId: selectCategory!.id!, - isFavorite: isBestSeller ? 1 : 0, - printerType: printType, - ); + // final Product product = widget.product!.copyWith( + // name: name, + // price: priceValue.toString(), + // stock: stock, + // categoryId: selectCategory!.id!, + // isFavorite: isBestSeller ? 1 : 0, + // printerType: printType, + // ); - context.read().add( - UpdateProductEvent.updateProduct( - product, imageFile)); + // context.read().add( + // UpdateProductEvent.updateProduct( + // product, imageFile)); }, label: 'Ubah Produk', ); @@ -354,33 +354,33 @@ class _FormProductDialogState extends State { orElse: () { return Button.filled( onPressed: () { - if (selectCategory == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select a category'), - backgroundColor: Colors.red, - ), - ); - return; - } + // if (selectCategory == null) { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Please select a category'), + // backgroundColor: Colors.red, + // ), + // ); + // return; + // } - log("isBestSeller: $isBestSeller"); - final String name = nameController!.text; + // log("isBestSeller: $isBestSeller"); + // final String name = nameController!.text; - final int stock = - stockController!.text.toIntegerFromText; - final Product product = Product( - name: name, - price: priceValue.toString(), - stock: stock, - categoryId: selectCategory!.id!, - isFavorite: isBestSeller ? 1 : 0, - image: imageFile!.path, - printerType: printType, - ); - context.read().add( - AddProductEvent.addProduct( - product, imageFile!)); + // final int stock = + // stockController!.text.toIntegerFromText; + // final Product product = Product( + // name: name, + // price: priceValue.toString(), + // stock: stock, + // categoryId: selectCategory!.id!, + // isFavorite: isBestSeller ? 1 : 0, + // image: imageFile!.path, + // printerType: printType, + // ); + // context.read().add( + // AddProductEvent.addProduct( + // product, imageFile!)); }, label: 'Simpan Produk', ); @@ -441,14 +441,14 @@ class _FormProductDialogOldState extends State { if (isEditMode) { // Pre-fill the form with existing product data - final product = widget.product!; - nameController!.text = product.name ?? ''; - priceValue = int.tryParse(product.price ?? '0') ?? 0; - priceController!.text = priceValue.currencyFormatRp; - stockController!.text = (product.stock ?? 0).toString(); - isBestSeller = product.isFavorite == 1; - printType = product.printerType ?? 'kitchen'; - imageUrl = product.image; + // final product = widget.product!; + // nameController!.text = product.name ?? ''; + // priceValue = int.tryParse(product.price ?? '0') ?? 0; + // priceController!.text = priceValue.currencyFormatRp; + // stockController!.text = (product.stock ?? 0).toString(); + // isBestSeller = product.isFavorite == 1; + // printType = product.printerType ?? 'kitchen'; + // imageUrl = product.image; } super.initState(); @@ -542,19 +542,19 @@ class _FormProductDialogOldState extends State { }, success: (categories) { // Set the selected category if in edit mode and not already set - if (isEditMode && - selectCategory == null && - widget.product?.category != null) { - try { - selectCategory = categories.firstWhere( - (cat) => cat.id == widget.product!.category!.id, - ); - } catch (e) { - // If no exact match found, leave selectCategory as null - // This will show the hint text instead - log("No matching category found for product category ID: ${widget.product!.category!.id}"); - } - } + // if (isEditMode && + // selectCategory == null && + // widget.product?.category != null) { + // try { + // selectCategory = categories.firstWhere( + // (cat) => cat.id == widget.product!.category!.id, + // ); + // } catch (e) { + // // If no exact match found, leave selectCategory as null + // // This will show the hint text instead + // log("No matching category found for product category ID: ${widget.product!.category!.id}"); + // } + // } return DropdownButtonHideUnderline( child: Container( @@ -696,18 +696,18 @@ class _FormProductDialogOldState extends State { final int stock = stockController!.text.toIntegerFromText; - final Product product = widget.product!.copyWith( - name: name, - price: priceValue.toString(), - stock: stock, - categoryId: selectCategory!.id!, - isFavorite: isBestSeller ? 1 : 0, - printerType: printType, - ); + // final Product product = widget.product!.copyWith( + // name: name, + // price: priceValue.toString(), + // stock: stock, + // categoryId: selectCategory!.id!, + // isFavorite: isBestSeller ? 1 : 0, + // printerType: printType, + // ); - context.read().add( - UpdateProductEvent.updateProduct( - product, imageFile)); + // context.read().add( + // UpdateProductEvent.updateProduct( + // product, imageFile)); }, label: 'Update Product', ); @@ -764,18 +764,18 @@ class _FormProductDialogOldState extends State { final int stock = stockController!.text.toIntegerFromText; - final Product product = Product( - name: name, - price: priceValue.toString(), - stock: stock, - categoryId: selectCategory!.id!, - isFavorite: isBestSeller ? 1 : 0, - image: imageFile!.path, - printerType: printType, - ); - context.read().add( - AddProductEvent.addProduct( - product, imageFile!)); + // final Product product = Product( + // name: name, + // price: priceValue.toString(), + // stock: stock, + // categoryId: selectCategory!.id!, + // isFavorite: isBestSeller ? 1 : 0, + // image: imageFile!.path, + // printerType: printType, + // ); + // context.read().add( + // AddProductEvent.addProduct( + // product, imageFile!)); }, label: 'Save Product', ); diff --git a/lib/presentation/setting/pages/sync_data_page.dart b/lib/presentation/setting/pages/sync_data_page.dart index b64734e..d87f361 100644 --- a/lib/presentation/setting/pages/sync_data_page.dart +++ b/lib/presentation/setting/pages/sync_data_page.dart @@ -62,7 +62,7 @@ class _SyncDataPageState extends State { .deleteAllProducts(); await ProductLocalDatasource.instance .insertProducts( - productResponseModel.data!, + productResponseModel.data!.products!, ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( diff --git a/lib/presentation/setting/widgets/menu_product_item.dart b/lib/presentation/setting/widgets/menu_product_item.dart index b9cc346..2092f99 100644 --- a/lib/presentation/setting/widgets/menu_product_item.dart +++ b/lib/presentation/setting/widgets/menu_product_item.dart @@ -36,9 +36,9 @@ class MenuProductItem extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( - imageUrl: data.image!.contains('http') - ? data.image! - : '${Variables.baseUrl}/${data.image}', + imageUrl: data.name!.contains('http') + ? data.name! + : '${Variables.baseUrl}/${data.name}', fit: BoxFit.cover, errorWidget: (context, url, error) => Container( width: double.infinity, @@ -67,7 +67,7 @@ class MenuProductItem extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - data.category?.name ?? "", + "", style: const TextStyle( color: AppColors.white, fontSize: 10, @@ -185,7 +185,7 @@ class MenuProductItemOld extends StatelessWidget { child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(10.0)), child: CachedNetworkImage( - imageUrl: '${Variables.baseUrl}/${data.image}', + imageUrl: '${Variables.baseUrl}/${data.name}', placeholder: (context, url) => const Center(child: CircularProgressIndicator()), errorWidget: (context, url, error) => const Icon( @@ -214,7 +214,7 @@ class MenuProductItemOld extends StatelessWidget { overflow: TextOverflow.ellipsis, ), Text( - data.category?.name ?? '-', + '-', style: const TextStyle( fontSize: 12, color: Colors.grey, @@ -260,7 +260,7 @@ class MenuProductItemOld extends StatelessWidget { Radius.circular(10.0)), child: CachedNetworkImage( imageUrl: - '${Variables.baseUrl}${data.image}', + '${Variables.baseUrl}${data.name}', placeholder: (context, url) => const Center( child: @@ -275,7 +275,7 @@ class MenuProductItemOld extends StatelessWidget { ), const SpaceHeight(10.0), Text( - data.category?.name ?? '-', + '-', style: const TextStyle( fontSize: 12, color: Colors.grey, @@ -291,7 +291,7 @@ class MenuProductItemOld extends StatelessWidget { ), const SpaceHeight(10.0), Text( - data.stock.toString(), + "data.stock.toString()", style: const TextStyle( fontSize: 12, color: Colors.grey, diff --git a/lib/presentation/table/models/draft_order_item.dart b/lib/presentation/table/models/draft_order_item.dart index afe8d66..694e5c0 100644 --- a/lib/presentation/table/models/draft_order_item.dart +++ b/lib/presentation/table/models/draft_order_item.dart @@ -26,7 +26,7 @@ class DraftOrderItem { Map toMapForLocal(int orderId) { return { 'id_draft_order': orderId, - 'id_product': product.productId, + 'id_product': product.id, 'quantity': quantity, 'price': product.price, }; diff --git a/lib/presentation/table/pages/payment_table_page.dart b/lib/presentation/table/pages/payment_table_page.dart index 7dbbbf5..8f32541 100644 --- a/lib/presentation/table/pages/payment_table_page.dart +++ b/lib/presentation/table/pages/payment_table_page.dart @@ -46,7 +46,7 @@ class _PaymentTablePageState extends State { PaymentMethod? selectedPaymentMethod; int totalPriceFinal = 0; int discountAmountFinal = 0; - + // Helper method to handle post-payment cleanup Future _handlePostPaymentCleanup() async { if (widget.table != null && widget.draftOrder?.id != null) { @@ -60,21 +60,22 @@ class _PaymentTablePageState extends State { startTime: DateTime.now().toIso8601String(), position: widget.table!.position, ); - + // Update table status await ProductLocalDatasource.instance.updateStatusTable(newTable); - + // Remove draft order - await ProductLocalDatasource.instance.removeDraftOrderById(widget.draftOrder!.id!); - + await ProductLocalDatasource.instance + .removeDraftOrderById(widget.draftOrder!.id!); + // Refresh table status context.read().add( - GetTableStatusEvent.getTablesStatus('all'), - ); - + GetTableStatusEvent.getTablesStatus('all'), + ); + log("Table ${widget.table!.tableName} freed up and draft order removed"); } - + // Safely navigate back - pop multiple times to get to table management // Pop the success dialog first, then the payment page if (Navigator.of(context).canPop()) { @@ -84,6 +85,7 @@ class _PaymentTablePageState extends State { } } } + @override void initState() { context @@ -93,7 +95,7 @@ class _PaymentTablePageState extends State { .read() .add(PaymentMethodsEvent.fetchPaymentMethods()); super.initState(); - + // Set a default payment method in case API fails selectedPaymentMethod = PaymentMethod( id: 1, @@ -135,7 +137,8 @@ class _PaymentTablePageState extends State { builder: (BuildContext context) { return AlertDialog( title: const Text('Kembali'), - content: const Text('Apakah Anda yakin ingin kembali? Order akan tetap tersimpan.'), + content: const Text( + 'Apakah Anda yakin ingin kembali? Order akan tetap tersimpan.'), actions: [ TextButton( onPressed: () { @@ -146,7 +149,8 @@ class _PaymentTablePageState extends State { TextButton( onPressed: () { Navigator.of(context).pop(); // Close dialog - Navigator.of(context).pop(); // Go back to previous page + Navigator.of(context) + .pop(); // Go back to previous page }, child: const Text('Ya'), ), @@ -157,7 +161,8 @@ class _PaymentTablePageState extends State { }, child: const Text( 'Kembali', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), ), ), ], @@ -251,7 +256,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) { + draftName, + orderType) { if (products.isEmpty) { return const Center( child: Text('No Items'), @@ -294,13 +300,13 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => products.fold( 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), )); return Text( @@ -374,7 +380,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => tax, ); final price = state.maybeWhen( @@ -387,13 +394,13 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => products.fold( 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -408,7 +415,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) { + draftName, + orderType) { return discountAmount; }); @@ -446,7 +454,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => tax, ); final price = state.maybeWhen( @@ -459,13 +468,13 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => products.fold( 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -480,7 +489,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) { + draftName, + orderType) { return discountAmount; }); @@ -494,7 +504,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => serviceCharge, ); @@ -536,13 +547,13 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => products.fold( 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -557,7 +568,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) { + draftName, + orderType) { return discountAmount; }); @@ -571,7 +583,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => serviceCharge, ); @@ -585,7 +598,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) => + draftName, + orderType) => tax, ); @@ -666,7 +680,8 @@ class _PaymentTablePageState extends State { serviceCharge, totalQuantity, totalPrice, - draftName, orderType) { + draftName, + orderType) { customerController.text = draftName; return TextFormField( readOnly: true, @@ -699,7 +714,8 @@ class _PaymentTablePageState extends State { ), ), const SpaceHeight(12.0), - BlocBuilder( + BlocBuilder( builder: (context, state) { return state.maybeWhen( orElse: () => const Center( @@ -717,14 +733,16 @@ class _PaymentTablePageState extends State { error: (message) => Column( children: [ Center( - child: Text('Error loading payment methods: $message'), + child: Text( + 'Error loading payment methods: $message'), ), const SpaceHeight(16.0), Button.filled( onPressed: () { context .read() - .add(PaymentMethodsEvent.fetchPaymentMethods()); + .add(PaymentMethodsEvent + .fetchPaymentMethods()); }, label: 'Retry', ), @@ -739,32 +757,39 @@ class _PaymentTablePageState extends State { return Column( children: [ const Center( - child: Text('No payment methods available'), + child: Text( + 'No payment methods available'), ), const SpaceHeight(16.0), Button.filled( onPressed: () { context .read() - .add(PaymentMethodsEvent.fetchPaymentMethods()); + .add(PaymentMethodsEvent + .fetchPaymentMethods()); }, label: 'Retry', ), ], ); } - + // Set default selected payment method if none selected or if current selection is not in the list - if (selectedPaymentMethod == null || - !paymentMethods.any((method) => method.id == selectedPaymentMethod?.id)) { - selectedPaymentMethod = paymentMethods.first; + if (selectedPaymentMethod == null || + !paymentMethods.any((method) => + method.id == + selectedPaymentMethod?.id)) { + selectedPaymentMethod = + paymentMethods.first; } - + return Wrap( spacing: 12.0, runSpacing: 8.0, children: paymentMethods.map((method) { - final isSelected = selectedPaymentMethod?.id == method.id; + final isSelected = + selectedPaymentMethod?.id == + method.id; return Container( constraints: const BoxConstraints( minWidth: 120.0, @@ -775,31 +800,44 @@ class _PaymentTablePageState extends State { color: AppColors.primary, width: 2.0, ), - borderRadius: BorderRadius.circular(8.0), + borderRadius: + BorderRadius.circular( + 8.0), ) : null, child: Tooltip( - message: method.description ?? 'No description available', + message: method.description ?? + 'No description available', child: isSelected ? Button.filled( width: double.infinity, height: 50.0, onPressed: () { setState(() { - selectedPaymentMethod = method; + selectedPaymentMethod = + method; }); }, - label: method.name?.isNotEmpty == true ? method.name! : 'Unknown', + label: method.name + ?.isNotEmpty == + true + ? method.name! + : 'Unknown', ) : Button.outlined( width: double.infinity, height: 50.0, onPressed: () { setState(() { - selectedPaymentMethod = method; + selectedPaymentMethod = + method; }); }, - label: method.name?.isNotEmpty == true ? method.name! : 'Unknown', + label: method.name + ?.isNotEmpty == + true + ? method.name! + : 'Unknown', ), ), ); @@ -881,30 +919,41 @@ class _PaymentTablePageState extends State { builder: (context) => AlertDialog( title: Row( children: [ - Icon(Icons.warning, color: AppColors.red), + Icon(Icons.warning, + color: AppColors.red), SizedBox(width: 8), - Text('Batalkan Pesanan?'), + Text('Batalkan Pesanan?'), ], ), content: Text( 'Apakah anda yakin ingin membatalkan pesanan untuk meja ${widget.table?.tableName ?? "ini"}?\n\nPesanan akan dihapus secara permanen.'), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Tidak', - style: TextStyle(color: AppColors.primary)), + onPressed: () => + Navigator.pop(context), + child: Text('Tidak', + style: TextStyle( + color: + AppColors.primary)), ), - BlocListener( + BlocListener( listener: (context, state) { state.maybeWhen( orElse: () {}, success: () { - Navigator.pop(context); // Close void dialog - Navigator.pop(context); // Close payment page - ScaffoldMessenger.of(context).showSnackBar( + Navigator.pop( + context); // Close void dialog + Navigator.pop( + context); // Close payment page + ScaffoldMessenger.of( + context) + .showSnackBar( const SnackBar( - content: Text('Pesanan berhasil dibatalkan'), - backgroundColor: AppColors.primary, + content: Text( + 'Pesanan berhasil dibatalkan'), + backgroundColor: + AppColors.primary, ), ); }, @@ -912,34 +961,47 @@ class _PaymentTablePageState extends State { }, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: AppColors.red, + backgroundColor: + AppColors.red, ), onPressed: () { // Void the order if (widget.table != null) { final newTable = TableModel( id: widget.table!.id, - tableName: widget.table!.tableName, + tableName: widget + .table!.tableName, status: 'available', orderId: 0, paymentAmount: 0, - startTime: DateTime.now().toIso8601String(), - position: widget.table!.position, + startTime: DateTime.now() + .toIso8601String(), + position: widget + .table!.position, ); - context.read().add( - StatusTableEvent.statusTabel(newTable), + context + .read() + .add( + StatusTableEvent + .statusTabel( + newTable), ); } // Remove draft order from local storage - if (widget.draftOrder?.id != null) { - ProductLocalDatasource.instance - .removeDraftOrderById(widget.draftOrder!.id!); + if (widget.draftOrder?.id != + null) { + ProductLocalDatasource + .instance + .removeDraftOrderById( + widget.draftOrder! + .id!); } log("Voided order from payment page"); }, child: const Text( "Ya, Batalkan", - style: TextStyle(color: Colors.white), + style: TextStyle( + color: Colors.white), ), ), ), @@ -1011,8 +1073,7 @@ class _PaymentTablePageState extends State { 0, (previousValue, element) => previousValue + - (element.product.price! - .toIntegerFromText * + (element.product.price! * element.quantity), ), ); @@ -1080,22 +1141,29 @@ class _PaymentTablePageState extends State { child: Button.filled( onPressed: () async { if (selectedPaymentMethod == null) { - ScaffoldMessenger.of(context).showSnackBar( + ScaffoldMessenger.of(context) + .showSnackBar( const SnackBar( - content: Text('Please select a payment method'), + content: Text( + 'Please select a payment method'), backgroundColor: Colors.red, ), ); return; } - - final paymentMethodName = selectedPaymentMethod?.name?.toLowerCase() ?? ''; + + final paymentMethodName = + selectedPaymentMethod?.name + ?.toLowerCase() ?? + ''; log("Selected payment method: ${selectedPaymentMethod?.name} (normalized: $paymentMethodName)"); - - if (paymentMethodName == 'cash' || + + if (paymentMethodName == 'cash' || paymentMethodName == 'tunai' || - paymentMethodName == 'uang tunai' || - paymentMethodName == 'cash payment') { + paymentMethodName == + 'uang tunai' || + paymentMethodName == + 'cash payment') { context.read().add( OrderEvent.order( items, @@ -1109,7 +1177,9 @@ class _PaymentTablePageState extends State { widget.table?.id ?? 0, 'completed', 'paid', - selectedPaymentMethod?.name ?? 'Cash', + selectedPaymentMethod + ?.name ?? + 'Cash', totalPriceFinal, orderType)); @@ -1149,7 +1219,9 @@ class _PaymentTablePageState extends State { widget.table?.id ?? 0, 'completed', 'paid', - selectedPaymentMethod?.name ?? 'Unknown Payment Method', + selectedPaymentMethod + ?.name ?? + 'Unknown Payment Method', totalPriceFinal, orderType)); @@ -1173,7 +1245,7 @@ class _PaymentTablePageState extends State { customerController.text, ), ); - + // Handle post-payment cleanup await _handlePostPaymentCleanup(); }