dev #1

Merged
aefril merged 128 commits from dev into main 2025-08-13 17:19:48 +00:00
28 changed files with 1922 additions and 948 deletions
Showing only changes of commit 7678b4791d - Show all commits

View File

@ -60,12 +60,12 @@ class PrintDataoutputs {
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: text:
'${product.product.price!.toIntegerFromText.currencyFormatRp} x ${product.quantity}', '${product.product.price!.currencyFormatRp} x ${product.quantity}',
width: 8, width: 8,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: '${product.product.price!.toIntegerFromText * product.quantity}' text: '${product.product.price! * product.quantity}'
.toIntegerFromText .toIntegerFromText
.currencyFormatRp, .currencyFormatRp,
width: 4, width: 4,
@ -328,8 +328,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (product.product.price!.toIntegerFromText * product.quantity) text: (product.product.price! * product.quantity).currencyFormatRp,
.currencyFormatRp,
width: 4, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
@ -398,8 +397,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (products[0].product.price!.toIntegerFromText * text: (products[0].product.price! * products[0].quantity)
products[0].quantity)
.currencyFormatRp, .currencyFormatRp,
width: 4, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
@ -430,8 +428,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (products[0].product.price!.toIntegerFromText * text: (products[0].product.price! * products[0].quantity)
products[0].quantity)
.currencyFormatRp, .currencyFormatRp,
width: 4, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
@ -610,8 +607,8 @@ class PrintDataoutputs {
styles: const PosStyles(bold: true, align: PosAlign.left), styles: const PosStyles(bold: true, align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: '${product.product.price!.toIntegerFromText * product.quantity}' text:
.currencyFormatRpV2, '${product.product.price! * product.quantity}'.currencyFormatRpV2,
width: 4, width: 4,
styles: const PosStyles(bold: true, align: PosAlign.right), styles: const PosStyles(bold: true, align: PosAlign.right),
), ),
@ -626,8 +623,7 @@ class PrintDataoutputs {
final subTotalPrice = products.fold<int>( final subTotalPrice = products.fold<int>(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue + (element.product.price! * element.quantity));
(element.product.price!.toIntegerFromText * element.quantity));
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Subtotal $totalQuantity Product', text: 'Subtotal $totalQuantity Product',
@ -995,27 +991,27 @@ class PrintDataoutputs {
: '--------------------------------', : '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.feed(1); bytes += generator.feed(1);
final kitchenProducts = // final kitchenProducts =
products.where((p) => p.product.printerType == 'kitchen'); // products.where((p) => p.product.printerType == 'kitchen');
for (final product in kitchenProducts) { // for (final product in kitchenProducts) {
bytes += generator.text('${product.quantity} x ${product.product.name}', // bytes += generator.text('${product.quantity} x ${product.product.name}',
styles: const PosStyles( // styles: const PosStyles(
align: PosAlign.left, // align: PosAlign.left,
bold: false, // bold: false,
height: PosTextSize.size2, // height: PosTextSize.size2,
width: PosTextSize.size1, // width: PosTextSize.size1,
)); // ));
if (product.notes.isNotEmpty) { // if (product.notes.isNotEmpty) {
bytes += generator.text(' Notes: ${product.notes}', // bytes += generator.text(' Notes: ${product.notes}',
styles: const PosStyles( // styles: const PosStyles(
align: PosAlign.left, // align: PosAlign.left,
bold: false, // bold: false,
height: PosTextSize.size1, // height: PosTextSize.size1,
width: PosTextSize.size1, // width: PosTextSize.size1,
fontType: PosFontType.fontA, // fontType: PosFontType.fontA,
)); // ));
} // }
} // }
bytes += generator.feed(1); bytes += generator.feed(1);
bytes += generator.text( bytes += generator.text(
@ -1109,26 +1105,26 @@ class PrintDataoutputs {
: '--------------------------------', : '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.feed(1); bytes += generator.feed(1);
final barProducts = products.where((p) => p.product.printerType == 'bar'); // final barProducts = products.where((p) => p.product.printerType == 'bar');
for (final product in barProducts) { // for (final product in barProducts) {
bytes += generator.text('${product.quantity} x ${product.product.name}', // bytes += generator.text('${product.quantity} x ${product.product.name}',
styles: const PosStyles( // styles: const PosStyles(
align: PosAlign.left, // align: PosAlign.left,
bold: false, // bold: false,
height: PosTextSize.size2, // height: PosTextSize.size2,
width: PosTextSize.size1, // width: PosTextSize.size1,
)); // ));
if (product.notes.isNotEmpty) { // if (product.notes.isNotEmpty) {
bytes += generator.text(' Notes: ${product.notes}', // bytes += generator.text(' Notes: ${product.notes}',
styles: const PosStyles( // styles: const PosStyles(
align: PosAlign.left, // align: PosAlign.left,
bold: false, // bold: false,
height: PosTextSize.size1, // height: PosTextSize.size1,
width: PosTextSize.size1, // width: PosTextSize.size1,
fontType: PosFontType.fontA, // fontType: PosFontType.fontA,
)); // ));
} // }
} // }
bytes += generator.feed(1); bytes += generator.feed(1);
bytes += generator.text( bytes += generator.text(

View File

@ -308,7 +308,7 @@ class ProductLocalDatasource {
tableProduct, tableProduct,
product.toLocalMap(), product.toLocalMap(),
where: 'product_id = ?', where: 'product_id = ?',
whereArgs: [product.productId], whereArgs: [product.id],
); );
} }
@ -319,7 +319,7 @@ class ProductLocalDatasource {
for (var product in products) { for (var product in products) {
await db.insert(tableProduct, product.toLocalMap(), await db.insert(tableProduct, product.toLocalMap(),
conflictAlgorithm: ConflictAlgorithm.replace); 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} ');
} }
} }

View File

@ -1,6 +1,8 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dartz/dartz.dart'; 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/request/product_request_model.dart';
import 'package:enaklo_pos/data/models/response/add_product_response_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'; 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'; import 'auth_local_datasource.dart';
class ProductRemoteDatasource { class ProductRemoteDatasource {
final Dio dio = DioClient.instance;
Future<Either<String, ProductResponseModel>> getProducts() async { Future<Either<String, ProductResponseModel>> getProducts() async {
final url = Uri.parse('${Variables.baseUrl}/api/products'); try {
final authData = await AuthLocalDataSource().getAuthData(); final authData = await AuthLocalDataSource().getAuthData();
final response = await http.get(url, headers: { final url = '${Variables.baseUrl}/api/v1/products';
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json', final response = await dio.get(
}); url,
log("Status Code: ${response.statusCode}"); options: Options(
log("Body: ${response.body}"); headers: {
if (response.statusCode == 200) { 'Authorization': 'Bearer ${authData.token}',
return Right(ProductResponseModel.fromJson(response.body)); 'Accept': 'application/json',
} else { },
return const Left('Failed to get products'); ),
);
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');
} }
} }

View File

@ -3,7 +3,7 @@ import 'dart:developer';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
class ProductRequestModel { class ProductRequestModel {
final int? id; final String? id;
final String name; final String name;
final int price; final int price;
final int stock; final int stock;

View File

@ -1,17 +1,15 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first // ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart';
class ProductResponseModel { class ProductResponseModel {
final String? status; final bool? success;
final List<Product>? data; final ProductData? data;
final dynamic errors;
ProductResponseModel({ ProductResponseModel({
this.status, this.success,
this.data, this.data,
this.errors,
}); });
factory ProductResponseModel.fromJson(String str) => factory ProductResponseModel.fromJson(String str) =>
@ -21,50 +19,86 @@ class ProductResponseModel {
factory ProductResponseModel.fromMap(Map<String, dynamic> json) => factory ProductResponseModel.fromMap(Map<String, dynamic> json) =>
ProductResponseModel( ProductResponseModel(
status: json["status"], success: json["success"],
data: json["data"] == null data: json["data"] == null ? null : ProductData.fromMap(json["data"]),
? [] errors: json["errors"],
: List<Product>.from(json["data"]!.map((x) => Product.fromMap(x))),
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"status": status, "success": success,
"data": "data": data?.toMap(),
data == null ? [] : List<dynamic>.from(data!.map((x) => x.toMap())), "errors": errors,
};
}
class ProductData {
final List<Product>? 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<String, dynamic> json) => ProductData(
products: json["products"] == null
? []
: List<Product>.from(
json["products"].map((x) => Product.fromMap(x))),
totalCount: json["total_count"],
page: json["page"],
limit: json["limit"],
totalPages: json["total_pages"],
);
Map<String, dynamic> toMap() => {
"products": products == null
? []
: List<dynamic>.from(products!.map((x) => x.toMap())),
"total_count": totalCount,
"page": page,
"limit": limit,
"total_pages": totalPages,
}; };
} }
class Product { class Product {
final int? id; final String? id;
final int? productId; final String? organizationId;
final int? categoryId; final String? categoryId;
final String? sku;
final String? name; final String? name;
final String? description; final String? description;
final String? image; final int? price;
final String? price; final int? cost;
final int? stock; final String? businessType;
final int? status; final Map<String, dynamic>? metadata;
final int? isFavorite; final bool? isActive;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final Category? category; final List<ProductVariant>? variants;
final String? printerType;
Product({ Product({
this.id, this.id,
this.productId, this.organizationId,
this.categoryId, this.categoryId,
this.sku,
this.name, this.name,
this.description, this.description,
this.image,
this.price, this.price,
this.stock, this.cost,
this.status, this.businessType,
this.isFavorite, this.metadata,
this.isActive,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
this.category, this.variants,
this.printerType,
}); });
factory Product.fromJson(String str) => Product.fromMap(json.decode(str)); factory Product.fromJson(String str) => Product.fromMap(json.decode(str));
@ -72,91 +106,94 @@ class Product {
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
factory Product.fromMap(Map<String, dynamic> json) => Product( factory Product.fromMap(Map<String, dynamic> json) => Product(
id: json["id"] is String ? int.tryParse(json["id"]) : json["id"], id: json["id"],
productId: json["product_id"] is String ? int.tryParse(json["product_id"]) : json["product_id"], organizationId: json["organization_id"],
categoryId: json["category_id"] is String categoryId: json["category_id"],
? int.tryParse(json["category_id"]) sku: json["sku"],
: json["category_id"],
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
image: json["image"], price: json["price"],
// price: json["price"].substring(0, json["price"].length - 3), cost: json["cost"],
price: json["price"].toString().replaceAll('.00', ''), businessType: json["business_type"],
stock: json["stock"] is String ? int.tryParse(json["stock"]) : json["stock"], metadata: json["metadata"] ?? {},
status: json["status"] is String ? int.tryParse(json["status"]) : json["status"], isActive: json["is_active"],
isFavorite: json["is_favorite"] is String ? int.tryParse(json["is_favorite"]) : json["is_favorite"],
createdAt: json["created_at"] == null createdAt: json["created_at"] == null
? null ? null
: DateTime.parse(json["created_at"]), : DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null updatedAt: json["updated_at"] == null
? null ? null
: DateTime.parse(json["updated_at"]), : DateTime.parse(json["updated_at"]),
category: json["category"] == null variants: json["variants"] == null
? null ? []
: Category.fromMap(json["category"]), : List<ProductVariant>.from(
printerType: json["printer_type"] ?? 'bar', json["variants"].map((x) => ProductVariant.fromMap(x))),
); );
factory Product.fromOrderMap(Map<String, dynamic> json) => Product( factory Product.fromOrderMap(Map<String, dynamic> json) => Product(
id: json["id_product"], id: json["id_product"],
price: json["price"].toString(), price: json["price"],
); );
factory Product.fromLocalMap(Map<String, dynamic> json) => Product( factory Product.fromLocalMap(Map<String, dynamic> json) => Product(
id: json["id"], id: json["id"],
productId: json["product_id"], organizationId: json["organization_id"],
categoryId: json["categoryId"], categoryId: json["category_id"],
category: Category( sku: json["sku"],
id: json["categoryId"],
name: json["categoryName"],
),
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
image: json["image"],
price: json["price"], price: json["price"],
stock: json["stock"], cost: json["cost"],
status: json["status"], businessType: json["business_type"],
isFavorite: json["isFavorite"], metadata: json["metadata"] ?? {},
createdAt: json["createdAt"] == null isActive: json["is_active"],
createdAt: json["created_at"] == null
? null ? null
: DateTime.parse(json["createdAt"]), : DateTime.parse(json["created_at"]),
updatedAt: json["updatedAt"] == null updatedAt: json["updated_at"] == null
? null ? null
: DateTime.parse(json["updatedAt"]), : DateTime.parse(json["updated_at"]),
printerType: json["printer_type"] ?? 'bar', variants: json["variants"] == null
? []
: List<ProductVariant>.from(
json["variants"].map((x) => ProductVariant.fromMap(x))),
); );
Map<String, dynamic> toLocalMap() => { Map<String, dynamic> toLocalMap() => {
"product_id": id, "id": id,
"categoryId": categoryId, "organization_id": organizationId,
"categoryName": category?.name, "category_id": categoryId,
"sku": sku,
"name": name, "name": name,
"description": description, "description": description,
"image": image, "price": price,
"price": price?.replaceAll(RegExp(r'\.0+$'), ''), "cost": cost,
"stock": stock, "business_type": businessType,
"status": status, "metadata": metadata,
"isFavorite": isFavorite, "is_active": isActive,
"createdAt": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updatedAt": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
"printer_type": printerType, "variants": variants == null
? []
: List<dynamic>.from(variants!.map((x) => x.toMap())),
}; };
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"id": id, "id": id,
"product_id": productId, "organization_id": organizationId,
"category_id": categoryId, "category_id": categoryId,
"sku": sku,
"name": name, "name": name,
"description": description, "description": description,
"image": image,
"price": price, "price": price,
"stock": stock, "cost": cost,
"status": status, "business_type": businessType,
"is_favorite": isFavorite, "metadata": metadata,
"is_active": isActive,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
"category": category?.toMap(), "variants": variants == null
"printer_type": printerType, ? []
: List<dynamic>.from(variants!.map((x) => x.toMap())),
}; };
@override @override
@ -164,70 +201,80 @@ class Product {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.id == id && return other.id == id &&
other.productId == productId && other.organizationId == organizationId &&
other.categoryId == categoryId && other.categoryId == categoryId &&
other.sku == sku &&
other.name == name && other.name == name &&
other.description == description && other.description == description &&
other.image == image &&
other.price == price && other.price == price &&
other.stock == stock && other.cost == cost &&
other.status == status && other.businessType == businessType &&
other.isFavorite == isFavorite && other.metadata == metadata &&
other.isActive == isActive &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.updatedAt == updatedAt && other.updatedAt == updatedAt &&
other.category == category && _listEquals(other.variants, variants);
other.printerType == printerType; }
bool _listEquals(List<ProductVariant>? a, List<ProductVariant>? 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 @override
int get hashCode { int get hashCode {
return id.hashCode ^ return id.hashCode ^
productId.hashCode ^ organizationId.hashCode ^
categoryId.hashCode ^ categoryId.hashCode ^
sku.hashCode ^
name.hashCode ^ name.hashCode ^
description.hashCode ^ description.hashCode ^
image.hashCode ^
price.hashCode ^ price.hashCode ^
stock.hashCode ^ cost.hashCode ^
status.hashCode ^ businessType.hashCode ^
isFavorite.hashCode ^ metadata.hashCode ^
isActive.hashCode ^
createdAt.hashCode ^ createdAt.hashCode ^
updatedAt.hashCode ^ updatedAt.hashCode ^
category.hashCode ^ variants.hashCode;
printerType.hashCode;
} }
Product copyWith({ Product copyWith({
int? id, String? id,
int? productId, String? organizationId,
int? categoryId, String? categoryId,
String? sku,
String? name, String? name,
String? description, String? description,
String? image, int? price,
String? price, int? cost,
int? stock, String? businessType,
int? status, Map<String, dynamic>? metadata,
int? isFavorite, bool? isActive,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
Category? category, List<ProductVariant>? variants,
String? printerType,
}) { }) {
return Product( return Product(
id: id ?? this.id, id: id ?? this.id,
productId: productId ?? this.productId, organizationId: organizationId ?? this.organizationId,
categoryId: categoryId ?? this.categoryId, categoryId: categoryId ?? this.categoryId,
sku: sku ?? this.sku,
name: name ?? this.name, name: name ?? this.name,
description: description ?? this.description, description: description ?? this.description,
image: image ?? this.image,
price: price ?? this.price, price: price ?? this.price,
stock: stock ?? this.stock, cost: cost ?? this.cost,
status: status ?? this.status, businessType: businessType ?? this.businessType,
isFavorite: isFavorite ?? this.isFavorite, metadata: metadata ?? this.metadata,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
category: category ?? this.category, variants: variants ?? this.variants,
printerType: printerType ?? this.printerType,
); );
} }
} }
@ -297,3 +344,51 @@ class Category {
updatedAt.hashCode; updatedAt.hashCode;
} }
} }
class ProductVariant {
final String? id;
final String? productId;
final String? name;
final int? priceModifier;
final int? cost;
final Map<String, dynamic>? 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<String, dynamic> 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<String, dynamic> toMap() => {
"id": id,
"product_id": productId,
"name": name,
"price_modifier": priceModifier,
"cost": cost,
"metadata": metadata,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
};
}

View File

@ -1,5 +1,6 @@
import 'dart:developer'; import 'dart:developer';
import 'package:enaklo_pos/core/constants/theme.dart'; 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:flutter/material.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart';
@ -217,6 +218,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider( BlocProvider(
create: (context) => AddOrderItemsBloc(OrderRemoteDatasource()), create: (context) => AddOrderItemsBloc(OrderRemoteDatasource()),
), ),
BlocProvider(
create: (context) => ProductLoaderBloc(ProductRemoteDatasource()),
),
], ],
child: MaterialApp( child: MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,

View File

@ -3,7 +3,6 @@ import 'dart:developer';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/order_remote_datasource.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/presentation/home/models/product_quantity.dart';
import 'package:enaklo_pos/core/extensions/string_ext.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'add_order_items_event.dart'; part 'add_order_items_event.dart';
@ -21,12 +20,14 @@ class AddOrderItemsBloc extends Bloc<AddOrderItemsEvent, AddOrderItemsState> {
try { try {
// Convert ProductQuantity list to the format expected by the API // Convert ProductQuantity list to the format expected by the API
final orderItems = event.items.map((item) => { final orderItems = event.items
'id_product': item.product.productId, .map((item) => {
'quantity': item.quantity, 'id_product': item.product.id,
'price': item.product.price!.toIntegerFromText, 'quantity': item.quantity,
'notes': item.notes, 'price': item.product.price,
}).toList(); 'notes': item.notes,
})
.toList();
log("Adding order items: ${orderItems.toString()}"); log("Adding order items: ${orderItems.toString()}");

View File

@ -3,7 +3,6 @@ import 'dart:developer';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.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/auth_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
@ -33,8 +32,7 @@ class OrderBloc extends Bloc<OrderEvent, OrderState> {
final subTotal = event.items.fold<int>( final subTotal = event.items.fold<int>(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue + (element.product.price! * element.quantity));
(element.product.price!.toIntegerFromText * element.quantity));
// final total = subTotal + event.tax + event.serviceCharge - event.discount; // final total = subTotal + event.tax + event.serviceCharge - event.discount;
final totalItem = event.items.fold<int>( final totalItem = event.items.fold<int>(

View File

@ -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<ProductLoaderEvent, ProductLoaderState> {
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 ?? [])),
);
});
}
}

View File

@ -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>(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<TResult extends Object?>({
required TResult Function() getProduct,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function() getProduct,
}) {
return getProduct();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? getProduct,
}) {
return getProduct?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? getProduct,
required TResult orElse(),
}) {
if (getProduct != null) {
return getProduct();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_GetProduct value) getProduct,
}) {
return getProduct(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_GetProduct value)? getProduct,
}) {
return getProduct?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(String message) error,
}) {
return initial();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(String message)? error,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(String message) error,
}) {
return loading();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(String message)? error,
}) {
return loading?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<Product> 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<Product>,
));
}
}
/// @nodoc
class _$LoadedImpl implements _Loaded {
const _$LoadedImpl(final List<Product> products) : _products = products;
final List<Product> _products;
@override
List<Product> 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<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(String message) error,
}) {
return loaded(products);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(String message)? error,
}) {
return loaded?.call(products);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(products);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<Product> products) = _$LoadedImpl;
List<Product> 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<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(List<Product> products) loaded,
required TResult Function(String message) error,
}) {
return error(message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(List<Product> products)? loaded,
TResult? Function(String message)? error,
}) {
return error?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(List<Product> products)? loaded,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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;
}

View File

@ -0,0 +1,6 @@
part of 'product_loader_bloc.dart';
@freezed
class ProductLoaderEvent with _$ProductLoaderEvent {
const factory ProductLoaderEvent.getProduct() = _GetProduct;
}

View File

@ -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<Product> products) = _Loaded;
const factory ProductLoaderState.error(String message) = _Error;
}

View File

@ -39,7 +39,7 @@ class ProductQuantity {
return { return {
'id_order': orderId, 'id_order': orderId,
'id_product': product.productId, 'id_product': product.id,
'quantity': quantity, 'quantity': quantity,
'price': product.price, 'price': product.price,
'notes': notes, 'notes': notes,
@ -47,11 +47,11 @@ class ProductQuantity {
} }
Map<String, dynamic> toServerMap(int? orderId) { Map<String, dynamic> toServerMap(int? orderId) {
log("toServerMap: ${product.productId}"); log("toServerMap: ${product.id}");
return { return {
'id_order': orderId ?? 0, 'id_order': orderId ?? 0,
'id_product': product.productId, 'id_product': product.id,
'quantity': quantity, 'quantity': quantity,
'price': product.price, 'price': product.price,
'notes': notes, 'notes': notes,

View File

@ -254,8 +254,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
)); ));
return Text( return Text(
@ -315,8 +314,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
)); ));
@ -373,8 +371,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -457,8 +454,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -509,8 +505,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -1262,8 +1257,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -1566,10 +1560,8 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(sum, item) => (sum, item) =>
sum + sum +
(int.tryParse(item (item.product
.product .price ??
.price ??
'0') ??
0) * 0) *
item.quantity), item.quantity),
); );

View File

@ -282,8 +282,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
)); ));
return Text( return Text(
@ -340,8 +339,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -417,8 +415,7 @@ class _ConfirmPaymentPageState extends State<ConfirmPaymentPage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );

View File

@ -1,16 +1,13 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first // 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:enaklo_pos/presentation/home/widgets/home_right_title.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.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/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/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/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 'package:enaklo_pos/data/models/response/product_response_model.dart';
import '../../../core/assets/assets.gen.dart'; import '../../../core/assets/assets.gen.dart';
@ -49,12 +46,15 @@ class _HomePageState extends State<HomePage> {
void _syncAndLoadProducts() { void _syncAndLoadProducts() {
// Trigger sync from API first // Trigger sync from API first
context.read<SyncProductBloc>().add(const SyncProductEvent.syncProduct()); // context.read<SyncProductBloc>().add(const SyncProductEvent.syncProduct());
// Also load local products initially in case sync fails or takes time // Also load local products initially in case sync fails or takes time
// context
// .read<LocalProductBloc>()
// .add(const LocalProductEvent.getLocalProduct());
context context
.read<LocalProductBloc>() .read<ProductLoaderBloc>()
.add(const LocalProductEvent.getLocalProduct()); .add(const ProductLoaderEvent.getProduct());
// Initialize checkout with tax and service charge settings // Initialize checkout with tax and service charge settings
context.read<CheckoutBloc>().add(const CheckoutEvent.started()); context.read<CheckoutBloc>().add(const CheckoutEvent.started());
@ -83,7 +83,7 @@ class _HomePageState extends State<HomePage> {
List<Product> products, int categoryId) { List<Product> products, int categoryId) {
final filteredBySearch = _filterProducts(products); final filteredBySearch = _filterProducts(products);
return filteredBySearch return filteredBySearch
.where((element) => element.category?.id == categoryId) .where((element) => element.price == categoryId)
.toList(); .toList();
} }
@ -93,451 +93,420 @@ class _HomePageState extends State<HomePage> {
tag: 'confirmation_screen', tag: 'confirmation_screen',
child: Scaffold( child: Scaffold(
backgroundColor: AppColors.white, backgroundColor: AppColors.white,
body: BlocListener<SyncProductBloc, SyncProductState>( body: Row(
listener: (context, state) { children: [
state.maybeWhen( Expanded(
orElse: () {}, flex: 3,
error: (message) { child: Align(
// If sync fails, still try to load local products alignment: AlignmentDirectional.topStart,
context child: Column(
.read<LocalProductBloc>() mainAxisAlignment: MainAxisAlignment.start,
.add(const LocalProductEvent.getLocalProduct()); children: [
}, HomeTitle(
loaded: (productResponseModel) async { controller: searchController,
// Store context reference before async operations onChanged: (value) {
final localProductBloc = context.read<LocalProductBloc>(); setState(() {
searchQuery = value;
// Save synced products to local database });
await ProductLocalDatasource.instance.deleteAllProducts(); },
await ProductLocalDatasource.instance.insertProducts( ),
productResponseModel.data!, BlocBuilder<ProductLoaderBloc, ProductLoaderState>(
); builder: (context, state) {
// Then load local products to display return Expanded(
localProductBloc.add(const LocalProductEvent.getLocalProduct()); child: CustomTabBarV2(
}, tabTitles: const [
); 'Semua',
}, 'Makanan',
child: Row( 'Minuman',
children: [ 'Paket'
Expanded( ],
flex: 3, tabViews: [
child: Align( // All Products Tab
alignment: AlignmentDirectional.topStart, SizedBox(
child: Column( child: state.maybeWhen(orElse: () {
mainAxisAlignment: MainAxisAlignment.start, return const Center(
children: [ child: CircularProgressIndicator(),
HomeTitle( );
controller: searchController, }, loading: () {
onChanged: (value) { return const Center(
setState(() { child: CircularProgressIndicator(),
searchQuery = value; );
}); }, loaded: (products) {
}, final filteredProducts =
), _filterProducts(products);
BlocBuilder<LocalProductBloc, LocalProductState>( if (filteredProducts.isEmpty) {
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( 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( return const Center(
child: CircularProgressIndicator(), child: Text('No Items'),
); );
}, loaded: (products) { }
final filteredProducts = final filteredProducts =
_filterProducts(products); _filterProductsByCategory(products, 1);
if (filteredProducts.isEmpty) { return filteredProducts.isEmpty
return const Center( ? const _IsEmpty()
child: Text('No Items Found'), : GridView.builder(
); itemCount: filteredProducts.length,
} padding: const EdgeInsets.all(16),
return GridView.builder( gridDelegate:
itemCount: filteredProducts.length, SliverGridDelegateWithMaxCrossAxisExtent(
padding: const EdgeInsets.all(16), maxCrossAxisExtent:
gridDelegate: 200, // Lebar maksimal tiap item (bisa kamu ubah)
SliverGridDelegateWithMaxCrossAxisExtent( mainAxisSpacing: 30,
maxCrossAxisExtent: 180, crossAxisSpacing: 30,
mainAxisSpacing: 30, childAspectRatio: 0.85,
crossAxisSpacing: 30, ),
childAspectRatio: 180 / 240, itemBuilder: (context, index) =>
), ProductCard(
itemBuilder: (context, index) => data: filteredProducts[index],
ProductCard( onCartButton: () {},
data: filteredProducts[index], ),
onCartButton: () {}, );
), }),
); ),
}), // Minuman Tab
), SizedBox(
// Makanan Tab child: state.maybeWhen(orElse: () {
SizedBox( return const Center(
child: state.maybeWhen(orElse: () { child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products) {
if (products.isEmpty) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: Text('No Items'),
); );
}, loading: () { }
return const Center( final filteredProducts =
child: CircularProgressIndicator(), _filterProductsByCategory(products, 2);
); return filteredProducts.isEmpty
}, loaded: (products) { ? const _IsEmpty()
if (products.isEmpty) { : GridView.builder(
return const Center( itemCount: filteredProducts.length,
child: Text('No Items'), padding: const EdgeInsets.all(16),
); gridDelegate:
} SliverGridDelegateWithMaxCrossAxisExtent(
final filteredProducts = maxCrossAxisExtent:
_filterProductsByCategory(products, 1); 200, // Lebar maksimal tiap item (bisa kamu ubah)
return filteredProducts.isEmpty mainAxisSpacing: 30,
? const _IsEmpty() crossAxisSpacing: 30,
: GridView.builder( childAspectRatio: 0.85,
itemCount: filteredProducts.length, ),
padding: const EdgeInsets.all(16), itemBuilder: (context, index) {
gridDelegate: return ProductCard(
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) =>
ProductCard(
data: filteredProducts[index], data: filteredProducts[index],
onCartButton: () {}, onCartButton: () {},
), );
); },
}), );
), }),
// Minuman Tab ),
SizedBox( // Snack Tab
child: state.maybeWhen(orElse: () { SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products) {
if (products.isEmpty) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: Text('No Items'),
); );
}, loading: () { }
return const Center( final filteredProducts =
child: CircularProgressIndicator(), _filterProductsByCategory(products, 3);
); return filteredProducts.isEmpty
}, loaded: (products) { ? const _IsEmpty()
if (products.isEmpty) { : GridView.builder(
return const Center( itemCount: filteredProducts.length,
child: Text('No Items'), padding: const EdgeInsets.all(16),
); gridDelegate:
} SliverGridDelegateWithMaxCrossAxisExtent(
final filteredProducts = maxCrossAxisExtent:
_filterProductsByCategory(products, 2); 200, // Lebar maksimal tiap item (bisa kamu ubah)
return filteredProducts.isEmpty mainAxisSpacing: 30,
? const _IsEmpty() crossAxisSpacing: 30,
: GridView.builder( childAspectRatio: 0.85,
itemCount: filteredProducts.length, ),
padding: const EdgeInsets.all(16), itemBuilder: (context, index) {
gridDelegate: return ProductCard(
SliverGridDelegateWithMaxCrossAxisExtent( data: filteredProducts[index],
maxCrossAxisExtent: onCartButton: () {},
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: () {},
);
},
);
}),
),
],
),
);
},
),
],
),
), ),
), ),
Expanded( ),
flex: 2, Expanded(
child: Align( flex: 2,
alignment: Alignment.topCenter, child: Align(
child: Material( alignment: Alignment.topCenter,
color: Colors.white, child: Material(
child: Column( color: Colors.white,
children: [ child: Column(
HomeRightTitle( children: [
table: widget.table, HomeRightTitle(
), table: widget.table,
Padding( ),
padding: const EdgeInsets.all(16.0) Padding(
.copyWith(bottom: 0, top: 27), padding: const EdgeInsets.all(16.0)
child: Column( .copyWith(bottom: 0, top: 27),
children: [ child: Column(
const Row( children: [
mainAxisAlignment: const Row(
MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Item', 'Item',
style: TextStyle(
color: AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
SizedBox(
width: 130,
),
SizedBox(
width: 50.0,
child: Text(
'Qty',
style: TextStyle( style: TextStyle(
color: AppColors.primary, color: AppColors.primary,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox( ),
width: 130, SizedBox(
), child: Text(
SizedBox( 'Price',
width: 50.0, style: TextStyle(
child: Text( color: AppColors.primary,
'Qty', fontSize: 16,
style: TextStyle( fontWeight: FontWeight.w600,
color: AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
), ),
), ),
SizedBox( ),
child: Text( ],
'Price', ),
style: TextStyle( const SpaceHeight(8),
color: AppColors.primary, const Divider(),
fontSize: 16, ],
fontWeight: FontWeight.w600, ),
), ),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0).copyWith(top: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<CheckoutBloc, CheckoutState>(
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 SpaceHeight(8.0),
const Divider(),
], ],
), ),
), ),
Expanded( ),
child: SingleChildScrollView( Padding(
padding: padding: const EdgeInsets.all(16.0).copyWith(top: 0),
const EdgeInsets.all(16.0).copyWith(top: 0), child: Column(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, const Divider(),
const SpaceHeight(16.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text(
'Pajak',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.bold,
),
),
BlocBuilder<CheckoutBloc, CheckoutState>( BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) { builder: (context, state) {
return state.maybeWhen( final tax = state.maybeWhen(
orElse: () => const Center( orElse: () => 0,
child: Text('No Items'), 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),
], ],
), ),
), const SpaceHeight(16.0),
), Row(
Padding( mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.all(16.0).copyWith(top: 0), children: [
child: Column( const Text(
children: [ 'Sub total',
const Divider(), style: TextStyle(
const SpaceHeight(16.0), color: AppColors.black,
Row( fontWeight: FontWeight.bold,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'Pajak',
style: TextStyle(
color: AppColors.black,
fontWeight: FontWeight.bold,
),
),
BlocBuilder<CheckoutBloc, CheckoutState>(
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<CheckoutBloc, CheckoutState>(
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',
), ),
), ),
BlocBuilder<CheckoutBloc, CheckoutState>(
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',
),
), ),
], ),
), ],
), ),
], ),
), ],
), ),
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/constants/variables.dart'; import 'package:enaklo_pos/core/constants/variables.dart';
import 'package:enaklo_pos/core/extensions/int_ext.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/bloc/checkout/checkout_bloc.dart';
import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart';
@ -52,7 +51,7 @@ class _OrderMenuState extends State<OrderMenu> {
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(50.0)), borderRadius: BorderRadius.all(Radius.circular(8.0)),
child: child:
// Icon( // Icon(
// Icons.fastfood, // Icons.fastfood,
@ -60,14 +59,23 @@ class _OrderMenuState extends State<OrderMenu> {
// color: AppColors.primary, // color: AppColors.primary,
// ), // ),
CachedNetworkImage( CachedNetworkImage(
imageUrl: widget.data.product.image!.contains('http') imageUrl: widget.data.product.name!.contains('http')
? widget.data.product.image! ? widget.data.product.name!
: '${Variables.baseUrl}/${widget.data.product.image}', : '${Variables.baseUrl}/${widget.data.product.name}',
width: 50.0, width: 50.0,
height: 50.0, height: 50.0,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) => errorWidget: (context, url, error) => Container(
const Icon(Icons.error), width: 50.0,
height: 50.0,
decoration: BoxDecoration(
color: AppColors.disabled.withOpacity(0.4),
),
child: const Icon(
Icons.image,
color: AppColors.grey,
),
),
), ),
), ),
title: Row( title: Row(
@ -86,8 +94,7 @@ class _OrderMenuState extends State<OrderMenu> {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(widget.data.product.price!.toIntegerFromText Text(widget.data.product.price!.currencyFormatRp),
.currencyFormatRp),
], ],
), ),
), ),
@ -139,8 +146,7 @@ class _OrderMenuState extends State<OrderMenu> {
SizedBox( SizedBox(
width: 80.0, width: 80.0,
child: Text( child: Text(
(widget.data.product.price!.toIntegerFromText * (widget.data.product.price! * widget.data.quantity)
widget.data.quantity)
.currencyFormatRp, .currencyFormatRp,
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: const TextStyle( style: const TextStyle(

View File

@ -1,9 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart'; 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/constants/variables.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/data/models/response/product_response_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart';
@ -43,9 +42,9 @@ class ProductCard extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8.0)), borderRadius: BorderRadius.all(Radius.circular(8.0)),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: data.image!.contains('http') imageUrl: data.name!.contains('http')
? data.image! ? data.name!
: '${Variables.baseUrl}/${data.image}', : '${Variables.baseUrl}/${data.name}',
fit: BoxFit.cover, fit: BoxFit.cover,
width: double.infinity, width: double.infinity,
height: 120, height: 120,
@ -76,7 +75,7 @@ class ProductCard extends StatelessWidget {
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
data.category?.name ?? '-', '-',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: AppColors.grey, color: AppColors.grey,
@ -90,7 +89,7 @@ class ProductCard extends StatelessWidget {
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
data.price!.toIntegerFromText.currencyFormatRp, data.price!.currencyFormatRp,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 12,

View File

@ -62,7 +62,7 @@ class SalesListOrder extends StatelessWidget {
), ),
), ),
Text( Text(
product.product.price ?? '', (product.product.price ?? 0) as String,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
), ),
@ -76,7 +76,7 @@ class SalesListOrder extends StatelessWidget {
), ),
), ),
Text( Text(
product.product.price ?? '', (product.product.price ?? 0) as String,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
), ),

View File

@ -20,10 +20,10 @@ class AddProductBloc extends Bloc<AddProductEvent, AddProductState> {
emit(const _Loading()); emit(const _Loading());
final requestData = ProductRequestModel( final requestData = ProductRequestModel(
name: event.product.name!, name: event.product.name!,
price: int.parse(event.product.price!), price: event.product.price!,
stock: event.product.stock!, stock: 0,
categoryId: event.product.categoryId!, categoryId: 0,
isBestSeller: event.product.isFavorite!, isBestSeller: 0,
image: event.image, image: event.image,
); );
log("requestData: ${requestData.toString()}"); log("requestData: ${requestData.toString()}");

View File

@ -1,4 +1,3 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart';
@ -19,7 +18,7 @@ class GetProductsBloc extends Bloc<GetProductsEvent, GetProductsState> {
response.fold( response.fold(
(l) => emit(_Error(l)), (l) => emit(_Error(l)),
(r) { (r) {
emit(_Success(r.data!)); emit(_Success(r.data!.products!));
}, },
); );
}); });

View File

@ -27,15 +27,15 @@ class UpdateProductBloc extends Bloc<UpdateProductEvent, UpdateProductState> {
return; 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')); emit(_Error('Product price is required'));
return; return;
} }
if (event.product.stock == null) { // if (event.product.stock == null) {
emit(_Error('Product stock is required')); // emit(_Error('Product stock is required'));
return; // return;
} // }
if (event.product.categoryId == null) { if (event.product.categoryId == null) {
emit(_Error('Product category is required')); emit(_Error('Product category is required'));
@ -43,8 +43,8 @@ class UpdateProductBloc extends Bloc<UpdateProductEvent, UpdateProductState> {
} }
// Parse price safely // Parse price safely
final price = int.tryParse(event.product.price!); final price = event.product.price!;
if (price == null) { if (price == 0) {
emit(_Error('Invalid price format')); emit(_Error('Invalid price format'));
return; return;
} }
@ -53,11 +53,11 @@ class UpdateProductBloc extends Bloc<UpdateProductEvent, UpdateProductState> {
id: event.product.id, id: event.product.id,
name: event.product.name!, name: event.product.name!,
price: price, price: price,
stock: event.product.stock!, stock: 0,
categoryId: event.product.categoryId!, categoryId: 0,
isBestSeller: event.product.isFavorite ?? 0, // Default to 0 if null isBestSeller: 0, // Default to 0 if null
image: event.image, 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("Update requestData: ${requestData.toString()}");
@ -69,7 +69,8 @@ class UpdateProductBloc extends Bloc<UpdateProductEvent, UpdateProductState> {
(r) async { (r) async {
// Update local database after successful API update // Update local database after successful API update
try { try {
await ProductLocalDatasource.instance.updateProduct(event.product); await ProductLocalDatasource.instance
.updateProduct(event.product);
log("Local product updated successfully"); log("Local product updated successfully");
} catch (e) { } catch (e) {
log("Error updating local product: $e"); log("Error updating local product: $e");

View File

@ -28,9 +28,9 @@ class DetailProductDialog extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: product.image!.contains('http') imageUrl: product.name!.contains('http')
? product.image! ? product.name!
: '${Variables.baseUrl}/${product.image}', : '${Variables.baseUrl}/${product.name}',
fit: BoxFit.cover, fit: BoxFit.cover,
width: 120, width: 120,
height: 120, height: 120,
@ -88,20 +88,20 @@ class DetailProductDialog extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_buildItem( _buildItem(
product.category?.name ?? "-", "-",
"Kategori", "Kategori",
), ),
// _buildItem(
// "${product.stock}",
// "Stok",
// valueColor: product.stock! < 50
// ? AppColors.red
// : product.stock! < 100
// ? Colors.yellow
// : AppColors.green,
// ),
_buildItem( _buildItem(
"${product.stock}", (product.price ?? 0).toString().currencyFormatRpV2,
"Stok",
valueColor: product.stock! < 50
? AppColors.red
: product.stock! < 100
? Colors.yellow
: AppColors.green,
),
_buildItem(
(product.price ?? "0").currencyFormatRpV2,
"Harga", "Harga",
valueColor: AppColors.primary, valueColor: AppColors.primary,
), ),
@ -142,11 +142,11 @@ class DetailProductDialog extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: product.status == 1 ? AppColors.green : AppColors.red, color: product.isActive == true ? AppColors.green : AppColors.red,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
product.status == 1 ? 'Aktif' : 'Tidak Aktif', product.isActive == true ? 'Aktif' : 'Tidak Aktif',
style: const TextStyle( style: const TextStyle(
color: Colors.white, fontSize: 12, fontWeight: FontWeight.w700), color: Colors.white, fontSize: 12, fontWeight: FontWeight.w700),
), ),

View File

@ -57,12 +57,12 @@ class _FormProductDialogState extends State<FormProductDialog> {
// Pre-fill the form with existing product data // Pre-fill the form with existing product data
final product = widget.product!; final product = widget.product!;
nameController!.text = product.name ?? ''; nameController!.text = product.name ?? '';
priceValue = int.tryParse(product.price ?? '0') ?? 0; priceValue = product.price ?? 0;
priceController!.text = priceValue.currencyFormatRp; priceController!.text = priceValue.currencyFormatRp;
stockController!.text = (product.stock ?? 0).toString(); stockController!.text = '';
isBestSeller = product.isFavorite == 1; isBestSeller = false;
printType = product.printerType ?? 'kitchen'; printType = 'kitchen';
imageUrl = product.image; imageUrl = '';
} }
super.initState(); super.initState();
@ -129,72 +129,72 @@ class _FormProductDialogState extends State<FormProductDialog> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
const SpaceHeight(20.0), const SpaceHeight(20.0),
const Text( // const Text(
"Kategori", // "Kategori",
style: TextStyle( // style: TextStyle(
fontSize: 14, // fontSize: 14,
fontWeight: FontWeight.w700, // fontWeight: FontWeight.w700,
), // ),
), // ),
const SpaceHeight(12.0), // const SpaceHeight(12.0),
BlocBuilder<GetCategoriesBloc, GetCategoriesState>( // BlocBuilder<GetCategoriesBloc, GetCategoriesState>(
builder: (context, state) { // builder: (context, state) {
return state.maybeWhen( // return state.maybeWhen(
orElse: () { // orElse: () {
return const Center( // return const Center(
child: CircularProgressIndicator(), // child: CircularProgressIndicator(),
); // );
}, // },
success: (categories) { // success: (categories) {
// Set the selected category if in edit mode and not already set // // Set the selected category if in edit mode and not already set
if (isEditMode && // if (isEditMode &&
selectCategory == null && // selectCategory == null &&
widget.product?.category != null) { // widget.product?.category != null) {
try { // try {
selectCategory = categories.firstWhere( // selectCategory = categories.firstWhere(
(cat) => cat.id == widget.product!.category!.id, // (cat) => cat.id == widget.product!.category!.id,
); // );
} catch (e) { // } catch (e) {
// If no exact match found, leave selectCategory as null // // If no exact match found, leave selectCategory as null
// This will show the hint text instead // // This will show the hint text instead
log("No matching category found for product category ID: ${widget.product!.category!.id}"); // log("No matching category found for product category ID: ${widget.product!.category!.id}");
} // }
} // }
return DropdownButtonHideUnderline( // return DropdownButtonHideUnderline(
child: Container( // child: Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
border: Border.all(color: Colors.grey), // border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(12), // borderRadius: BorderRadius.circular(12),
), // ),
padding: const EdgeInsets.symmetric( // padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), // horizontal: 10, vertical: 5),
child: DropdownButton<CategoryModel>( // child: DropdownButton<CategoryModel>(
value: selectCategory, // value: selectCategory,
hint: const Text("Pilih Kategori"), // hint: const Text("Pilih Kategori"),
isExpanded: true, // Untuk mengisi lebar container // isExpanded: true, // Untuk mengisi lebar container
onChanged: (newValue) { // onChanged: (newValue) {
if (newValue != null) { // if (newValue != null) {
selectCategory = newValue; // selectCategory = newValue;
setState(() {}); // setState(() {});
log("selectCategory: ${selectCategory!.name}"); // log("selectCategory: ${selectCategory!.name}");
} // }
}, // },
items: categories // items: categories
.map<DropdownMenuItem<CategoryModel>>( // .map<DropdownMenuItem<CategoryModel>>(
(CategoryModel category) { // (CategoryModel category) {
return DropdownMenuItem<CategoryModel>( // return DropdownMenuItem<CategoryModel>(
value: category, // value: category,
child: Text(category.name!), // child: Text(category.name!),
); // );
}).toList(), // }).toList(),
), // ),
), // ),
); // );
}, // },
); // );
}, // },
), // ),
const SpaceHeight(12.0), const SpaceHeight(12.0),
const Text( const Text(
"Tipe Print", "Tipe Print",
@ -296,23 +296,23 @@ class _FormProductDialogState extends State<FormProductDialog> {
return; return;
} }
log("isBestSeller: $isBestSeller"); // log("isBestSeller: $isBestSeller");
final String name = nameController!.text; // final String name = nameController!.text;
final int stock = // final int stock =
stockController!.text.toIntegerFromText; // stockController!.text.toIntegerFromText;
final Product product = widget.product!.copyWith( // final Product product = widget.product!.copyWith(
name: name, // name: name,
price: priceValue.toString(), // price: priceValue.toString(),
stock: stock, // stock: stock,
categoryId: selectCategory!.id!, // categoryId: selectCategory!.id!,
isFavorite: isBestSeller ? 1 : 0, // isFavorite: isBestSeller ? 1 : 0,
printerType: printType, // printerType: printType,
); // );
context.read<UpdateProductBloc>().add( // context.read<UpdateProductBloc>().add(
UpdateProductEvent.updateProduct( // UpdateProductEvent.updateProduct(
product, imageFile)); // product, imageFile));
}, },
label: 'Ubah Produk', label: 'Ubah Produk',
); );
@ -354,33 +354,33 @@ class _FormProductDialogState extends State<FormProductDialog> {
orElse: () { orElse: () {
return Button.filled( return Button.filled(
onPressed: () { onPressed: () {
if (selectCategory == null) { // if (selectCategory == null) {
ScaffoldMessenger.of(context).showSnackBar( // ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( // const SnackBar(
content: Text('Please select a category'), // content: Text('Please select a category'),
backgroundColor: Colors.red, // backgroundColor: Colors.red,
), // ),
); // );
return; // return;
} // }
log("isBestSeller: $isBestSeller"); // log("isBestSeller: $isBestSeller");
final String name = nameController!.text; // final String name = nameController!.text;
final int stock = // final int stock =
stockController!.text.toIntegerFromText; // stockController!.text.toIntegerFromText;
final Product product = Product( // final Product product = Product(
name: name, // name: name,
price: priceValue.toString(), // price: priceValue.toString(),
stock: stock, // stock: stock,
categoryId: selectCategory!.id!, // categoryId: selectCategory!.id!,
isFavorite: isBestSeller ? 1 : 0, // isFavorite: isBestSeller ? 1 : 0,
image: imageFile!.path, // image: imageFile!.path,
printerType: printType, // printerType: printType,
); // );
context.read<AddProductBloc>().add( // context.read<AddProductBloc>().add(
AddProductEvent.addProduct( // AddProductEvent.addProduct(
product, imageFile!)); // product, imageFile!));
}, },
label: 'Simpan Produk', label: 'Simpan Produk',
); );
@ -441,14 +441,14 @@ class _FormProductDialogOldState extends State<FormProductDialogOld> {
if (isEditMode) { if (isEditMode) {
// Pre-fill the form with existing product data // Pre-fill the form with existing product data
final product = widget.product!; // final product = widget.product!;
nameController!.text = product.name ?? ''; // nameController!.text = product.name ?? '';
priceValue = int.tryParse(product.price ?? '0') ?? 0; // priceValue = int.tryParse(product.price ?? '0') ?? 0;
priceController!.text = priceValue.currencyFormatRp; // priceController!.text = priceValue.currencyFormatRp;
stockController!.text = (product.stock ?? 0).toString(); // stockController!.text = (product.stock ?? 0).toString();
isBestSeller = product.isFavorite == 1; // isBestSeller = product.isFavorite == 1;
printType = product.printerType ?? 'kitchen'; // printType = product.printerType ?? 'kitchen';
imageUrl = product.image; // imageUrl = product.image;
} }
super.initState(); super.initState();
@ -542,19 +542,19 @@ class _FormProductDialogOldState extends State<FormProductDialogOld> {
}, },
success: (categories) { success: (categories) {
// Set the selected category if in edit mode and not already set // Set the selected category if in edit mode and not already set
if (isEditMode && // if (isEditMode &&
selectCategory == null && // selectCategory == null &&
widget.product?.category != null) { // widget.product?.category != null) {
try { // try {
selectCategory = categories.firstWhere( // selectCategory = categories.firstWhere(
(cat) => cat.id == widget.product!.category!.id, // (cat) => cat.id == widget.product!.category!.id,
); // );
} catch (e) { // } catch (e) {
// If no exact match found, leave selectCategory as null // // If no exact match found, leave selectCategory as null
// This will show the hint text instead // // This will show the hint text instead
log("No matching category found for product category ID: ${widget.product!.category!.id}"); // log("No matching category found for product category ID: ${widget.product!.category!.id}");
} // }
} // }
return DropdownButtonHideUnderline( return DropdownButtonHideUnderline(
child: Container( child: Container(
@ -696,18 +696,18 @@ class _FormProductDialogOldState extends State<FormProductDialogOld> {
final int stock = final int stock =
stockController!.text.toIntegerFromText; stockController!.text.toIntegerFromText;
final Product product = widget.product!.copyWith( // final Product product = widget.product!.copyWith(
name: name, // name: name,
price: priceValue.toString(), // price: priceValue.toString(),
stock: stock, // stock: stock,
categoryId: selectCategory!.id!, // categoryId: selectCategory!.id!,
isFavorite: isBestSeller ? 1 : 0, // isFavorite: isBestSeller ? 1 : 0,
printerType: printType, // printerType: printType,
); // );
context.read<UpdateProductBloc>().add( // context.read<UpdateProductBloc>().add(
UpdateProductEvent.updateProduct( // UpdateProductEvent.updateProduct(
product, imageFile)); // product, imageFile));
}, },
label: 'Update Product', label: 'Update Product',
); );
@ -764,18 +764,18 @@ class _FormProductDialogOldState extends State<FormProductDialogOld> {
final int stock = final int stock =
stockController!.text.toIntegerFromText; stockController!.text.toIntegerFromText;
final Product product = Product( // final Product product = Product(
name: name, // name: name,
price: priceValue.toString(), // price: priceValue.toString(),
stock: stock, // stock: stock,
categoryId: selectCategory!.id!, // categoryId: selectCategory!.id!,
isFavorite: isBestSeller ? 1 : 0, // isFavorite: isBestSeller ? 1 : 0,
image: imageFile!.path, // image: imageFile!.path,
printerType: printType, // printerType: printType,
); // );
context.read<AddProductBloc>().add( // context.read<AddProductBloc>().add(
AddProductEvent.addProduct( // AddProductEvent.addProduct(
product, imageFile!)); // product, imageFile!));
}, },
label: 'Save Product', label: 'Save Product',
); );

View File

@ -62,7 +62,7 @@ class _SyncDataPageState extends State<SyncDataPage> {
.deleteAllProducts(); .deleteAllProducts();
await ProductLocalDatasource.instance await ProductLocalDatasource.instance
.insertProducts( .insertProducts(
productResponseModel.data!, productResponseModel.data!.products!,
); );
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(

View File

@ -36,9 +36,9 @@ class MenuProductItem extends StatelessWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: data.image!.contains('http') imageUrl: data.name!.contains('http')
? data.image! ? data.name!
: '${Variables.baseUrl}/${data.image}', : '${Variables.baseUrl}/${data.name}',
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
width: double.infinity, width: double.infinity,
@ -67,7 +67,7 @@ class MenuProductItem extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
data.category?.name ?? "", "",
style: const TextStyle( style: const TextStyle(
color: AppColors.white, color: AppColors.white,
fontSize: 10, fontSize: 10,
@ -185,7 +185,7 @@ class MenuProductItemOld extends StatelessWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderRadius: const BorderRadius.all(Radius.circular(10.0)),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: '${Variables.baseUrl}/${data.image}', imageUrl: '${Variables.baseUrl}/${data.name}',
placeholder: (context, url) => placeholder: (context, url) =>
const Center(child: CircularProgressIndicator()), const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) => const Icon( errorWidget: (context, url, error) => const Icon(
@ -214,7 +214,7 @@ class MenuProductItemOld extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(
data.category?.name ?? '-', '-',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey, color: Colors.grey,
@ -260,7 +260,7 @@ class MenuProductItemOld extends StatelessWidget {
Radius.circular(10.0)), Radius.circular(10.0)),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: imageUrl:
'${Variables.baseUrl}${data.image}', '${Variables.baseUrl}${data.name}',
placeholder: (context, url) => placeholder: (context, url) =>
const Center( const Center(
child: child:
@ -275,7 +275,7 @@ class MenuProductItemOld extends StatelessWidget {
), ),
const SpaceHeight(10.0), const SpaceHeight(10.0),
Text( Text(
data.category?.name ?? '-', '-',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey, color: Colors.grey,
@ -291,7 +291,7 @@ class MenuProductItemOld extends StatelessWidget {
), ),
const SpaceHeight(10.0), const SpaceHeight(10.0),
Text( Text(
data.stock.toString(), "data.stock.toString()",
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey, color: Colors.grey,

View File

@ -26,7 +26,7 @@ class DraftOrderItem {
Map<String, dynamic> toMapForLocal(int orderId) { Map<String, dynamic> toMapForLocal(int orderId) {
return { return {
'id_draft_order': orderId, 'id_draft_order': orderId,
'id_product': product.productId, 'id_product': product.id,
'quantity': quantity, 'quantity': quantity,
'price': product.price, 'price': product.price,
}; };

View File

@ -65,12 +65,13 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
await ProductLocalDatasource.instance.updateStatusTable(newTable); await ProductLocalDatasource.instance.updateStatusTable(newTable);
// Remove draft order // Remove draft order
await ProductLocalDatasource.instance.removeDraftOrderById(widget.draftOrder!.id!); await ProductLocalDatasource.instance
.removeDraftOrderById(widget.draftOrder!.id!);
// Refresh table status // Refresh table status
context.read<GetTableStatusBloc>().add( context.read<GetTableStatusBloc>().add(
GetTableStatusEvent.getTablesStatus('all'), GetTableStatusEvent.getTablesStatus('all'),
); );
log("Table ${widget.table!.tableName} freed up and draft order removed"); log("Table ${widget.table!.tableName} freed up and draft order removed");
} }
@ -84,6 +85,7 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
} }
} }
} }
@override @override
void initState() { void initState() {
context context
@ -135,7 +137,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('Kembali'), 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: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -146,7 +149,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); // Close dialog 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'), child: const Text('Ya'),
), ),
@ -157,7 +161,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
}, },
child: const Text( child: const Text(
'Kembali', '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<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) { draftName,
orderType) {
if (products.isEmpty) { if (products.isEmpty) {
return const Center( return const Center(
child: Text('No Items'), child: Text('No Items'),
@ -294,13 +300,13 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
products.fold( products.fold(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
)); ));
return Text( return Text(
@ -374,7 +380,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
tax, tax,
); );
final price = state.maybeWhen( final price = state.maybeWhen(
@ -387,13 +394,13 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
products.fold( products.fold(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -408,7 +415,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) { draftName,
orderType) {
return discountAmount; return discountAmount;
}); });
@ -446,7 +454,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
tax, tax,
); );
final price = state.maybeWhen( final price = state.maybeWhen(
@ -459,13 +468,13 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
products.fold( products.fold(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -480,7 +489,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) { draftName,
orderType) {
return discountAmount; return discountAmount;
}); });
@ -494,7 +504,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
serviceCharge, serviceCharge,
); );
@ -536,13 +547,13 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
products.fold( products.fold(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -557,7 +568,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) { draftName,
orderType) {
return discountAmount; return discountAmount;
}); });
@ -571,7 +583,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
serviceCharge, serviceCharge,
); );
@ -585,7 +598,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) => draftName,
orderType) =>
tax, tax,
); );
@ -666,7 +680,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
serviceCharge, serviceCharge,
totalQuantity, totalQuantity,
totalPrice, totalPrice,
draftName, orderType) { draftName,
orderType) {
customerController.text = draftName; customerController.text = draftName;
return TextFormField( return TextFormField(
readOnly: true, readOnly: true,
@ -699,7 +714,8 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
), ),
), ),
const SpaceHeight(12.0), const SpaceHeight(12.0),
BlocBuilder<PaymentMethodsBloc, PaymentMethodsState>( BlocBuilder<PaymentMethodsBloc,
PaymentMethodsState>(
builder: (context, state) { builder: (context, state) {
return state.maybeWhen( return state.maybeWhen(
orElse: () => const Center( orElse: () => const Center(
@ -717,14 +733,16 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
error: (message) => Column( error: (message) => Column(
children: [ children: [
Center( Center(
child: Text('Error loading payment methods: $message'), child: Text(
'Error loading payment methods: $message'),
), ),
const SpaceHeight(16.0), const SpaceHeight(16.0),
Button.filled( Button.filled(
onPressed: () { onPressed: () {
context context
.read<PaymentMethodsBloc>() .read<PaymentMethodsBloc>()
.add(PaymentMethodsEvent.fetchPaymentMethods()); .add(PaymentMethodsEvent
.fetchPaymentMethods());
}, },
label: 'Retry', label: 'Retry',
), ),
@ -739,14 +757,16 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
return Column( return Column(
children: [ children: [
const Center( const Center(
child: Text('No payment methods available'), child: Text(
'No payment methods available'),
), ),
const SpaceHeight(16.0), const SpaceHeight(16.0),
Button.filled( Button.filled(
onPressed: () { onPressed: () {
context context
.read<PaymentMethodsBloc>() .read<PaymentMethodsBloc>()
.add(PaymentMethodsEvent.fetchPaymentMethods()); .add(PaymentMethodsEvent
.fetchPaymentMethods());
}, },
label: 'Retry', label: 'Retry',
), ),
@ -756,15 +776,20 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
// Set default selected payment method if none selected or if current selection is not in the list // Set default selected payment method if none selected or if current selection is not in the list
if (selectedPaymentMethod == null || if (selectedPaymentMethod == null ||
!paymentMethods.any((method) => method.id == selectedPaymentMethod?.id)) { !paymentMethods.any((method) =>
selectedPaymentMethod = paymentMethods.first; method.id ==
selectedPaymentMethod?.id)) {
selectedPaymentMethod =
paymentMethods.first;
} }
return Wrap( return Wrap(
spacing: 12.0, spacing: 12.0,
runSpacing: 8.0, runSpacing: 8.0,
children: paymentMethods.map((method) { children: paymentMethods.map((method) {
final isSelected = selectedPaymentMethod?.id == method.id; final isSelected =
selectedPaymentMethod?.id ==
method.id;
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: 120.0, minWidth: 120.0,
@ -775,31 +800,44 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
color: AppColors.primary, color: AppColors.primary,
width: 2.0, width: 2.0,
), ),
borderRadius: BorderRadius.circular(8.0), borderRadius:
BorderRadius.circular(
8.0),
) )
: null, : null,
child: Tooltip( child: Tooltip(
message: method.description ?? 'No description available', message: method.description ??
'No description available',
child: isSelected child: isSelected
? Button.filled( ? Button.filled(
width: double.infinity, width: double.infinity,
height: 50.0, height: 50.0,
onPressed: () { onPressed: () {
setState(() { setState(() {
selectedPaymentMethod = method; selectedPaymentMethod =
method;
}); });
}, },
label: method.name?.isNotEmpty == true ? method.name! : 'Unknown', label: method.name
?.isNotEmpty ==
true
? method.name!
: 'Unknown',
) )
: Button.outlined( : Button.outlined(
width: double.infinity, width: double.infinity,
height: 50.0, height: 50.0,
onPressed: () { onPressed: () {
setState(() { 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<PaymentTablePage> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Row( title: Row(
children: [ children: [
Icon(Icons.warning, color: AppColors.red), Icon(Icons.warning,
color: AppColors.red),
SizedBox(width: 8), SizedBox(width: 8),
Text('Batalkan Pesanan?'), Text('Batalkan Pesanan?'),
], ],
), ),
content: Text( content: Text(
'Apakah anda yakin ingin membatalkan pesanan untuk meja ${widget.table?.tableName ?? "ini"}?\n\nPesanan akan dihapus secara permanen.'), 'Apakah anda yakin ingin membatalkan pesanan untuk meja ${widget.table?.tableName ?? "ini"}?\n\nPesanan akan dihapus secara permanen.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () =>
Navigator.pop(context),
child: Text('Tidak', child: Text('Tidak',
style: TextStyle(color: AppColors.primary)), style: TextStyle(
color:
AppColors.primary)),
), ),
BlocListener<StatusTableBloc, StatusTableState>( BlocListener<StatusTableBloc,
StatusTableState>(
listener: (context, state) { listener: (context, state) {
state.maybeWhen( state.maybeWhen(
orElse: () {}, orElse: () {},
success: () { success: () {
Navigator.pop(context); // Close void dialog Navigator.pop(
Navigator.pop(context); // Close payment page context); // Close void dialog
ScaffoldMessenger.of(context).showSnackBar( Navigator.pop(
context); // Close payment page
ScaffoldMessenger.of(
context)
.showSnackBar(
const SnackBar( const SnackBar(
content: Text('Pesanan berhasil dibatalkan'), content: Text(
backgroundColor: AppColors.primary, 'Pesanan berhasil dibatalkan'),
backgroundColor:
AppColors.primary,
), ),
); );
}, },
@ -912,34 +961,47 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
}, },
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.red, backgroundColor:
AppColors.red,
), ),
onPressed: () { onPressed: () {
// Void the order // Void the order
if (widget.table != null) { if (widget.table != null) {
final newTable = TableModel( final newTable = TableModel(
id: widget.table!.id, id: widget.table!.id,
tableName: widget.table!.tableName, tableName: widget
.table!.tableName,
status: 'available', status: 'available',
orderId: 0, orderId: 0,
paymentAmount: 0, paymentAmount: 0,
startTime: DateTime.now().toIso8601String(), startTime: DateTime.now()
position: widget.table!.position, .toIso8601String(),
position: widget
.table!.position,
); );
context.read<StatusTableBloc>().add( context
StatusTableEvent.statusTabel(newTable), .read<StatusTableBloc>()
.add(
StatusTableEvent
.statusTabel(
newTable),
); );
} }
// Remove draft order from local storage // Remove draft order from local storage
if (widget.draftOrder?.id != null) { if (widget.draftOrder?.id !=
ProductLocalDatasource.instance null) {
.removeDraftOrderById(widget.draftOrder!.id!); ProductLocalDatasource
.instance
.removeDraftOrderById(
widget.draftOrder!
.id!);
} }
log("Voided order from payment page"); log("Voided order from payment page");
}, },
child: const Text( child: const Text(
"Ya, Batalkan", "Ya, Batalkan",
style: TextStyle(color: Colors.white), style: TextStyle(
color: Colors.white),
), ),
), ),
), ),
@ -1011,8 +1073,7 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue +
(element.product.price! (element.product.price! *
.toIntegerFromText *
element.quantity), element.quantity),
), ),
); );
@ -1080,22 +1141,29 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
child: Button.filled( child: Button.filled(
onPressed: () async { onPressed: () async {
if (selectedPaymentMethod == null) { if (selectedPaymentMethod == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar( const SnackBar(
content: Text('Please select a payment method'), content: Text(
'Please select a payment method'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
return; return;
} }
final paymentMethodName = selectedPaymentMethod?.name?.toLowerCase() ?? ''; final paymentMethodName =
selectedPaymentMethod?.name
?.toLowerCase() ??
'';
log("Selected payment method: ${selectedPaymentMethod?.name} (normalized: $paymentMethodName)"); log("Selected payment method: ${selectedPaymentMethod?.name} (normalized: $paymentMethodName)");
if (paymentMethodName == 'cash' || if (paymentMethodName == 'cash' ||
paymentMethodName == 'tunai' || paymentMethodName == 'tunai' ||
paymentMethodName == 'uang tunai' || paymentMethodName ==
paymentMethodName == 'cash payment') { 'uang tunai' ||
paymentMethodName ==
'cash payment') {
context.read<OrderBloc>().add( context.read<OrderBloc>().add(
OrderEvent.order( OrderEvent.order(
items, items,
@ -1109,7 +1177,9 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
widget.table?.id ?? 0, widget.table?.id ?? 0,
'completed', 'completed',
'paid', 'paid',
selectedPaymentMethod?.name ?? 'Cash', selectedPaymentMethod
?.name ??
'Cash',
totalPriceFinal, totalPriceFinal,
orderType)); orderType));
@ -1149,7 +1219,9 @@ class _PaymentTablePageState extends State<PaymentTablePage> {
widget.table?.id ?? 0, widget.table?.id ?? 0,
'completed', 'completed',
'paid', 'paid',
selectedPaymentMethod?.name ?? 'Unknown Payment Method', selectedPaymentMethod
?.name ??
'Unknown Payment Method',
totalPriceFinal, totalPriceFinal,
orderType)); orderType));